22
33How to add telemetry so every instrumentation reads the same way. The framework
44lives in ` src/telemetry ` ; the per-domain instrumentation business code talks to
5- lives here in ` src/instrumentation ` . See ` cli.ts ` (spans + phases) and ` ssh.ts `
6- (point-in-time logs) as references.
5+ lives here in ` src/instrumentation ` .
76
87## Checklist
98
@@ -19,8 +18,8 @@ lives here in `src/instrumentation`. See `cli.ts` (spans + phases) and `ssh.ts`
1918- No secrets, tokens, query strings, or unbounded values in properties; routes
2019 go through ` normalizeRoute ` .
2120- Let the framework set ` result ` ; add a domain ` outcome ` only when an operation
22- has several success modes. Failures go to a typed union; non-error early exits
23- call ` markAborted ` .
21+ has several success modes. Errors go to a typed ` error.type ` union; non-error
22+ early exits call ` markAborted ` .
2423
2524## Layers
2625
@@ -31,6 +30,15 @@ lives here in `src/instrumentation`. See `cli.ts` (spans + phases) and `ssh.ts`
3130- ** Instrumentation** (` src/instrumentation/* ` ): one typed class per domain, the
3231 only telemetry surface business code sees.
3332
33+ ## Structure
34+
35+ - Split instrumentation files along the same boundaries as the business code,
36+ not one catch-all module.
37+ - Shared span helpers (` recordError ` , ` recordAborted ` ) live in one shared module,
38+ not duplicated per file.
39+ - Record-error-then-rethrow-outside-the-span logic lives once per class, in a
40+ single private helper, not in every ` traceX ` method.
41+
3442## Threading
3543
3644Spans are passed ** explicitly** as a callback argument; there is no
@@ -46,6 +54,15 @@ ambient/active-span context. Two patterns keep telemetry out of business logic:
4654Never return a value purely so a caller can log it; that couples the return type
4755to observability. Returning is fine when the business uses the value too.
4856
57+ ## Callers
58+
59+ - Declare telemetry dimensions explicitly at the call site; pass ` source: "uri" `
60+ rather than inferring it from which arguments happen to be set.
61+ - Keep business bodies in named private ` runX(args, trace) ` methods; the public
62+ method just opens the span and wraps them. Small diffs, named telemetry seam.
63+ - When sibling events share a correlating property, emit it on every event in the
64+ family; don't drop it from new ones.
65+
4966## Spans, phases, logs
5067
5168- ` trace(name, fn, props?, meas?) ` — a span with framework-set ` result ` and
@@ -62,7 +79,7 @@ to observability. Returning is fine when the business uses the value too.
6279| Event name | ` domain.snake_case ` | ` cli.resolve ` , ` remote.setup ` , ` connection.dropped ` |
6380| Point-in-time log | past tense | ` connection.dropped ` , ` ssh.process.lost ` |
6481| Child phase | bare ` snake_case ` | ` cache_lookup ` , ` version_check ` , ` connection_handoff ` |
65- | Property / measurement key | lowercase; ` _ ` splits words, ` . ` for groups | ` cache_source ` , ` failure_category ` , ` status.from ` |
82+ | Property / measurement key | lowercase; ` _ ` splits words, ` . ` for groups | ` cache_source ` , ` error.type ` , ` status.from ` |
6683| Enumerated value | typed ` snake_case ` union | ` "cache_hit" ` , ` "session_token" ` , ` "unrecoverable_close" ` |
6784
6885This is the [ OTel attribute convention] ( https://opentelemetry.io/docs/specs/semconv/general/naming/ ) :
@@ -71,6 +88,10 @@ camelCase. Default to a flat `snake_case` key; use `.` only to group genuinely
7188related attributes (a ` status.from ` / ` status.to ` pair). Keep phase names
7289subject-first within a domain (` agent_resolve ` , not ` resolve_agent ` ).
7390
91+ ** Methods.** Span-wrapper methods are ` trace<Noun> ` (` traceOpen ` ,
92+ ` traceConfirmationPrompt ` ); don't echo the event's past-tense suffix
93+ (` traceUpdateConfirmationPrompted ` ), and drop qualifiers the class implies.
94+
7495** Grouping.** Group related events under a shared dotted namespace so a prefix
7596query returns the whole family: ` workspace.update.triggered ` and
7697` workspace.update.prompted ` both sit under ` workspace.update.* ` , as do
@@ -102,23 +123,39 @@ Coder's own pipeline, so a bare `cache_source` can't collide with a future OTel
102123 ` latency ` , unit ` ms ` , which Prometheus then suffixes itself); log and span
103124 attributes keep the key as written. You always author ` latency_ms ` ; only the
104125 exported metric name changes.
126+ - ** Counts.** Name a count ` <entity>.count ` , singular entity (` agent.count ` ,
127+ ` workspace.count ` ), per OTel (` system.process.count ` ) — not flat ` agent_count ` .
128+ Related counts share the namespace (` agent.count ` , ` agent.connected_count ` ); a
129+ count with no entity (` retry_count ` ) stays flat.
105130
106- ## Outcomes, failures , aborts
131+ ## Outcomes, errors , aborts
107132
108133- The framework sets ` result ` (` success ` / ` error ` / ` aborted ` ) on every span;
109134 don't duplicate it.
110135- Add a domain ` outcome ` property only when an operation has several success
111136 modes worth distinguishing (e.g. ` cli.resolve ` : ` cache_hit ` , ` downloaded ` ).
112- - Classify failures into a typed union (` failure_category ` , or a domain
113- ` reason ` ) via a ` categorize*Failure ` helper rather than emitting raw error
114- strings; the
115- framework already captures the error block.
116- - For a non-error early exit (cancelled, not-found), call ` span.markAborted() `
117- rather than throwing.
137+ - Classify errors into a typed ` error.type ` union via a ` categorize*Error ` helper
138+ rather than emitting raw error strings; the framework captures the error block.
139+ - For a non-error early exit (backed out, not-found), call ` span.markAborted() `
140+ rather than throwing, recording its reason in a separate key, not ` error.type ` .
141+ - A trace exposes one outcome trio — ` abort(stage) ` , ` error(category?) ` ,
142+ ` succeed*(payload) ` — over the shared ` recordAborted ` / ` recordError ` helpers;
143+ each outcome sets one, never two.
144+ - Prefer ** error** over "failure" and ** abort** over "cancel" here (` recordError `
145+ / ` error.type ` / ` error() ` ; ` recordAborted ` / ` markAborted ` / ` abort() ` ).
146+ Point-in-time logs keep past tense (` recovery_failed ` ) — they state what
147+ happened, not a span's ` error ` result.
118148
119149## Safety
120150
121151Never put tokens, credentials, full URLs with query strings, or unbounded user
122152input into properties. Routes go through ` normalizeRoute `
123153(` src/logging/routeNormalization.ts ` ). Prefer a closed union over a free-form
124154string for any property a dashboard groups by.
155+
156+ ## Tests
157+
158+ - Telemetry-only tests of business code are ` <subject>.telemetry.test.ts ` ;
159+ instrumentation modules keep ` <module>.test.ts ` and split when the module does.
160+ - Assert what privacy intends to omit, not only what is present (e.g.
161+ ` workspace_name ` undefined on command events).
0 commit comments