Observability as an engineering discipline — wide events / canonical log lines, OpenTelemetry instrumentation (traces, metrics, context propagation, sampling, Collector), SLIs/SLOs/error budgets, symptom-based alerting with burn rates, telemetry hygiene, and testing instrumentation as behavior. Use when calls between services cannot be correlated, another team cannot line their side of a call up with ours or find the request it came from, a request or trace id has to cross a service hop, a failure cannot be debugged from the logs, or when instrumenting a service, designing SLOs or alerts, choosing what to log/trace/measure, investigating production unknowns, or reviewing telemetry cost and cardinality. For log transport and shape (stdout, JSON, levels, timestamps) see twelve-factor; for CI failure diagnosis see ci-debugging; for where instrumentation lives in ports-and-adapters codebases see hexagonal-architecture; for environment drift see production-parity-skill-builder; for HTTP error response shape see ap
Instrument for the questions you haven't asked yet. Monitoring verifies failure modes you predicted; observability lets you interrogate the system about failures you never anticipated. Effective telemetry answers two questions — what's broken (symptom) and why (cause) — and the effort split is lopsided: spend far more on catching symptoms than on enumerating causes (Google SRE, Monitoring Distributed Systems).
This skill covers what goes into telemetry and how it is consumed. The twelve-factor skill owns log transport and shape (structured records on platform-captured process streams, levels, timestamps). The hexagonal-architecture skill owns where instrumentation code lives in ports-and-adapters codebases.
Deep-dive resources are in the resources/ directory. Load them on demand:
Resource
Load when...
node-patterns.md
Wiring OpenTelemetry into a Node/TypeScript service — NodeSDK setup, --import loading, wide-event middleware, log-trace correlation, Collector config, semantic-convention cheat sheet
Writing Vitest tests for instrumentation — in-memory exporters, asserting wide-event fields, fakes for telemetry ports
references.md
Checking the rationale or original sources behind this guidance
When to Use
Instrumenting a new or existing service (backend, worker, API)
Defining SLIs/SLOs or an error budget policy
Designing, reviewing, or pruning alerts
"We can't see what production is doing" — debugging unknown-unknowns
Reviewing telemetry spend, metric cardinality, or sampling strategy
Not this skill: diagnosing a failing CI run (ci-debugging), local-vs-production drift (production-parity-skill-builder), Core Web Vitals optimization (core-web-vitals where installed).
The Wide Event (Canonical Log Line)
The opinionated default: emit one structured, information-dense event per request per service. This is Stripe's canonical log line — "one long log line at the end that includes many of their key characteristics" (Stripe) — and the "arbitrarily-wide structured event" at the heart of modern observability (Charity Majors).
The mechanics:
Middleware creates a request-scoped accumulator when the request enters the service
Business logic and middleware add fields as work happens
The event is emitted once, at the end of the request, in finally/teardown logic — it must survive the exception path, because that is exactly when you need it
Construction rule — one exit, proved by a throw. Open the try before anything that can throw: request parsing and body reads included, not just the business call. The only call site that finishes the event — setting the outcome attributes, recording the exception, ending the span or emitting the record — sits in the matching finally. A helper that does all three is fine, but it is called from the finally and from nowhere else. A helper invoked on the success path, paired with a finally that merely ends a span or flushes, still loses every field on the throw. Before calling the work done, run a test that makes the handler throw outside its own catch and assert that one event carrying the outcome is still recorded.
What goes in it:
Category
Example fields
Request
method, normalized route template (omit when unavailable), status, duration
Identity
user/principal ID, auth method, API key ID
Rate limiting
allowed, quota, remaining
Performance
DB query count, cache hits, external call timings
Business context
which rule fired, rejection reason, feature flags
Error
error code, error class, retry count
Correlation
trace ID, request ID, build/deploy ID
High-cardinality identifiers belong in events, subject to privacy policy — request IDs, build IDs, and privacy-approved or pseudonymous subject identifiers can make an event useful. High cardinality is a feature for events and a cost explosion on metrics, but event suitability is never permission to emit personal data (see Sampling and Cost Economics and Telemetry Hygiene below).
Why this beats scattered log lines: the data arrives pre-joined. You query complete rows instead of reconstructing a request from fragments with regex and hope. An OpenTelemetry root span with rich attributes is a valid implementation of the same pattern — instrumenting via spans satisfies both the wide-event and the tracing camps at once.
Pillars, Honestly
The industry framing is "three pillars": logs, metrics, traces. The serious critique (Majors, Observability 1.0 vs 2.0): three pillars means many sources of truth scattered across tools, each request stored several times, engineers correlating by hand, and cost multiplied per pillar. The alternative: one source of truth — wide structured events — from which metrics, traces, and SLOs are derived at read time.
House position:
Wide events are the instrumentation default. They avoid duplicate
instrumentation and preserve more future questions, but export, processing,
and storage still need an explicit telemetry cost budget.
Metrics still earn their place for cheap, long-retention aggregates and alerting math — with strictly bounded label sets.
Traces are wide events with structure — parent/child causality across services. Instrument via OTel spans and you get both.
Honest limit: deriving everything at read time assumes a backend that can aggregate over raw events at scale. Not every stack has one. Don't cargo-cult either camp — emit wide events regardless (the data is the asset; backends change), and keep a small set of bounded metrics for dashboards and alert rules.
OpenTelemetry: The Substrate
OpenTelemetry (OTel) is the vendor-neutral standard for producing telemetry. Adopting it means the instrumentation outlives any backend choice.
The pieces:
Signals — traces, metrics, logs, all exported over OTLP
Resource attributes — service.name is mandatory; it identifies the emitting service in every backend
Semantic conventions — standardized attribute names (http.request.method, db.system) that make telemetry correlatable across teams and tools (semconv). Never invent an attribute name semconv already defines.
Context propagation — the W3C traceparent header carries trace ID and parent span ID across every service hop, which is what makes distributed traces exist at all (context propagation). Whenever your code builds an outbound request to another service — through a vendor SDK, a generated client, or hand-written headers — inject the active context into that request's headers (propagation.inject(context.active(), headers)) instead of writing a correlation header of your own. A downstream team asking for "something we can search on to line your call up with ours" is asking for traceparent, whether or not they know the name: x-trace-id, x-correlation-id and a bare trace id under any custom key drop the parent span id and the sampling flag, and no standard tooling joins them to anything
The Collector — a receive → process → export pipeline that runs beside your services. It is the recommended production default for batching, retry, redaction, and backend swaps without code changes. A documented direct-export design is acceptable when the deployment is simple and the SDK/backend path demonstrably meets its buffering, retry, backpressure, filtering, and operational requirements (Collector)
Minimal TypeScript adoption:@opentelemetry/sdk-node + @opentelemetry/api + @opentelemetry/auto-instrumentations-node, an instrumentation file loaded before application code via node --import ./instrumentation.mjs (or a reviewed, repository-installed TypeScript runner during development), OTLP exporters. Auto-instrumentation gives HTTP/framework/DB spans for free; manual attributes add the business meaning that makes traces queryable. Initialization order matters — instrumentation loaded after the app's modules silently captures nothing. See resources/node-patterns.md.
Sampling and Cost Economics
The cardinality routing rule
Every unique label combination on a metric is a separate time series. Add tenant_id (10,000 values) to a metric with 1,000 existing series and you have 10 million series, not 11,000; each active series costs memory and most vendors bill per series or per custom metric (Grafana on high cardinality).
The rule: bounded, low-cardinality dimensions go on metrics; unbounded, high-cardinality dimensions go on events/spans. Request-derived labels are safe only after normalization to a fixed allowlist (for example, a known method or result category); map unknown values to _OTHER or omit them. Never use raw or unbounded attacker-controlled values such as IDs, paths, query strings, arbitrary headers, or body fields. When someone asks "can we break this metric down by customer?", the answer is "that question belongs to the event store."
The sampling ladder
Stage
When
How
No sampling
Low volume
Keep everything — sampling is a cost tool, not a virtue
Head sampling
Volume grows
Decision at trace start (probabilistic on trace ID), propagates consistently, cheap — but cannot guarantee capturing errors
Tail sampling
Decisions need completed-trace attributes
Decision after the full trace arrives; can preferentially retain selected errors and outliers, but requires a stateful Collector tier — buffering, scaling, operational cost, possible lock-in
Never silently sample the stream your SLOs are computed from. If sampling is unavoidable there, account for it in the SLI math. Configure and verify error-trace retention separately; tail sampling policy and capacity determine what is actually retained.
SLIs, SLOs, Error Budgets
Definitions from the Google SRE book: an SLI is a carefully defined quantitative measure of service level; an SLO is a target value for that SLI; an SLA is an SLO plus consequences. The error budget is 100% minus the SLO — and its purpose is to license shipping: spend the budget on releases instead of chasing a 100% that users can't distinguish anyway.
SLI menus (mnemonics for shopping, not mandates):
RED — Rate, Errors, Duration — per request-driven service; explicitly a proxy for user experience (Tom Wilkie)
USE — Utilization, Saturation, Errors — per hardware resource; infrastructure-focused (Brendan Gregg)
Four Golden Signals — latency, traffic, errors, saturation — with two nuances everyone drops: track latency of failed requests separately (a slow 500 is a different pathology from a fast 500), and use histograms because averages hide the tail — at 1,000 rps averaging 100ms, 1% of requests can easily take 5s (SRE book)
SLO discipline: keep few SLOs, each simple enough to explain in a sentence. Define availability, correctness, and freshness as good events over valid events. Define latency objectives as the proportion of valid events below an explicit threshold; use percentiles rather than means when diagnosing latency distributions. Keep internal targets slightly stricter than published ones. Don't overachieve — users come to depend on the reliability you deliver, not the one you promised.
Alerting: Symptoms, Pages, Burn Rates
Page on symptoms, not causes. "Do your users care if your MySQL servers are down? No, they care if their queries are failing" (Rob Ewaschuk, My Philosophy on Alerting — the doc that became SRE book chapter 6). Cause-based data belongs on dashboards and in tickets, not pages. The rare exception: imminent, definite causes (quota exhaustion in 4 hours).
Every page must be: urgent, actionable, user-visible, and require human intelligence to handle. If the response to a page could be scripted, script it and delete the page. Measure false pages, then fix or remove alerts that repeatedly waste responders' attention.
The default alert construction is the multiwindow, multi-burn-rate alert (SRE Workbook). Burn rate = how fast you consume error budget relative to the SLO (burn rate 1 = exactly out of budget at period end). For a 99.9% SLO over 30 days:
Severity
Burn rate
Long window
Short window
Budget consumed
Page
14.4
1 h
5 m
2%
Page
6
6 h
30 m
5%
Ticket
1
3 d
6 h
10%
The short window (1/12 of the long) confirms the problem is still happening, which fixes the reset-time failure of naive threshold alerts. This is the only construction that scores well on precision, recall, detection time, and reset time simultaneously — the derivation is in resources/slo-alerting.md.
Every page links a runbook — a concise "what this alert means and current mitigations", not an exhaustive troubleshooting tree.
Structured Logging Craft
The twelve-factor skill owns transport and shape (structured records on platform-captured process streams, recognized levels, ISO timestamps). This section is about content discipline.
The levels test (from Dave Cheney): "nobody reads warnings, because by definition nothing went wrong," and "if you choose to handle the error by logging it, by definition it's not an error any more." Keep the standard four levels — but apply Cheney's test to every line: every warn needs a named reader; every error log must correspond to a genuinely unhandled failure, not a handled one being double-reported.
Log at boundaries; accumulate in between. Most in-request info chatter should become fields on the wide event, not separate lines. A request that produces 40 log lines produces one canonical event plus a handful of genuinely independent facts.
Correlation: every log record carries the active trace ID (W3C trace context), so logs join to traces and to the canonical event for free.
PII and secret hygiene:
Never emit passwords, tokens, API keys, cookies, or session IDs into any signal
Emit personal identifiers only when a stated operational need and the applicable privacy policy permit it; minimize or pseudonymize them and apply appropriate retention, access, and deletion controls
Allowlist named fields — never serialize whole request/user/config objects; a JSON serializer will happily dump auth headers
Redact at source, in the app — the pipeline is a second line of defense, not the first
In regulated environments, every log line containing PII becomes a compliance obligation (retention, access control, right-to-erasure)
Where Instrumentation Lives (Architecture Placement)
In ports-and-adapters codebases, observability code has four homes — the four-tier model (full treatment: hexagonal-architecture skill, resources/cross-cutting-concerns.md):
Technical telemetry → adapters. Request/response logging, SQL timings, retries, auto-instrumentation. Never in domain code.
Domain-significant observations → an explicit driven port (Domain Probe) or domain events. When an intermediate business fact matters ("which pricing rule fired") or the observation is a requirement (support logging, business metrics), the observability backend is a driven actor behind a per-capability, severity-free, fire-and-forget port — never a generic Logger port. Where domain events already exist, an observability subscriber beats a second channel.
Correlation and wide-event assembly → middleware/adapters only. The domain never sees a trace ID. Domain dimensions reach the wide event via result types, the probe, or events.
Instrumentation is tested behavior. A probe is a driven port; every driven port gets a fake (see Testing Observability below).
Even without hexagonal architecture, the same instinct applies: business logic returns data and announces facts; edges translate those into telemetry.
Testing Observability
Instrumentation is behavior, so it is test-driven like behavior (see the tdd and testing skills).
In-memory exporters — the OTel SDKs ship them — let Vitest assert on every span, attribute, and status without network calls: "this request emits one canonical event containing pledge.rejection_reason", "this failure sets span status to error". See resources/testing-telemetry.md.
Telemetry ports get fakes, same as any driven port — a recording fake accumulates observations and tests assert on them through the public API. Worked example: hexagonal-architecture skill, resources/testing-hex-arch.md.
Alert rules are code. Where the stack allows (Prometheus rule unit tests, SLO-as-code tools), test that the burn-rate expression fires on synthetic data.
Mutation-testing note: an unasserted probe/telemetry call is a surviving-mutant farm — which is itself the argument for asserting observations.
Honest limits: you cannot meaningfully unit-test sampling percentages, Collector pipelines, or backend retention. Verify those in a staging environment with a real Collector — and note that sampling config is itself a parity surface (staging at 100%, prod at 1% behave differently under debugging).
Frontend Note (Out of Scope for v1)
Browser observability is RUM (real-user monitoring) with Core Web Vitals as the standard SLIs — LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1 at the 75th percentile of real page loads; lab tools are "not a substitute for field measurement" (web.dev). This skill covers services; for the user-facing half see the core-web-vitals and performance skills where installed.
Anti-Patterns
#
Anti-Pattern
Why It's Wrong
1
Unbounded label on a metric (user ID, raw URL, container ID)
Cardinality explosion — memory, query time, and bills scale per series
2
Scattered log lines instead of an accumulated wide event
The event vanishes exactly when it matters most; emit in finally
4
Paging on causes (CPU, disk, replica lag)
Users don't experience causes; symptom pages catch more with less noise
5
A page without a runbook or with no possible action
Pages that can't be acted on train people to ignore pages
6
Serializing whole objects into logs
Auth headers, tokens, and PII ride along; allowlist named fields
7
Inventing attribute names semconv already defines
Breaks cross-team and cross-tool correlation for zero benefit
8
Sampling the stream SLOs are computed from, silently
SLI math becomes fiction; retain errors at 100% or adjust the math
9
console.log debugging left behind as "instrumentation"
Unstructured, unqueryable, uncorrelated — remove or promote to a real field. The removal is scoped to the file, not to the request: when a file you are already editing carries this noise on paths you were not asked about, take all of it out of that file — half-cleaned leaves lines nobody can query and nobody will come back for
10
One vendor agent per signal instead of OTel
Locks instrumentation to a backend; OTel makes backends swappable
11
Alerting on every error the moment it happens
Error budgets exist so that noise below the burn-rate threshold stays out of pagers
Every request emits exactly one canonical wide event, including on the exception path
Privacy-approved high-cardinality identifiers (subject, request, build, tenant) live on events/spans, never on metric labels
Metric labels use bounded normalized allowlists; unknown request values map to _OTHER or are omitted, and raw attacker-controlled values never become labels
service.name and resource attributes are set; semantic-convention names used where they exist
Trace context (W3C traceparent) propagates across every service hop and into every log record
OTel SDK initializes before application code loads (--import), auto-instrumentation enabled
Production telemetry uses the recommended Collector path, or a documented direct-export exception meets buffering, retry, backpressure, filtering, and operational requirements
Sampling and Collector capacity meet a stated error-trace retention objective and loss budget; use unsampled collection when the objective cannot tolerate sampling loss
Each user journey has a handful of ratio-based SLOs at most; latency thresholds and diagnostic percentiles are explicit, with an error budget policy
Paging alerts are symptom-based and burn-rate-driven (multiwindow); each links a runbook
No page fires for a condition with no immediate human action
Telemetry contains no secrets; personal identifiers are necessary, policy-approved, minimized or pseudonymized, and fields are allowlisted and redacted at source
Instrumentation is covered by tests (in-memory exporter or fake-probe assertions)
Adding a metric label or new signal triggers a written cardinality/cost estimate
1---2name: observability3description: Observability as an engineering discipline — wide events / canonical log lines, OpenTelemetry instrumentation (traces, metrics, context propagation, sampling, Collector), SLIs/SLOs/error budgets, symptom-based alerting with burn rates, telemetry hygiene, and testing instrumentation as behavior. Use when calls between services cannot be correlated, another team cannot line their side of a call up with ours or find the request it came from, a request or trace id has to cross a service hop, a failure cannot be debugged from the logs, or when instrumenting a service, designing SLOs or alerts, choosing what to log/trace/measure, investigating production unknowns, or reviewing telemetry cost and cardinality. For log transport and shape (stdout, JSON, levels, timestamps) see twelve-factor; for CI failure diagnosis see ci-debugging; for where instrumentation lives in ports-and-adapters codebases see hexagonal-architecture; for environment drift see production-parity-skill-builder; for HTTP error response shape see ap4---56# Observability78**Instrument for the questions you haven't asked yet.** Monitoring verifies failure modes you predicted; observability lets you interrogate the system about failures you never anticipated. Effective telemetry answers two questions — *what's broken* (symptom) and *why* (cause) — and the effort split is lopsided: spend far more on catching symptoms than on enumerating causes ([Google SRE, Monitoring Distributed Systems](https://sre.google/sre-book/monitoring-distributed-systems/)).910This skill covers what goes *into* telemetry and how it is consumed. The `twelve-factor` skill owns log transport and shape (structured records on platform-captured process streams, levels, timestamps). The `hexagonal-architecture` skill owns where instrumentation code lives in ports-and-adapters codebases.1112**Deep-dive resources** are in the `resources/` directory. Load them on demand:1314| Resource | Load when... |15|----------|-------------|16| `node-patterns.md` | Wiring OpenTelemetry into a Node/TypeScript service — NodeSDK setup, `--import` loading, wide-event middleware, log-trace correlation, Collector config, semantic-convention cheat sheet |17| `slo-alerting.md` | Defining SLIs/SLOs, computing burn rates, building multiwindow multi-burn-rate alert rules, writing runbooks |18| `testing-telemetry.md` | Writing Vitest tests for instrumentation — in-memory exporters, asserting wide-event fields, fakes for telemetry ports |19| `references.md` | Checking the rationale or original sources behind this guidance |2021---2223## When to Use2425- Instrumenting a new or existing service (backend, worker, API)26- Defining SLIs/SLOs or an error budget policy27- Designing, reviewing, or pruning alerts28- "We can't see what production is doing" — debugging unknown-unknowns29- Reviewing telemetry spend, metric cardinality, or sampling strategy3031**Not this skill:** diagnosing a failing CI run (`ci-debugging`), local-vs-production drift (`production-parity-skill-builder`), Core Web Vitals optimization (`core-web-vitals` where installed).3233---3435## The Wide Event (Canonical Log Line)3637**The opinionated default: emit one structured, information-dense event per request per service.** This is Stripe's canonical log line — "one long log line at the end that includes many of their key characteristics" ([Stripe](https://stripe.com/blog/canonical-log-lines)) — and the "arbitrarily-wide structured event" at the heart of modern observability ([Charity Majors](https://charity.wtf/2019/02/05/logs-vs-structured-events/)).3839**The mechanics:**40411. Middleware creates a request-scoped accumulator when the request enters the service422. Business logic and middleware add fields as work happens433. The event is emitted once, at the end of the request, in `finally`/teardown logic — **it must survive the exception path**, because that is exactly when you need it4445**Construction rule — one exit, proved by a throw.** Open the `try` before anything that can throw: request parsing and body reads included, not just the business call. The *only* call site that finishes the event — setting the outcome attributes, recording the exception, ending the span or emitting the record — sits in the matching `finally`. A helper that does all three is fine, but it is called from the `finally` and from nowhere else. A helper invoked on the success path, paired with a `finally` that merely ends a span or flushes, still loses every field on the throw. Before calling the work done, run a test that makes the handler throw *outside* its own `catch` and assert that one event carrying the outcome is still recorded.4647**What goes in it:**4849| Category | Example fields |50|----------|---------------|51| Request | method, normalized route template (omit when unavailable), status, duration |52| Identity | user/principal ID, auth method, API key ID |53| Rate limiting | allowed, quota, remaining |54| Performance | DB query count, cache hits, external call timings |55| Business context | which rule fired, rejection reason, feature flags |56| Error | error code, error class, retry count |57| Correlation | trace ID, request ID, build/deploy ID |5859**High-cardinality identifiers belong in events, subject to privacy policy** — request IDs, build IDs, and privacy-approved or pseudonymous subject identifiers can make an event useful. High cardinality is a feature for events and a cost explosion on metrics, but event suitability is never permission to emit personal data (see Sampling and Cost Economics and Telemetry Hygiene below).6061Why this beats scattered log lines: the data arrives pre-joined. You query complete rows instead of reconstructing a request from fragments with regex and hope. An OpenTelemetry root span with rich attributes is a valid implementation of the same pattern — instrumenting via spans satisfies both the wide-event and the tracing camps at once.6263---6465## Pillars, Honestly6667The industry framing is "three pillars": logs, metrics, traces. The serious critique (Majors, [Observability 1.0 vs 2.0](https://charity.wtf/2024/11/19/there-is-only-one-key-difference-between-observability-1-0-and-2-0/)): three pillars means many sources of truth scattered across tools, each request stored several times, engineers correlating by hand, and cost multiplied per pillar. The alternative: one source of truth — wide structured events — from which metrics, traces, and SLOs are *derived* at read time.6869**House position:**7071- **Wide events are the instrumentation default.** They avoid duplicate72 instrumentation and preserve more future questions, but export, processing,73 and storage still need an explicit telemetry cost budget.74- **Metrics still earn their place** for cheap, long-retention aggregates and alerting math — with strictly bounded label sets.75- **Traces are wide events with structure** — parent/child causality across services. Instrument via OTel spans and you get both.7677**Honest limit:** deriving everything at read time assumes a backend that can aggregate over raw events at scale. Not every stack has one. Don't cargo-cult either camp — emit wide events regardless (the data is the asset; backends change), and keep a small set of bounded metrics for dashboards and alert rules.7879---8081## OpenTelemetry: The Substrate8283OpenTelemetry (OTel) is the vendor-neutral standard for producing telemetry. Adopting it means the instrumentation outlives any backend choice.8485**The pieces:**8687- **Signals** — traces, metrics, logs, all exported over OTLP88- **Resource attributes** — `service.name` is mandatory; it identifies the emitting service in every backend89- **Semantic conventions** — standardized attribute names (`http.request.method`, `db.system`) that make telemetry correlatable across teams and tools ([semconv](https://opentelemetry.io/docs/specs/semconv/)). **Never invent an attribute name semconv already defines.**90- **Context propagation** — the W3C `traceparent` header carries trace ID and parent span ID across every service hop, which is what makes distributed traces exist at all ([context propagation](https://opentelemetry.io/docs/concepts/context-propagation/)). **Whenever your code builds an outbound request to another service — through a vendor SDK, a generated client, or hand-written headers — inject the active context into that request's headers (`propagation.inject(context.active(), headers)`) instead of writing a correlation header of your own.** A downstream team asking for "something we can search on to line your call up with ours" is asking for `traceparent`, whether or not they know the name: `x-trace-id`, `x-correlation-id` and a bare trace id under any custom key drop the parent span id and the sampling flag, and no standard tooling joins them to anything91- **The Collector** — a receive → process → export pipeline that runs beside your services. It is the recommended production default for batching, retry, redaction, and backend swaps without code changes. A documented direct-export design is acceptable when the deployment is simple and the SDK/backend path demonstrably meets its buffering, retry, backpressure, filtering, and operational requirements ([Collector](https://opentelemetry.io/docs/collector/))9293**Minimal TypeScript adoption:** `@opentelemetry/sdk-node` + `@opentelemetry/api` + `@opentelemetry/auto-instrumentations-node`, an instrumentation file loaded *before* application code via `node --import ./instrumentation.mjs` (or a reviewed, repository-installed TypeScript runner during development), OTLP exporters. Auto-instrumentation gives HTTP/framework/DB spans for free; manual attributes add the business meaning that makes traces queryable. Initialization order matters — instrumentation loaded after the app's modules silently captures nothing. See `resources/node-patterns.md`.9495---9697## Sampling and Cost Economics9899### The cardinality routing rule100101Every unique label combination on a metric is a separate time series. Add `tenant_id` (10,000 values) to a metric with 1,000 existing series and you have 10 million series, not 11,000; each active series costs memory and most vendors bill per series or per custom metric ([Grafana on high cardinality](https://grafana.com/blog/how-to-manage-high-cardinality-metrics-in-prometheus-and-kubernetes/)).102103**The rule: bounded, low-cardinality dimensions go on metrics; unbounded, high-cardinality dimensions go on events/spans.** Request-derived labels are safe only after normalization to a fixed allowlist (for example, a known method or result category); map unknown values to `_OTHER` or omit them. Never use raw or unbounded attacker-controlled values such as IDs, paths, query strings, arbitrary headers, or body fields. When someone asks "can we break this metric down by customer?", the answer is "that question belongs to the event store."104105### The sampling ladder106107| Stage | When | How |108|-------|------|-----|109| No sampling | Low volume | Keep everything — sampling is a cost tool, not a virtue |110| Head sampling | Volume grows | Decision at trace start (probabilistic on trace ID), propagates consistently, cheap — but cannot guarantee capturing errors |111| Tail sampling | Decisions need completed-trace attributes | Decision after the full trace arrives; can preferentially retain selected errors and outliers, but requires a stateful Collector tier — buffering, scaling, operational cost, possible lock-in |112113([OTel sampling concepts](https://opentelemetry.io/docs/concepts/sampling/))114115**Never silently sample the stream your SLOs are computed from.** If sampling is unavoidable there, account for it in the SLI math. Configure and verify error-trace retention separately; tail sampling policy and capacity determine what is actually retained.116117---118119## SLIs, SLOs, Error Budgets120121Definitions from the [Google SRE book](https://sre.google/sre-book/service-level-objectives/): an **SLI** is a carefully defined quantitative measure of service level; an **SLO** is a target value for that SLI; an **SLA** is an SLO plus consequences. The **error budget** is 100% minus the SLO — and its purpose is to *license shipping*: spend the budget on releases instead of chasing a 100% that users can't distinguish anyway.122123**SLI menus (mnemonics for shopping, not mandates):**124125- **RED** — Rate, Errors, Duration — per request-driven service; explicitly a proxy for user experience ([Tom Wilkie](https://grafana.com/blog/the-red-method-how-to-instrument-your-services/))126- **USE** — Utilization, Saturation, Errors — per hardware resource; infrastructure-focused (Brendan Gregg)127- **Four Golden Signals** — latency, traffic, errors, saturation — with two nuances everyone drops: track latency of *failed* requests separately (a slow 500 is a different pathology from a fast 500), and use histograms because averages hide the tail — at 1,000 rps averaging 100ms, 1% of requests can easily take 5s ([SRE book](https://sre.google/sre-book/monitoring-distributed-systems/))128129**SLO discipline:** keep few SLOs, each simple enough to explain in a sentence. Define availability, correctness, and freshness as good events over valid events. Define latency objectives as the proportion of valid events below an explicit threshold; use percentiles rather than means when diagnosing latency distributions. Keep internal targets slightly stricter than published ones. Don't overachieve — users come to depend on the reliability you deliver, not the one you promised.130131---132133## Alerting: Symptoms, Pages, Burn Rates134135**Page on symptoms, not causes.** "Do your users care if your MySQL servers are down? No, they care if their queries are failing" ([Rob Ewaschuk, My Philosophy on Alerting](https://docs.google.com/document/d/199PqyG3UsyXlwieHaqbGiWVa8eMWi8zzAn0YfcApr8Q/mobilebasic) — the doc that became SRE book chapter 6). Cause-based data belongs on dashboards and in tickets, not pages. The rare exception: imminent, definite causes (quota exhaustion in 4 hours).136137**Every page must be:** urgent, actionable, user-visible, and require human intelligence to handle. If the response to a page could be scripted, script it and delete the page. Measure false pages, then fix or remove alerts that repeatedly waste responders' attention.138139**The default alert construction is the multiwindow, multi-burn-rate alert** ([SRE Workbook](https://sre.google/workbook/alerting-on-slos/)). Burn rate = how fast you consume error budget relative to the SLO (burn rate 1 = exactly out of budget at period end). For a 99.9% SLO over 30 days:140141| Severity | Burn rate | Long window | Short window | Budget consumed |142|----------|-----------|-------------|--------------|-----------------|143| Page | 14.4 | 1 h | 5 m | 2% |144| Page | 6 | 6 h | 30 m | 5% |145| Ticket | 1 | 3 d | 6 h | 10% |146147The short window (1/12 of the long) confirms the problem is *still happening*, which fixes the reset-time failure of naive threshold alerts. This is the only construction that scores well on precision, recall, detection time, and reset time simultaneously — the derivation is in `resources/slo-alerting.md`.148149**Every page links a runbook** — a concise "what this alert means and current mitigations", not an exhaustive troubleshooting tree.150151---152153## Structured Logging Craft154155The `twelve-factor` skill owns transport and shape (structured records on platform-captured process streams, recognized levels, ISO timestamps). This section is about *content discipline*.156157**The levels test** (from [Dave Cheney](https://dave.cheney.net/2015/11/05/lets-talk-about-logging)): "nobody reads warnings, because by definition nothing went wrong," and "if you choose to handle the error by logging it, by definition it's not an error any more." Keep the standard four levels — but apply Cheney's test to every line: every `warn` needs a named reader; every `error` log must correspond to a genuinely unhandled failure, not a handled one being double-reported.158159**Log at boundaries; accumulate in between.** Most in-request `info` chatter should become fields on the wide event, not separate lines. A request that produces 40 log lines produces one canonical event plus a handful of genuinely independent facts.160161**Correlation:** every log record carries the active trace ID (W3C trace context), so logs join to traces and to the canonical event for free.162163**PII and secret hygiene:**164165- Never emit passwords, tokens, API keys, cookies, or session IDs into any signal166- Emit personal identifiers only when a stated operational need and the applicable privacy policy permit it; minimize or pseudonymize them and apply appropriate retention, access, and deletion controls167- **Allowlist named fields** — never serialize whole request/user/config objects; a JSON serializer will happily dump auth headers168- Redact at source, in the app — the pipeline is a second line of defense, not the first169- In regulated environments, every log line containing PII becomes a compliance obligation (retention, access control, right-to-erasure)170171---172173## Where Instrumentation Lives (Architecture Placement)174175In ports-and-adapters codebases, observability code has four homes — the four-tier model (full treatment: `hexagonal-architecture` skill, `resources/cross-cutting-concerns.md`):1761771. **Technical telemetry → adapters.** Request/response logging, SQL timings, retries, auto-instrumentation. Never in domain code.1782. **Domain-significant observations → an explicit driven port (Domain Probe) or domain events.** When an intermediate business fact matters ("which pricing rule fired") or the observation is a requirement (support logging, business metrics), the observability backend is a driven actor behind a per-capability, severity-free, fire-and-forget port — never a generic `Logger` port. Where domain events already exist, an observability subscriber beats a second channel.1793. **Correlation and wide-event assembly → middleware/adapters only.** The domain never sees a trace ID. Domain dimensions reach the wide event via result types, the probe, or events.1804. **Instrumentation is tested behavior.** A probe is a driven port; every driven port gets a fake (see Testing Observability below).181182Even without hexagonal architecture, the same instinct applies: business logic returns data and announces facts; edges translate those into telemetry.183184---185186## Testing Observability187188Instrumentation is behavior, so it is test-driven like behavior (see the `tdd` and `testing` skills).189190- **In-memory exporters** — the OTel SDKs ship them — let Vitest assert on every span, attribute, and status without network calls: "this request emits one canonical event containing `pledge.rejection_reason`", "this failure sets span status to error". See `resources/testing-telemetry.md`.191- **Telemetry ports get fakes**, same as any driven port — a recording fake accumulates observations and tests assert on them through the public API. Worked example: `hexagonal-architecture` skill, `resources/testing-hex-arch.md`.192- **Alert rules are code.** Where the stack allows (Prometheus rule unit tests, SLO-as-code tools), test that the burn-rate expression fires on synthetic data.193- **Mutation-testing note:** an unasserted probe/telemetry call is a surviving-mutant farm — which is itself the argument for asserting observations.194195**Honest limits:** you cannot meaningfully unit-test sampling percentages, Collector pipelines, or backend retention. Verify those in a staging environment with a real Collector — and note that sampling config is itself a parity surface (staging at 100%, prod at 1% behave differently under debugging).196197---198199## Frontend Note (Out of Scope for v1)200201Browser observability is RUM (real-user monitoring) with Core Web Vitals as the standard SLIs — LCP ≤ 2.5s, INP ≤ 200ms, CLS ≤ 0.1 at the 75th percentile of real page loads; lab tools are "not a substitute for field measurement" ([web.dev](https://web.dev/articles/vitals)). This skill covers services; for the user-facing half see the `core-web-vitals` and `performance` skills where installed.202203---204205## Anti-Patterns206207| # | Anti-Pattern | Why It's Wrong |208|---|-------------|----------------|209| 1 | Unbounded label on a metric (user ID, raw URL, container ID) | Cardinality explosion — memory, query time, and bills scale per series |210| 2 | Scattered log lines instead of an accumulated wide event | Context arrives fragmented; investigation becomes regex archaeology |211| 3 | Canonical event skipped on the exception path | The event vanishes exactly when it matters most; emit in `finally` |212| 4 | Paging on causes (CPU, disk, replica lag) | Users don't experience causes; symptom pages catch more with less noise |213| 5 | A page without a runbook or with no possible action | Pages that can't be acted on train people to ignore pages |214| 6 | Serializing whole objects into logs | Auth headers, tokens, and PII ride along; allowlist named fields |215| 7 | Inventing attribute names semconv already defines | Breaks cross-team and cross-tool correlation for zero benefit |216| 8 | Sampling the stream SLOs are computed from, silently | SLI math becomes fiction; retain errors at 100% or adjust the math |217| 9 | `console.log` debugging left behind as "instrumentation" | Unstructured, unqueryable, uncorrelated — remove or promote to a real field. The removal is scoped to the file, not to the request: when a file you are already editing carries this noise on paths you were not asked about, take all of it out of that file — half-cleaned leaves lines nobody can query and nobody will come back for |218| 10 | One vendor agent per signal instead of OTel | Locks instrumentation to a backend; OTel makes backends swappable |219| 11 | Alerting on every error the moment it happens | Error budgets exist so that noise below the burn-rate threshold stays out of pagers |220221---222223## Boundaries224225| Concern | Owner |226|---------|-------|227| Log transport, stdout, JSON shape, levels exist, timestamps | `twelve-factor` |228| What goes IN telemetry; wide events, traces, SLOs, alerts | this skill |229| Diagnosing a failing CI run | `ci-debugging` |230| Telemetry ports, Domain Probes, four-tier placement detail | `hexagonal-architecture` |231| Environment drift (works locally, not in prod) | `production-parity-skill-builder` |232| HTTP error response bodies (RFC 9457) | `api-design` |233| CLI usage telemetry consent | `cli-design` |234| Core Web Vitals / frontend RUM | `core-web-vitals` (external, where installed) |235236---237238## Verification Checklist239240- [ ] Every request emits exactly one canonical wide event, including on the exception path241- [ ] Privacy-approved high-cardinality identifiers (subject, request, build, tenant) live on events/spans, never on metric labels242- [ ] Metric labels use bounded normalized allowlists; unknown request values map to `_OTHER` or are omitted, and raw attacker-controlled values never become labels243- [ ] `service.name` and resource attributes are set; semantic-convention names used where they exist244- [ ] Trace context (W3C `traceparent`) propagates across every service hop and into every log record245- [ ] OTel SDK initializes before application code loads (`--import`), auto-instrumentation enabled246- [ ] Production telemetry uses the recommended Collector path, or a documented direct-export exception meets buffering, retry, backpressure, filtering, and operational requirements247- [ ] Sampling and Collector capacity meet a stated error-trace retention objective and loss budget; use unsampled collection when the objective cannot tolerate sampling loss248- [ ] Each user journey has a handful of ratio-based SLOs at most; latency thresholds and diagnostic percentiles are explicit, with an error budget policy249- [ ] Paging alerts are symptom-based and burn-rate-driven (multiwindow); each links a runbook250- [ ] No page fires for a condition with no immediate human action251- [ ] Telemetry contains no secrets; personal identifiers are necessary, policy-approved, minimized or pseudonymized, and fields are allowlisted and redacted at source252- [ ] Instrumentation is covered by tests (in-memory exporter or fake-probe assertions)253- [ ] Adding a metric label or new signal triggers a written cardinality/cost estimate
Run npx skillmds@latest add citypaul/observability in your terminal (requires Node.js), paste this page's agent-chat prompt into Claude, Cursor, or any MCP-connected agent, or download the SKILL.md file and copy it into your agent's skills directory.
Observability as an engineering discipline — wide events / canonical log lines, OpenTelemetry instrumentation (traces, metrics, context propagation, sampling, Collector), SLIs/SLOs/error budgets, symptom-based alerting with burn rates, telemetry hygiene, and testing instrumentation as behavior. Use when calls between services cannot be correlated, another team cannot line their side of a call up with ours or find the request it came from, a request or trace id has to cross a service hop, a failure cannot be debugged from the logs, or when instrumenting a service, designing SLOs or alerts, choosing what to log/trace/measure, investigating production unknowns, or reviewing telemetry cost and cardinality. For log transport and shape (stdout, JSON, levels, timestamps) see twelve-factor; for CI failure diagnosis see ci-debugging; for where instrumentation lives in ports-and-adapters codebases see hexagonal-architecture; for environment drift see production-parity-skill-builder; for HTTP error response shape see ap It is listed under Coding & Dev Tools on SkillMD.
This skill has not completed SkillMD's automated safety review yet. Capability flags: makes network calls, reads secrets. SkillMD never runs a skill's scripts for you; review the SKILL.md before installing.
This skill is tagged as working with Claude Code, Claude.ai, OpenAI Codex. SKILL.md is an open format, so most agents that read a skills directory can load it too.
Yes. Installing skills from SkillMD is free, and the skill stays under its author's original license.
citypaul (@citypaul) published this skill. Their other Agent Skills are listed on their SkillMD profile.