Logging & Observability
Patterns for building observable systems across the three pillars: logs, metrics, and traces.
Three Pillars
| Pillar |
Purpose |
Question It Answers |
Example |
| Logs |
What happened |
Why did this request fail? |
{"level":"error","msg":"payment declined","user_id":"u_82"} |
| Metrics |
How much / how fast |
Is latency increasing? |
http_request_duration_seconds{route="/api/orders"} 0.342 |
| Traces |
Request flow |
Where is the bottleneck? |
Span: api-gateway → auth → order-service → db |
Each pillar is strongest when correlated. Embed trace_id in every log line to jump from a log entry to the full distributed trace.
Installation
OpenClaw / Moltbot / Clawbot
npx clawhub@latest install logging-observability
Structured Logging
Always emit logs as structured JSON — never free-text strings.
Required Fields
| Field |
Purpose |
Required |
timestamp |
ISO-8601 with milliseconds |
Yes |
level |
Severity (DEBUG … FATAL) |
Yes |
service |
Originating service name |
Yes |
message |
Human-readable description |
Yes |
trace_id |
Distributed trace correlation |
Yes |
span_id |
Current span within trace |
Yes |
correlation_id |
Business-level correlation (order ID) |
When applicable |
error |
Structured error object |
On errors |
context |
Request-specific metadata |
Recommended |
Context Enrichment
Attach context at the middleware level so downstream logs inherit automatically:
app.use((req, res, next) => {
const ctx = {
trace_id: req.headers['x-trace-id'] || crypto.randomUUID(),
request_id: crypto.randomUUID(),
user_id: req.user?.id,
method: req.method,
path: req.path,
};
asyncLocalStorage.run(ctx, () => next());
});
Library Recommendations
| Library |
Language |
Strengths |
Perf |
| Pino |
Node.js |
Fastest Node logger, low overhead |
Excellent |
| structlog |
Python |
Composable processors, context binding |
Good |
| zerolog |
Go |
Zero-allocation JSON logging |
Excellent |
| zap |
Go |
High performance, typed fields |
Excellent |
| tracing |
Rust |
Spans + events, async-aware |
Excellent |
Choose a logger that outputs structured JSON natively. Avoid loggers requiring post-processing.
Log Levels
| Level |
When to Use |
Example |
| FATAL |
App cannot continue, process will exit |
Database connection pool exhausted |
| ERROR |
Operation failed, needs attention |
Payment charge failed: CARD_DECLINED |
| WARN |
Unexpected but recoverable |
Retry 2/3 for upstream timeout |
| INFO |
Normal business events |
Order ORD-1234 placed successfully |
| DEBUG |
Developer troubleshooting |
Cache miss for key user:82:preferences |
| TRACE |
Very fine-grained (rarely in prod) |
Entering validateAddress with payload |
Rules: Production default = INFO and above. If you log an ERROR, someone should act on it. Every FATAL should trigger an alert.
Distributed Tracing
OpenTelemetry Setup
Always prefer OpenTelemetry over vendor-specific SDKs:
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
serviceName: 'order-service',
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();
Span Creation
const tracer = trace.getTracer('order-service');
async function processOrder(order: Order) {
return tracer.startActiveSpan('processOrder', async (span) => {
try {
span.setAttribute('order.id', order.id);
span.setAttribute('order.total_cents', order.totalCents);
await validateInventory(order);
await chargePayment(order);
span.setStatus({ code: SpanStatusCode.OK });
} catch (err) {
span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
span.recordException(err);
throw err;
} finally {
span.end();
}
});
}
Context Propagation
- Use W3C Trace Context (
traceparent header) — default in OTel
- Propagate across HTTP, gRPC, and message queues
- For async workers: serialise
traceparent into the job payload
Trace Sampling
| Strategy |
Use When |
| Always On |
Low-traffic services, debugging |
| Probabilistic (N%) |
General production use |
| Rate-limited (N/sec) |
High-throughput services |
| Tail-based |
When you need all error traces |
Always sample 100% of error traces regardless of strategy.
Metrics Collection
RED Method (Request-Driven)
Monitor these three for every service endpoint:
| Metric |
What It Measures |
Prometheus Example |
| Rate |
Requests/sec |
rate(http_requests_total[5m]) |
| Errors |
Failed request ratio |
rate(http_requests_total{status=~"5.."}[5m]) |
| Duration |
Response time |
histogram_quantile(0.99, http_request_duration_seconds) |
USE Method (Resource-Driven)
For infrastructure components (CPU, memory, disk, network):
| Metric |
What It Measures |
Example |
| Utilization |
% resource busy |
CPU usage at 78% |
| Saturation |
Work queued/waiting |
12 requests queued in thread pool |
| Errors |
Error events on resource |
3 disk I/O errors in last minute |
Monitoring Stack
| Tool |
Category |
Best For |
| Prometheus |
Metrics |
Pull-based metrics, alerting rules |
| Grafana |
Visualisation |
Dashboards for metrics, logs, traces |
| Jaeger |
Tracing |
Distributed trace visualisation |
| Loki |
Logs |
Log aggregation (pairs with Grafana) |
| OpenTelemetry |
Collection |
Vendor-neutral telemetry collection |
Recommendation: Start with OTel Collector → Prometheus + Grafana + Loki + Jaeger. Migrate to SaaS only when operational overhead justifies cost.
Alert Design
Severity Levels
| Severity |
Response Time |
Example |
| P1 |
Immediate |
Service fully down, data loss |
| P2 |
< 30 min |
Error rate > 5%, latency p99 > 5s |
| P3 |
Business hours |
Disk > 80%, cert expiring in 7 days |
| P4 |
Best effort |
Non-critical deprecation warning |
Alert Fatigue Prevention
- Alert on symptoms, not causes — "error rate > 5%" not "pod restarted"
- Multi-window, multi-burn-rate — catch both sudden spikes and slow burns
- Require runbook links — every alert must link to diagnosis and remediation
- Review monthly — delete or tune alerts that never fire or always fire
- Group related alerts — use inhibition rules to suppress child alerts
- Set appropriate thresholds — if alert fires daily and is ignored, raise threshold or delete
Dashboard Patterns
Overview Dashboard ("War Room")
- Total requests/sec across all services
- Global error rate (%) with trendline
- p50 / p95 / p99 latency
- Active alerts count by severity
- Deployment markers overlaid on graphs
Service Dashboard (Per-Service)
- RED metrics for each endpoint
- Dependency health (upstream/downstream success rates)
- Resource utilisation (CPU, memory, connections)
- Top errors table with count and last seen
Observability Checklist
Every service must have:
Anti-Patterns
| Anti-Pattern |
Problem |
Fix |
| Logging PII |
Privacy/compliance violation |
Mask or exclude PII; use token references |
| Excessive logging |
Storage costs balloon, signal drowns |
Log business events, not data flow |
| Unstructured logs |
Cannot query or alert on fields |
Use structured JSON with consistent schema |
| String interpolation |
Breaks structured fields, injection risk |
Pass fields as metadata, not in message |
| Missing correlation IDs |
Cannot trace across services |
Generate and propagate trace_id everywhere |
| Alert storms |
On-call fatigue, real issues buried |
Use grouping, inhibition, deduplication |
| Metrics with high cardinality |
Prometheus OOM, dashboard timeouts |
Never use user ID or request ID as label |
NEVER Do
- NEVER log passwords, tokens, API keys, or secrets — even at DEBUG level
- NEVER use console.log / print in production — use a structured logger
- NEVER use user IDs, emails, or request IDs as metric labels — cardinality will explode
- NEVER create alerts without a runbook link — unactionable alerts erode trust
- NEVER rely on logs alone — you need metrics and traces for full observability
- NEVER log request/response bodies by default — opt-in only, with PII redaction
- NEVER ignore log volume — set budgets and alert when a service exceeds daily quota
- NEVER skip context propagation in async flows — broken traces are worse than no traces
1---2name: logging-observability3description: Structured logging, distributed tracing, and metrics collection patterns for building observable systems. Use when implementing logging infrastructure, setting up distributed tracing with OpenTelemetry, designing metrics collection (RED/USE methods), configuring alerting and dashboards, or reviewing observability practices. Covers structured JSON logging, context propagation, trace sampling, Prometheus/Grafana stack, alert design, and PII/secret scrubbing.4---56# Logging & Observability78Patterns for building observable systems across the three pillars: logs, metrics, and traces.910## Three Pillars1112| Pillar | Purpose | Question It Answers | Example |13|--------|---------|---------------------|---------|14| **Logs** | What happened | Why did this request fail? | `{"level":"error","msg":"payment declined","user_id":"u_82"}` |15| **Metrics** | How much / how fast | Is latency increasing? | `http_request_duration_seconds{route="/api/orders"} 0.342` |16| **Traces** | Request flow | Where is the bottleneck? | Span: `api-gateway → auth → order-service → db` |1718Each pillar is strongest when correlated. Embed `trace_id` in every log line to jump from a log entry to the full distributed trace.192021## Installation2223### OpenClaw / Moltbot / Clawbot2425```bash26npx clawhub@latest install logging-observability27```282930---3132## Structured Logging3334Always emit logs as structured JSON — never free-text strings.3536### Required Fields3738| Field | Purpose | Required |39|-------|---------|----------|40| `timestamp` | ISO-8601 with milliseconds | Yes |41| `level` | Severity (DEBUG … FATAL) | Yes |42| `service` | Originating service name | Yes |43| `message` | Human-readable description | Yes |44| `trace_id` | Distributed trace correlation | Yes |45| `span_id` | Current span within trace | Yes |46| `correlation_id` | Business-level correlation (order ID) | When applicable |47| `error` | Structured error object | On errors |48| `context` | Request-specific metadata | Recommended |4950### Context Enrichment5152Attach context at the middleware level so downstream logs inherit automatically:5354```typescript55app.use((req, res, next) => {56 const ctx = {57 trace_id: req.headers['x-trace-id'] || crypto.randomUUID(),58 request_id: crypto.randomUUID(),59 user_id: req.user?.id,60 method: req.method,61 path: req.path,62 };63 asyncLocalStorage.run(ctx, () => next());64});65```6667### Library Recommendations6869| Library | Language | Strengths | Perf |70|---------|----------|-----------|------|71| **Pino** | Node.js | Fastest Node logger, low overhead | Excellent |72| **structlog** | Python | Composable processors, context binding | Good |73| **zerolog** | Go | Zero-allocation JSON logging | Excellent |74| **zap** | Go | High performance, typed fields | Excellent |75| **tracing** | Rust | Spans + events, async-aware | Excellent |7677Choose a logger that outputs structured JSON natively. Avoid loggers requiring post-processing.7879---8081## Log Levels8283| Level | When to Use | Example |84|-------|-------------|---------|85| **FATAL** | App cannot continue, process will exit | Database connection pool exhausted |86| **ERROR** | Operation failed, needs attention | Payment charge failed: CARD_DECLINED |87| **WARN** | Unexpected but recoverable | Retry 2/3 for upstream timeout |88| **INFO** | Normal business events | Order ORD-1234 placed successfully |89| **DEBUG** | Developer troubleshooting | Cache miss for key user:82:preferences |90| **TRACE** | Very fine-grained (rarely in prod) | Entering validateAddress with payload |9192**Rules:** Production default = INFO and above. If you log an ERROR, someone should act on it. Every FATAL should trigger an alert.9394---9596## Distributed Tracing9798### OpenTelemetry Setup99100Always prefer OpenTelemetry over vendor-specific SDKs:101102```typescript103import { NodeSDK } from '@opentelemetry/sdk-node';104import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';105import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';106107const sdk = new NodeSDK({108 serviceName: 'order-service',109 traceExporter: new OTLPTraceExporter({110 url: 'http://otel-collector:4318/v1/traces',111 }),112 instrumentations: [getNodeAutoInstrumentations()],113});114sdk.start();115```116117### Span Creation118119```typescript120const tracer = trace.getTracer('order-service');121122async function processOrder(order: Order) {123 return tracer.startActiveSpan('processOrder', async (span) => {124 try {125 span.setAttribute('order.id', order.id);126 span.setAttribute('order.total_cents', order.totalCents);127 await validateInventory(order);128 await chargePayment(order);129 span.setStatus({ code: SpanStatusCode.OK });130 } catch (err) {131 span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });132 span.recordException(err);133 throw err;134 } finally {135 span.end();136 }137 });138}139```140141### Context Propagation142143- Use W3C Trace Context (`traceparent` header) — default in OTel144- Propagate across HTTP, gRPC, and message queues145- For async workers: serialise `traceparent` into the job payload146147### Trace Sampling148149| Strategy | Use When |150|----------|----------|151| **Always On** | Low-traffic services, debugging |152| **Probabilistic** (N%) | General production use |153| **Rate-limited** (N/sec) | High-throughput services |154| **Tail-based** | When you need all error traces |155156Always sample 100% of error traces regardless of strategy.157158---159160## Metrics Collection161162### RED Method (Request-Driven)163164Monitor these three for every service endpoint:165166| Metric | What It Measures | Prometheus Example |167|--------|-----------------|-------------------|168| **Rate** | Requests/sec | `rate(http_requests_total[5m])` |169| **Errors** | Failed request ratio | `rate(http_requests_total{status=~"5.."}[5m])` |170| **Duration** | Response time | `histogram_quantile(0.99, http_request_duration_seconds)` |171172### USE Method (Resource-Driven)173174For infrastructure components (CPU, memory, disk, network):175176| Metric | What It Measures | Example |177|--------|-----------------|---------|178| **Utilization** | % resource busy | CPU usage at 78% |179| **Saturation** | Work queued/waiting | 12 requests queued in thread pool |180| **Errors** | Error events on resource | 3 disk I/O errors in last minute |181182---183184## Monitoring Stack185186| Tool | Category | Best For |187|------|----------|----------|188| **Prometheus** | Metrics | Pull-based metrics, alerting rules |189| **Grafana** | Visualisation | Dashboards for metrics, logs, traces |190| **Jaeger** | Tracing | Distributed trace visualisation |191| **Loki** | Logs | Log aggregation (pairs with Grafana) |192| **OpenTelemetry** | Collection | Vendor-neutral telemetry collection |193194**Recommendation:** Start with OTel Collector → Prometheus + Grafana + Loki + Jaeger. Migrate to SaaS only when operational overhead justifies cost.195196---197198## Alert Design199200### Severity Levels201202| Severity | Response Time | Example |203|----------|---------------|---------|204| **P1** | Immediate | Service fully down, data loss |205| **P2** | < 30 min | Error rate > 5%, latency p99 > 5s |206| **P3** | Business hours | Disk > 80%, cert expiring in 7 days |207| **P4** | Best effort | Non-critical deprecation warning |208209### Alert Fatigue Prevention210211- **Alert on symptoms, not causes** — "error rate > 5%" not "pod restarted"212- **Multi-window, multi-burn-rate** — catch both sudden spikes and slow burns213- **Require runbook links** — every alert must link to diagnosis and remediation214- **Review monthly** — delete or tune alerts that never fire or always fire215- **Group related alerts** — use inhibition rules to suppress child alerts216- **Set appropriate thresholds** — if alert fires daily and is ignored, raise threshold or delete217218---219220## Dashboard Patterns221222### Overview Dashboard ("War Room")223- Total requests/sec across all services224- Global error rate (%) with trendline225- p50 / p95 / p99 latency226- Active alerts count by severity227- Deployment markers overlaid on graphs228229### Service Dashboard (Per-Service)230- RED metrics for each endpoint231- Dependency health (upstream/downstream success rates)232- Resource utilisation (CPU, memory, connections)233- Top errors table with count and last seen234235---236237## Observability Checklist238239Every service must have:240241- [ ] Structured JSON logging with consistent schema242- [ ] Correlation / trace IDs propagated on all requests243- [ ] RED metrics exposed for every external endpoint244- [ ] Health check endpoints (`/healthz` and `/readyz`)245- [ ] Distributed tracing with OpenTelemetry246- [ ] Dashboards for RED metrics and resource utilisation247- [ ] Alerts for error rate, latency, and saturation with runbook links248- [ ] Log level configurable at runtime without redeployment249- [ ] PII scrubbing verified and tested250- [ ] Retention policies defined for logs, metrics, and traces251252## Anti-Patterns253254| Anti-Pattern | Problem | Fix |255|-------------|---------|-----|256| Logging PII | Privacy/compliance violation | Mask or exclude PII; use token references |257| Excessive logging | Storage costs balloon, signal drowns | Log business events, not data flow |258| Unstructured logs | Cannot query or alert on fields | Use structured JSON with consistent schema |259| String interpolation | Breaks structured fields, injection risk | Pass fields as metadata, not in message |260| Missing correlation IDs | Cannot trace across services | Generate and propagate trace_id everywhere |261| Alert storms | On-call fatigue, real issues buried | Use grouping, inhibition, deduplication |262| Metrics with high cardinality | Prometheus OOM, dashboard timeouts | Never use user ID or request ID as label |263264## NEVER Do2652661. **NEVER log passwords, tokens, API keys, or secrets** — even at DEBUG level2672. **NEVER use console.log / print in production** — use a structured logger2683. **NEVER use user IDs, emails, or request IDs as metric labels** — cardinality will explode2694. **NEVER create alerts without a runbook link** — unactionable alerts erode trust2705. **NEVER rely on logs alone** — you need metrics and traces for full observability2716. **NEVER log request/response bodies by default** — opt-in only, with PII redaction2727. **NEVER ignore log volume** — set budgets and alert when a service exceeds daily quota2738. **NEVER skip context propagation in async flows** — broken traces are worse than no traces