Observability
If you cannot ask arbitrary questions about your system's behavior from the outside, your system is not observable —
it is merely monitored.
The Three Pillars
Logs — What Happened
Logs are timestamped, discrete event records. They capture what happened at a specific moment: an error thrown, a
user action, a configuration loaded, a connection refused.
Use logs when you need:
- Rich diagnostic context for a specific event
- Debugging information with full error details and stack traces
- Audit trails of who did what and when
- Record of discrete state transitions
Logs are poor at:
- Showing aggregate system health (use metrics)
- Tracing request flow across services (use traces)
- High-frequency numeric trends (too expensive at volume)
Metrics — How Is It Doing
Metrics are numeric measurements aggregated over time. They capture how the system is performing as quantitative
time series: request rates, error percentages, latencies, queue depths, resource utilization.
Use metrics when you need:
- Real-time health signals and alerting
- Trend analysis over hours, days, weeks
- Capacity planning and saturation monitoring
- Pre-aggregated data that scales cheaply regardless of traffic
Metrics are poor at:
- Explaining why something is broken (use logs)
- Showing the path of a single request (use traces)
- Storing per-event detail (cardinality explosion)
Traces — How Did It Flow
Traces record the causal chain of operations that make up a single request as it propagates through distributed
components. A trace is a tree of spans, where each span represents one unit of work (an HTTP call, a database query,
a queue publish).
Use traces when you need:
- End-to-end latency breakdown across services
- Dependency mapping and bottleneck identification
- Understanding the path a failing request took
- Correlating work across process and network boundaries
Traces are poor at:
- Aggregate health monitoring (use metrics)
- Detailed per-event diagnostics on a single node (use logs)
- Cheap, long-term trend storage (traces are expensive at 100% sampling)
Choosing the Right Signal
- "Is the system healthy right now?" — Metrics
- "Why did this specific request fail?" — Traces + Logs
- "What happened at 03:14 on node-7?" — Logs
- "Where is the bottleneck in checkout flow?" — Traces
- "Are error rates increasing over the last hour?" — Metrics
- "What was the full stack trace of that exception?" — Logs
- "Which downstream service is slow?" — Traces
- "How much headroom does the database have?" — Metrics
Structured Logging
Always Structured
Emit logs as structured records (JSON or equivalent key-value format) with a consistent schema. Unstructured string logs
are for local development only. Structured logs are machine-parseable, indexable, and filterable at scale.
Log Levels
Use levels consistently. Agree on what each level means across the team.
- FATAL/CRITICAL — Process cannot continue; about to crash. Alerting: Page immediately
- ERROR — Operation failed; requires investigation. Alerting: Alert / ticket
- WARN — Unexpected condition; system compensated. Alerting: Monitor trend
- INFO — Significant business or lifecycle event. Alerting: Dashboard
- DEBUG — Diagnostic detail for developers. Alerting: Never in production by default
- TRACE — Extremely verbose step-by-step flow. Alerting: Never in production
Rules:
- Production defaults to INFO or above. DEBUG/TRACE are off unless explicitly enabled for a bounded investigation
window.
- WARN is not a dumping ground. If it never leads to action, it is noise — downgrade to DEBUG or remove it.
- ERROR means something is broken. Expected conditions (404 for missing resources, validation failures from bad input)
are not errors — log at INFO with a status field.
- Log level must be configurable at runtime without restarts.
Structured Fields
Every log record should include these baseline fields:
timestamp: ISO 8601, UTC
level: Severity (ERROR, WARN, INFO, ...)
message: Human-readable summary of the event
service: Service name emitting the log
version: Service version / build / commit SHA
trace_id: Distributed trace ID (if in request context)
span_id: Current span ID (if in request context)
Add contextual fields relevant to the event:
user_id: User-initiated actions
request_id: Per-request correlation
duration_ms: Timed operations
error.type: Error class/name
error.message: Error description
error.stack: Stack trace (ERROR level only)
http.method, http.path, http.status: HTTP request/response
db.operation, db.duration_ms: Database calls
Sensitive Data
Never log:
- Passwords, tokens, API keys, secrets
- Full credit card numbers, SSNs, or equivalent PII
- Session tokens or authentication cookies
- Request/response bodies containing user-submitted personal data
When user identifiers are needed, log opaque IDs (user_id), not email addresses or names. If regulations (GDPR, HIPAA)
apply, verify logged fields comply. When in doubt, omit the field.
Logging at Boundaries
At application startup:
- INFO: service name, version, loaded configuration (without secrets), listen address
- WARN: degraded mode (e.g., fallback to local cache because Redis is unreachable)
- ERROR/FATAL: unrecoverable startup failures
Per incoming request:
- INFO: method, path (scrubbed of PII), status code, duration, request dimensions (tenant, region)
- WARN/ERROR: only for unexpected exceptions; catch at the top-level handler
Per outgoing dependency call:
- INFO or DEBUG: target service, operation, status, duration
- ERROR: failures in dependent services (Redis, database, queue, etc.)
Log Once, at the Right Level
Log a raised exception once. Do not catch-log-rethrow at every layer. Let exceptions propagate to the top-level
handler, which logs with full context. Log and rethrow only when adding context that would otherwise be lost.
Metrics
Metric Types
- Counter — Monotonically increasing; resets on restart. Use for totals: requests, errors, bytes sent
- Gauge — Arbitrary value; goes up and down. Use for snapshots: queue depth, memory usage, connections
- Histogram — Client-side aggregation into buckets. Use for distributions: request latency, payload size
- Summary — Client-side quantile calculation. Use for pre-computed percentiles (less flexible than histogram)
Rules:
- Use counters for events that accumulate. Derive rates with
rate() / increase() — never store pre-computed rates.
- Use gauges for current-state snapshots. Never
rate() a gauge.
- Use histograms for latency and size distributions. Histograms enable percentile calculation across instances;
summaries do not aggregate.
- Export timestamps as Unix epoch seconds, not "time since" values.
- Initialize all metrics with zero at startup to avoid missing-metric problems.
What to Measure
The Four Golden Signals (Google SRE)
For every user-facing service, measure these four:
- Latency — Time to serve a request. Example:
http_request_duration_seconds histogram
- Traffic — Demand on the system. Example:
http_requests_total counter by method/path
- Errors — Rate of failed requests. Example:
http_requests_total{status=~"5.."}
- Saturation — How "full" the service is. Example: CPU usage, memory, queue depth, thread pool
Distinguish successful latency from error latency. A fast 500 is not good latency. Track both.
RED Method (Request-Centric)
For every microservice:
- Rate — requests per second
- Errors — failed requests per second
- Duration — distribution of request latency
RED is a focused subset of the golden signals, optimized for request-driven services.
USE Method (Resource-Centric)
For every resource (CPU, memory, disk, network, thread pool):
- Utilization — percentage of capacity in use
- Saturation — backlog / queue depth
- Errors — resource-level error count
RED tells you what is degraded from the user's perspective. USE tells you why at the infrastructure level. Use both
together.
Service-Type Instrumentation
- Online-serving (HTTP, gRPC) — Request rate, error rate, latency (p50/p90/p99), in-flight requests
- Offline-processing (workers, pipelines) — Items in/out per stage, processing duration, last-processed timestamp,
queue depth
- Batch jobs — Last successful completion time, job duration, records processed, exit status
- Caches — Hit rate, miss rate, eviction count, latency to backend on miss
- Thread/connection pools — Pool size, active count, queue length, wait time
Metric Naming
Metric names should be self-documenting. Follow these conventions:
- Prefix with namespace.
myapp_http_requests_total, not requests_total.
- Use base units. Seconds (not milliseconds), bytes (not megabytes), ratio 0-1 (not percentage 0-100).
- Suffix with unit.
_seconds, _bytes, _total (for unit-less counters).
- One metric, one unit, one quantity. Never mix request size with request duration in the same metric.
- snake_case.
http_request_duration_seconds, not httpRequestDurationSeconds.
| Good |
Bad |
http_request_duration_seconds |
request_latency (no unit, ambiguous) |
http_requests_total |
http_responses_500_total (use labels) |
node_memory_usage_bytes |
memory_mb (not base unit) |
process_cpu_seconds_total |
cpu_percent (use ratio 0-1) |
Labels and Cardinality
Labels add dimensions to a metric. Every unique combination of label values creates a separate time series.
Good labels (bounded, low cardinality):
method (GET, POST, PUT, DELETE)
status_code (200, 404, 500 — or class: 2xx, 4xx, 5xx)
service, region, version
Dangerous labels (unbounded, high cardinality):
user_id, email, session_id
request_path with dynamic segments (/users/12345)
error_message (arbitrary strings)
Rules:
- Keep label cardinality below 10 values per label for most metrics.
- If a label can grow unbounded, it does not belong on a metric. Log it instead.
- Use labels instead of encoding dimensions in the metric name.
http_requests_total{method="GET"}, not
http_get_requests_total.
- Ensure
sum() or avg() across all label values is meaningful. If not, split into separate metrics.
Percentiles and Tail Latency
Averages hide outliers. A service with 100ms average latency may have 1% of requests taking 5 seconds. That 1% tail can
dominate user experience when users hit multiple services per page load.
- Always track p50, p90, p99 latency at minimum.
- Use histograms with exponentially distributed bucket boundaries (e.g., 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s,
2.5s, 5s, 10s).
- Alert on p99, not mean. Mean latency alerts miss tail degradation.
Distributed Tracing
Core Concepts
- Trace — End-to-end record of a single request across all services
- Span — One unit of work within a trace (HTTP call, DB query, function)
- Root span — First span in a trace; has no parent
- Child span — Span nested under a parent; represents a sub-operation
- Span context — Immutable bag of
trace_id + span_id + flags, propagated across boundaries
- Span attributes — Key-value metadata on a span (http.method, db.statement)
- Span events — Timestamped annotations within a span's lifetime
- Span links — Causal references between spans in different traces
Span Kinds
- Client — Outgoing synchronous call. Example: HTTP request to another service
- Server — Incoming synchronous call. Example: Handling an HTTP request
- Producer — Creates async work. Example: Publishing to a message queue
- Consumer — Processes async work. Example: Consuming from a message queue
- Internal — No network boundary. Example: In-process function instrumentation
Context Propagation
Context propagation connects spans across process boundaries into a single trace. Without it, you get disconnected
spans, not traces.
Rules:
- Propagate context on every outgoing call. HTTP headers (W3C Trace Context or B3), message metadata, gRPC metadata
— every cross-process boundary must carry trace context.
- Extract context on every incoming call. Extract trace context and create a child span under the propagated parent.
- Use W3C Trace Context (
traceparent/tracestate) as the default propagation format unless the ecosystem requires
otherwise (e.g., legacy B3).
- Never generate a new trace ID when continuing an existing trace. A new trace ID means a broken trace.
What to Trace
Instrument at meaningful boundaries:
- Incoming HTTP/gRPC requests — Always — auto-instrument
- Outgoing HTTP/gRPC calls — Always — auto-instrument
- Database queries — Always — auto-instrument or manual
- Cache operations — Yes — hit/miss as attribute
- Queue publish/consume — Yes — link producer and consumer spans
- Significant business operations — Yes — manual spans for key logic
- Tight loops / trivial functions — No — noise, performance cost
Span Attributes
Attach attributes that enable filtering and analysis:
http.method, http.route, http.status_code: HTTP spans
db.system, db.operation, db.statement: Database spans
messaging.system, messaging.operation: Queue spans
rpc.system, rpc.method: RPC spans
error (boolean), error.type, error.message: Error conditions
service.name, service.version: All spans (set on resource)
Use semantic conventions for attribute names rather than inventing
custom ones. Consistent naming enables cross-service analysis.
Span Status
Unset — Completed without error (default). When: most successful operations
Error — Operation failed. When: server errors, exceptions
Ok — Explicitly marked successful. When: only when you need to override ambiguity
Leave status as Unset for normal success. Set Error only for actual failures. Do not set Error for client errors
like 404 on a server span — the server operated correctly.
Sampling
At high traffic volumes, tracing 100% of requests is expensive. Sampling reduces cost while preserving signal. |
Strategy | How It Works | Trade-off | | ------------------------ |
------------------------------------------------------ | ---------------------------------------------- | |
Head-based | Decide at trace start whether to sample | Simple; may miss rare errors | | Tail-based | Decide
after trace completes based on content | Catches errors; needs buffering infrastructure | | Always-on for errors |
Sample 100% of error traces, probabilistic for success | Good default balance |
Rules:
- Never drop error traces. If cost is a concern, sample successful traces at a lower rate but keep 100% of error and
high-latency traces.
- Sample at the entry point (head) and propagate the decision. Each service deciding independently creates partial
traces.
- Start with a low sampling rate (1-10%) and increase based on need, not the reverse.
Connecting the Pillars
The three pillars become powerful when correlated. An alert fires on a metric → you find the offending trace → the trace
points to a span → the span's logs reveal the root cause.
Correlation Keys
trace_id — Links logs and spans to the same trace. Where: logs, span context
span_id — Links a log to the exact span that produced it. Where: logs, span context
request_id — Correlates all work for one inbound request. Where: logs, HTTP headers
service.name + service.version — Groups telemetry by source. Where: all signals
Rules:
- Embed trace_id and span_id in every log record emitted within a request context — the primary bridge between logs
and traces.
- Use a correlation/request ID assigned at the edge (API gateway, load balancer) and propagated to all downstream
services.
- Attach exemplars to metrics. An exemplar is a trace_id on a specific metric observation, enabling drill-down from
a metric spike to a representative trace.
The Correlation Workflow
Metrics dashboard shows error rate spike
→ Filter by service + time window
→ Find exemplar trace_id on the error counter
→ Open trace in tracing UI
→ Identify the failing span (database timeout)
→ Search logs by trace_id for full error details
→ Root cause: connection pool exhausted
Metric-to-Trace Exemplars
Exemplars attach a trace_id sample to a metric data point. When you see a latency spike on a histogram, the exemplar
gives you a concrete trace to investigate rather than guessing.
- Attach exemplars to histogram observations for latency metrics.
- Attach exemplars to counter increments for error metrics.
- Not every metric point needs an exemplar — one per scrape interval is sufficient.
Trace-to-Log Linking
When viewing a trace, each span should link to its logs. When viewing a log, the trace_id should link back to the full
trace. This bidirectional linking is the backbone of incident investigation.
Anti-Patterns
- Logging everything at DEBUG in production — Disk/cost explosion, noise buries signal. Fix: default to INFO; enable
DEBUG temporarily per-component
catch (err) { log(err); throw err; } at every layer — Same error logged N times across the call stack. Fix: log
once at the top-level handler
- Metrics with unbounded label cardinality — Time series explosion; monitoring system degrades. Fix: use bounded
labels; move high-cardinality data to logs
- Encoding dimensions in metric names — Cannot aggregate; proliferates metrics. Fix: use labels:
requests_total{method="GET"}
- Averaging latency for alerting — Hides tail latency; misses degradation for minority of users. Fix: alert on p99
from histograms
- Missing trace context propagation — Broken traces; spans from different services are disconnected. Fix: propagate
context on every cross-process call
- Sampling each service independently — Partial traces — some spans sampled, some dropped. Fix: decide at head,
propagate sampling decision
- Logging PII / secrets — Compliance violations, security risk. Fix: audit log fields; log opaque IDs, never raw PII
- Alert on every metric wiggle — Alert fatigue; team ignores pages. Fix: alert on symptoms (golden signals), not
causes; require actionability
- Treating WARN as a soft ERROR — WARN becomes noise nobody reads. Fix: WARN = system compensated but situation is
unusual; ERROR = broken
- Storing pre-computed rates instead of counters — Cannot re-aggregate over different windows. Fix: store raw
counters; derive rates at query time
- No baseline metrics for new services — Cannot tell if behavior is normal or degraded. Fix: instrument golden
signals from day one, before first deploy
Application
When Writing Code
- Instrument from the start. Add golden signal metrics, structured logging, and trace context propagation before the
first production deploy — not after the first incident.
- Follow the conventions silently. Apply structured logging, metric naming, and tracing patterns without narrating
each rule.
- If the codebase has existing patterns, follow them. Consistency beats theoretical correctness. Flag divergences
once, then move on.
- Choose the right pillar. Before adding instrumentation, ask: "Is this a metric, a log, or a span?" Use the
decision table above.
- Connect the signals. Every log in a request context must carry
trace_id and span_id. Every error metric should
have an exemplar.
When Reviewing Code
- Check that new endpoints/operations have golden signal coverage. Missing metrics on a new endpoint is a review
blocker.
- Verify structured logging. Unstructured
log.Print("something happened") in production code should be flagged
with the fix inline.
- Check log levels. Expected client errors logged as ERROR, or debug noise left on at INFO, are common mistakes.
- Verify trace context propagation. Any new outgoing HTTP/gRPC/queue call must propagate trace context. Missing
propagation breaks traces.
- Check label cardinality. New metric labels must be bounded. Flag unbounded labels (user IDs, free-text)
immediately.
- No sensitive data in logs or span attributes. Passwords, tokens, PII in telemetry is a security and compliance
defect.
Bad review comment:
"According to observability best practices, you should consider
adding structured logging with appropriate fields..."
Good review comment:
"Missing trace_id in log context — requests through this handler
won't correlate to traces. Add ctx.TraceID() to the logger fields."
Integration
This skill provides observability discipline alongside other skills:
- Coding skill — Discovery, planning, verification workflow
- Observability (this skill) — What to log, measure, and trace
- Tool-specific skills (Prometheus, StatsD, OTel) — How to implement with a specific technology
The coding skill governs workflow. This skill governs observability design decisions. Tool-specific skills govern
implementation details for their respective technologies.
1---2name: observability3description: Observability discipline: structured logging, metrics instrumentation, distributed tracing, and signal correlation. Invoke whenever task involves any interaction with observability concerns — adding logging, designing metrics, instrumenting traces, correlating signals, reviewing instrumentation, or understanding when to use which pillar.4---56# Observability78**If you cannot ask arbitrary questions about your system's behavior from the outside, your system is not observable —9it is merely monitored.**1011---1213## The Three Pillars1415<pillars>1617### Logs — What Happened1819Logs are timestamped, discrete event records. They capture **what happened** at a specific moment: an error thrown, a20user action, a configuration loaded, a connection refused.2122**Use logs when you need:**2324- Rich diagnostic context for a specific event25- Debugging information with full error details and stack traces26- Audit trails of who did what and when27- Record of discrete state transitions2829**Logs are poor at:**3031- Showing aggregate system health (use metrics)32- Tracing request flow across services (use traces)33- High-frequency numeric trends (too expensive at volume)3435### Metrics — How Is It Doing3637Metrics are numeric measurements aggregated over time. They capture **how the system is performing** as quantitative38time series: request rates, error percentages, latencies, queue depths, resource utilization.3940**Use metrics when you need:**4142- Real-time health signals and alerting43- Trend analysis over hours, days, weeks44- Capacity planning and saturation monitoring45- Pre-aggregated data that scales cheaply regardless of traffic4647**Metrics are poor at:**4849- Explaining _why_ something is broken (use logs)50- Showing the path of a single request (use traces)51- Storing per-event detail (cardinality explosion)5253### Traces — How Did It Flow5455Traces record the causal chain of operations that make up a single request as it propagates through distributed56components. A trace is a tree of **spans**, where each span represents one unit of work (an HTTP call, a database query,57a queue publish).5859**Use traces when you need:**6061- End-to-end latency breakdown across services62- Dependency mapping and bottleneck identification63- Understanding the path a failing request took64- Correlating work across process and network boundaries6566**Traces are poor at:**6768- Aggregate health monitoring (use metrics)69- Detailed per-event diagnostics on a single node (use logs)70- Cheap, long-term trend storage (traces are expensive at 100% sampling)7172</pillars>7374### Choosing the Right Signal7576- **"Is the system healthy right now?"** — Metrics77- **"Why did this specific request fail?"** — Traces + Logs78- **"What happened at 03:14 on node-7?"** — Logs79- **"Where is the bottleneck in checkout flow?"** — Traces80- **"Are error rates increasing over the last hour?"** — Metrics81- **"What was the full stack trace of that exception?"** — Logs82- **"Which downstream service is slow?"** — Traces83- **"How much headroom does the database have?"** — Metrics8485---8687## Structured Logging8889<structured-logging>9091### Always Structured9293Emit logs as structured records (JSON or equivalent key-value format) with a consistent schema. Unstructured string logs94are for local development only. Structured logs are machine-parseable, indexable, and filterable at scale.9596### Log Levels9798Use levels consistently. Agree on what each level means across the team.99100- **FATAL/CRITICAL** — Process cannot continue; about to crash. Alerting: Page immediately101- **ERROR** — Operation failed; requires investigation. Alerting: Alert / ticket102- **WARN** — Unexpected condition; system compensated. Alerting: Monitor trend103- **INFO** — Significant business or lifecycle event. Alerting: Dashboard104- **DEBUG** — Diagnostic detail for developers. Alerting: Never in production by default105- **TRACE** — Extremely verbose step-by-step flow. Alerting: Never in production106107Rules:108109- Production defaults to INFO or above. DEBUG/TRACE are off unless explicitly enabled for a bounded investigation110 window.111- WARN is not a dumping ground. If it never leads to action, it is noise — downgrade to DEBUG or remove it.112- ERROR means something is broken. Expected conditions (404 for missing resources, validation failures from bad input)113 are not errors — log at INFO with a status field.114- Log level must be configurable at runtime without restarts.115116### Structured Fields117118Every log record should include these baseline fields:119120- `timestamp`: ISO 8601, UTC121- `level`: Severity (ERROR, WARN, INFO, ...)122- `message`: Human-readable summary of the event123- `service`: Service name emitting the log124- `version`: Service version / build / commit SHA125- `trace_id`: Distributed trace ID (if in request context)126- `span_id`: Current span ID (if in request context)127128Add contextual fields relevant to the event:129130- `user_id`: User-initiated actions131- `request_id`: Per-request correlation132- `duration_ms`: Timed operations133- `error.type`: Error class/name134- `error.message`: Error description135- `error.stack`: Stack trace (ERROR level only)136- `http.method`, `http.path`, `http.status`: HTTP request/response137- `db.operation`, `db.duration_ms`: Database calls138139### Sensitive Data140141Never log:142143- Passwords, tokens, API keys, secrets144- Full credit card numbers, SSNs, or equivalent PII145- Session tokens or authentication cookies146- Request/response bodies containing user-submitted personal data147148When user identifiers are needed, log opaque IDs (user_id), not email addresses or names. If regulations (GDPR, HIPAA)149apply, verify logged fields comply. When in doubt, omit the field.150151### Logging at Boundaries152153**At application startup:**154155- INFO: service name, version, loaded configuration (without secrets), listen address156- WARN: degraded mode (e.g., fallback to local cache because Redis is unreachable)157- ERROR/FATAL: unrecoverable startup failures158159**Per incoming request:**160161- INFO: method, path (scrubbed of PII), status code, duration, request dimensions (tenant, region)162- WARN/ERROR: only for unexpected exceptions; catch at the top-level handler163164**Per outgoing dependency call:**165166- INFO or DEBUG: target service, operation, status, duration167- ERROR: failures in dependent services (Redis, database, queue, etc.)168169### Log Once, at the Right Level170171Log a raised exception **once**. Do not catch-log-rethrow at every layer. Let exceptions propagate to the top-level172handler, which logs with full context. Log and rethrow only when adding context that would otherwise be lost.173174</structured-logging>175176---177178## Metrics179180<metrics>181182### Metric Types183184- **Counter** — Monotonically increasing; resets on restart. Use for totals: requests, errors, bytes sent185- **Gauge** — Arbitrary value; goes up and down. Use for snapshots: queue depth, memory usage, connections186- **Histogram** — Client-side aggregation into buckets. Use for distributions: request latency, payload size187- **Summary** — Client-side quantile calculation. Use for pre-computed percentiles (less flexible than histogram)188189Rules:190191- Use counters for events that accumulate. Derive rates with `rate()` / `increase()` — never store pre-computed rates.192- Use gauges for current-state snapshots. Never `rate()` a gauge.193- Use histograms for latency and size distributions. Histograms enable percentile calculation across instances;194 summaries do not aggregate.195- Export timestamps as Unix epoch seconds, not "time since" values.196- Initialize all metrics with zero at startup to avoid missing-metric problems.197198### What to Measure199200#### The Four Golden Signals (Google SRE)201202For every user-facing service, measure these four:203204- **Latency** — Time to serve a request. Example: `http_request_duration_seconds` histogram205- **Traffic** — Demand on the system. Example: `http_requests_total` counter by method/path206- **Errors** — Rate of failed requests. Example: `http_requests_total{status=~"5.."}`207- **Saturation** — How "full" the service is. Example: CPU usage, memory, queue depth, thread pool208209Distinguish **successful latency from error latency**. A fast 500 is not good latency. Track both.210211#### RED Method (Request-Centric)212213For every microservice:214215- **R**ate — requests per second216- **E**rrors — failed requests per second217- **D**uration — distribution of request latency218219RED is a focused subset of the golden signals, optimized for request-driven services.220221#### USE Method (Resource-Centric)222223For every resource (CPU, memory, disk, network, thread pool):224225- **U**tilization — percentage of capacity in use226- **S**aturation — backlog / queue depth227- **E**rrors — resource-level error count228229RED tells you _what_ is degraded from the user's perspective. USE tells you _why_ at the infrastructure level. Use both230together.231232#### Service-Type Instrumentation233234- **Online-serving** (HTTP, gRPC) — Request rate, error rate, latency (p50/p90/p99), in-flight requests235- **Offline-processing** (workers, pipelines) — Items in/out per stage, processing duration, last-processed timestamp,236 queue depth237- **Batch jobs** — Last successful completion time, job duration, records processed, exit status238- **Caches** — Hit rate, miss rate, eviction count, latency to backend on miss239- **Thread/connection pools** — Pool size, active count, queue length, wait time240241### Metric Naming242243Metric names should be self-documenting. Follow these conventions:244245- **Prefix with namespace.** `myapp_http_requests_total`, not `requests_total`.246- **Use base units.** Seconds (not milliseconds), bytes (not megabytes), ratio 0-1 (not percentage 0-100).247- **Suffix with unit.** `_seconds`, `_bytes`, `_total` (for unit-less counters).248- **One metric, one unit, one quantity.** Never mix request size with request duration in the same metric.249- **snake_case.** `http_request_duration_seconds`, not `httpRequestDurationSeconds`.250251| Good | Bad |252| ------------------------------- | --------------------------------------- |253| `http_request_duration_seconds` | `request_latency` (no unit, ambiguous) |254| `http_requests_total` | `http_responses_500_total` (use labels) |255| `node_memory_usage_bytes` | `memory_mb` (not base unit) |256| `process_cpu_seconds_total` | `cpu_percent` (use ratio 0-1) |257258### Labels and Cardinality259260Labels add dimensions to a metric. Every unique combination of label values creates a separate time series.261262**Good labels** (bounded, low cardinality):263264- `method` (GET, POST, PUT, DELETE)265- `status_code` (200, 404, 500 — or class: 2xx, 4xx, 5xx)266- `service`, `region`, `version`267268**Dangerous labels** (unbounded, high cardinality):269270- `user_id`, `email`, `session_id`271- `request_path` with dynamic segments (`/users/12345`)272- `error_message` (arbitrary strings)273274Rules:275276- Keep label cardinality below 10 values per label for most metrics.277- If a label can grow unbounded, it does not belong on a metric. Log it instead.278- Use labels instead of encoding dimensions in the metric name. `http_requests_total{method="GET"}`, not279 `http_get_requests_total`.280- Ensure `sum()` or `avg()` across all label values is meaningful. If not, split into separate metrics.281282### Percentiles and Tail Latency283284Averages hide outliers. A service with 100ms average latency may have 1% of requests taking 5 seconds. That 1% tail can285dominate user experience when users hit multiple services per page load.286287- Always track **p50, p90, p99** latency at minimum.288- Use histograms with exponentially distributed bucket boundaries (e.g., 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1s,289 2.5s, 5s, 10s).290- Alert on p99, not mean. Mean latency alerts miss tail degradation.291292</metrics>293294---295296## Distributed Tracing297298<tracing>299300### Core Concepts301302- **Trace** — End-to-end record of a single request across all services303- **Span** — One unit of work within a trace (HTTP call, DB query, function)304- **Root span** — First span in a trace; has no parent305- **Child span** — Span nested under a parent; represents a sub-operation306- **Span context** — Immutable bag of `trace_id` + `span_id` + flags, propagated across boundaries307- **Span attributes** — Key-value metadata on a span (http.method, db.statement)308- **Span events** — Timestamped annotations within a span's lifetime309- **Span links** — Causal references between spans in different traces310311### Span Kinds312313- **Client** — Outgoing synchronous call. Example: HTTP request to another service314- **Server** — Incoming synchronous call. Example: Handling an HTTP request315- **Producer** — Creates async work. Example: Publishing to a message queue316- **Consumer** — Processes async work. Example: Consuming from a message queue317- **Internal** — No network boundary. Example: In-process function instrumentation318319### Context Propagation320321Context propagation connects spans across process boundaries into a single trace. Without it, you get disconnected322spans, not traces.323324Rules:325326- **Propagate context on every outgoing call.** HTTP headers (W3C Trace Context or B3), message metadata, gRPC metadata327 — every cross-process boundary must carry trace context.328- **Extract context on every incoming call.** Extract trace context and create a child span under the propagated parent.329- **Use W3C Trace Context (`traceparent`/`tracestate`)** as the default propagation format unless the ecosystem requires330 otherwise (e.g., legacy B3).331- **Never generate a new trace ID** when continuing an existing trace. A new trace ID means a broken trace.332333### What to Trace334335Instrument at meaningful boundaries:336337- **Incoming HTTP/gRPC requests** — Always — auto-instrument338- **Outgoing HTTP/gRPC calls** — Always — auto-instrument339- **Database queries** — Always — auto-instrument or manual340- **Cache operations** — Yes — hit/miss as attribute341- **Queue publish/consume** — Yes — link producer and consumer spans342- **Significant business operations** — Yes — manual spans for key logic343- **Tight loops / trivial functions** — No — noise, performance cost344345### Span Attributes346347Attach attributes that enable filtering and analysis:348349- `http.method`, `http.route`, `http.status_code`: HTTP spans350- `db.system`, `db.operation`, `db.statement`: Database spans351- `messaging.system`, `messaging.operation`: Queue spans352- `rpc.system`, `rpc.method`: RPC spans353- `error` (boolean), `error.type`, `error.message`: Error conditions354- `service.name`, `service.version`: All spans (set on resource)355356Use [semantic conventions](https://opentelemetry.io/docs/specs/semconv/) for attribute names rather than inventing357custom ones. Consistent naming enables cross-service analysis.358359### Span Status360361- `Unset` — Completed without error (default). When: most successful operations362- `Error` — Operation failed. When: server errors, exceptions363- `Ok` — Explicitly marked successful. When: only when you need to override ambiguity364365Leave status as `Unset` for normal success. Set `Error` only for actual failures. Do not set `Error` for client errors366like 404 on a server span — the server operated correctly.367368### Sampling369370At high traffic volumes, tracing 100% of requests is expensive. Sampling reduces cost while preserving signal. |371Strategy | How It Works | Trade-off | | ------------------------ |372------------------------------------------------------ | ---------------------------------------------- | |373**Head-based** | Decide at trace start whether to sample | Simple; may miss rare errors | | **Tail-based** | Decide374after trace completes based on content | Catches errors; needs buffering infrastructure | | **Always-on for errors** |375Sample 100% of error traces, probabilistic for success | Good default balance |376377Rules:378379- Never drop error traces. If cost is a concern, sample successful traces at a lower rate but keep 100% of error and380 high-latency traces.381- Sample at the entry point (head) and propagate the decision. Each service deciding independently creates partial382 traces.383- Start with a low sampling rate (1-10%) and increase based on need, not the reverse.384385</tracing>386387---388389## Connecting the Pillars390391<interconnection>392393The three pillars become powerful when correlated. An alert fires on a metric → you find the offending trace → the trace394points to a span → the span's logs reveal the root cause.395396### Correlation Keys397398- `trace_id` — Links logs and spans to the same trace. Where: logs, span context399- `span_id` — Links a log to the exact span that produced it. Where: logs, span context400- `request_id` — Correlates all work for one inbound request. Where: logs, HTTP headers401- `service.name` + `service.version` — Groups telemetry by source. Where: all signals402403Rules:404405- **Embed trace_id and span_id in every log record** emitted within a request context — the primary bridge between logs406 and traces.407- **Use a correlation/request ID** assigned at the edge (API gateway, load balancer) and propagated to all downstream408 services.409- **Attach exemplars to metrics.** An exemplar is a trace_id on a specific metric observation, enabling drill-down from410 a metric spike to a representative trace.411412### The Correlation Workflow413414```415Metrics dashboard shows error rate spike416 → Filter by service + time window417 → Find exemplar trace_id on the error counter418 → Open trace in tracing UI419 → Identify the failing span (database timeout)420 → Search logs by trace_id for full error details421 → Root cause: connection pool exhausted422```423424### Metric-to-Trace Exemplars425426Exemplars attach a `trace_id` sample to a metric data point. When you see a latency spike on a histogram, the exemplar427gives you a concrete trace to investigate rather than guessing.428429- Attach exemplars to histogram observations for latency metrics.430- Attach exemplars to counter increments for error metrics.431- Not every metric point needs an exemplar — one per scrape interval is sufficient.432433### Trace-to-Log Linking434435When viewing a trace, each span should link to its logs. When viewing a log, the `trace_id` should link back to the full436trace. This bidirectional linking is the backbone of incident investigation.437438</interconnection>439440---441442## Anti-Patterns443444- **Logging everything at DEBUG in production** — Disk/cost explosion, noise buries signal. Fix: default to INFO; enable445 DEBUG temporarily per-component446- **`catch (err) { log(err); throw err; }` at every layer** — Same error logged N times across the call stack. Fix: log447 once at the top-level handler448- **Metrics with unbounded label cardinality** — Time series explosion; monitoring system degrades. Fix: use bounded449 labels; move high-cardinality data to logs450- **Encoding dimensions in metric names** — Cannot aggregate; proliferates metrics. Fix: use labels:451 `requests_total{method="GET"}`452- **Averaging latency for alerting** — Hides tail latency; misses degradation for minority of users. Fix: alert on p99453 from histograms454- **Missing trace context propagation** — Broken traces; spans from different services are disconnected. Fix: propagate455 context on every cross-process call456- **Sampling each service independently** — Partial traces — some spans sampled, some dropped. Fix: decide at head,457 propagate sampling decision458- **Logging PII / secrets** — Compliance violations, security risk. Fix: audit log fields; log opaque IDs, never raw PII459- **Alert on every metric wiggle** — Alert fatigue; team ignores pages. Fix: alert on symptoms (golden signals), not460 causes; require actionability461- **Treating WARN as a soft ERROR** — WARN becomes noise nobody reads. Fix: WARN = system compensated but situation is462 unusual; ERROR = broken463- **Storing pre-computed rates instead of counters** — Cannot re-aggregate over different windows. Fix: store raw464 counters; derive rates at query time465- **No baseline metrics for new services** — Cannot tell if behavior is normal or degraded. Fix: instrument golden466 signals from day one, before first deploy467468---469470## Application471472<application>473474### When Writing Code475476- **Instrument from the start.** Add golden signal metrics, structured logging, and trace context propagation before the477 first production deploy — not after the first incident.478- **Follow the conventions silently.** Apply structured logging, metric naming, and tracing patterns without narrating479 each rule.480- **If the codebase has existing patterns, follow them.** Consistency beats theoretical correctness. Flag divergences481 once, then move on.482- **Choose the right pillar.** Before adding instrumentation, ask: "Is this a metric, a log, or a span?" Use the483 decision table above.484- **Connect the signals.** Every log in a request context must carry `trace_id` and `span_id`. Every error metric should485 have an exemplar.486487### When Reviewing Code488489- **Check that new endpoints/operations have golden signal coverage.** Missing metrics on a new endpoint is a review490 blocker.491- **Verify structured logging.** Unstructured `log.Print("something happened")` in production code should be flagged492 with the fix inline.493- **Check log levels.** Expected client errors logged as ERROR, or debug noise left on at INFO, are common mistakes.494- **Verify trace context propagation.** Any new outgoing HTTP/gRPC/queue call must propagate trace context. Missing495 propagation breaks traces.496- **Check label cardinality.** New metric labels must be bounded. Flag unbounded labels (user IDs, free-text)497 immediately.498- **No sensitive data in logs or span attributes.** Passwords, tokens, PII in telemetry is a security and compliance499 defect.500501```502Bad review comment:503 "According to observability best practices, you should consider504 adding structured logging with appropriate fields..."505506Good review comment:507 "Missing trace_id in log context — requests through this handler508 won't correlate to traces. Add ctx.TraceID() to the logger fields."509```510511</application>512513---514515## Integration516517This skill provides observability discipline alongside other skills:518519- **Coding skill** — Discovery, planning, verification workflow520- **Observability** (this skill) — What to log, measure, and trace521- **Tool-specific skills** (Prometheus, StatsD, OTel) — How to implement with a specific technology522523The coding skill governs workflow. This skill governs observability design decisions. Tool-specific skills govern524implementation details for their respective technologies.