ted metrics and compressed logs handle historical analysis. This architectural discipline directly compresses MTTR and stabilizes on-call burden.
Core Solution
Implementing backend observability requires a phased, pipeline-oriented approach. The goal is not to instrument everything, but to instrument the right things with guaranteed correlation.
Step 1: Define Telemetry Boundaries and SLOs
Map critical user journeys and define Service Level Objectives (SLOs) before instrumenting. SLOs dictate what metrics matter. Example: p99 latency < 200ms, error rate < 0.5%, availability > 99.9%. Telemetry outside these boundaries becomes noise.
Step 2: Instrumentation with OpenTelemetry
OpenTelemetry (OTel) provides vendor-neutral instrumentation. Use auto-instrumentation for HTTP clients, database drivers, and message queues. Apply manual spans for business-critical operations.
import { trace, context, SpanStatusCode } from '@opentelemetry/api';
import { NodeTracerProvider } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
const provider = new NodeTracerProvider();
const exporter = new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces'
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register();
const tracer = trace.getTracer('backend-service');
export async function processPayment(userId: string, amount: number) {
return tracer.startActiveSpan('process.payment', async (span) => {
span.setAttributes({ 'user.id': userId, 'payment.amount': amount });
try {
const result = await paymentGateway.charge(userId, amount);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
throw error;
} finally {
span.end();
}
});
}
Step 3: Enforce Correlation Architecture
Logs must carry trace IDs. Configure your logging library to inject trace_id and span_id from the active OTel context.
import winston from 'winston';
import { trace } from '@opentelemetry/api';
const logger = winston.createLogger({
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json(),
winston.format((info) => {
const spanContext = trace.getSpan(context.active())?.spanContext();
if (spanContext) {
info.trace_id = spanContext.traceId;
info.span_id = spanContext.spanId;
}
return info;
})()
),
transports: [new winston.transports.Console()]
});
Step 4: Collector Pipeline Design
Deploy an OpenTelemetry Collector as a sidecar or daemonset. Configure pipelines for metrics, traces, and logs. Use the batch processor to reduce downstream load, and memory_limiter to prevent OOM crashes.
Step 5: Backend Storage & Query Optimization
Route signals to specialized backends:
- Metrics: Prometheus/VictoriaMetrics (time-series optimized)
- Logs: Loki/ClickHouse (compressed, index-light)
- Traces: Tempo/Jaeger (span-tree optimized)
- Profiles: Pyroscope/Parca (CPU/memory flame graphs)
Query patterns must align with SLOs. Pre-aggregate high-cardinality dimensions. Use histogram buckets for latency instead of raw timestamps.
Architecture Decisions & Rationale
- OTel over vendor SDKs: Prevents lock-in, standardizes context propagation, and aligns with CNCF ecosystem maturity.
- Separate collectors per signal: Isolates failure domains. A log pipeline crash shouldn’t drop trace data.
- Batch processing: Reduces network overhead and downstream ingestion pressure. Configurable
timeout and send_batch_max_size balance latency vs. throughput.
- Context propagation via W3C Trace Context: Standardizes header formats (
traceparent, tracestate) across HTTP, gRPC, and message queues.
- Tiered retention: Hot storage for 7 days, warm for 30, cold for 90+. Aligns cost with actual debugging windows.
Pitfall Guide
1. Logging Without Structure or Context
Unstructured logs force engineers to parse text at query time. JSON formatting with consistent keys enables index-free filtering. Always attach service.name, environment, and trace context.
2. Ignoring Sampling Strategies
Recording every trace at high throughput crashes collectors and inflates storage costs. Use probabilistic sampling for normal traffic, and head-based or tail-based sampling for error paths. OTel’s traceidsampler and probabilisticsampler handle this natively.
3. High Cardinality in Metrics and Labels
Adding user_id, request_id, or ip_address as metric labels explodes series count. Prometheus backends degrade past 100k–500k series. Reserve high-cardinality data for logs and traces. Metrics should use low-cardinality dimensions like service, method, status_code.
4. Alerting on Symptoms Instead of SLOs
Alerting on CPU > 80% or memory > 90% creates noise. Alert on error budget burn rate. If your SLO allows 0.5% errors, trigger alerts when the burn rate exceeds 2x over a 1-hour window. This aligns alerts with user impact, not infrastructure state.
5. Missing Trace Context Propagation
Traces break when context isn’t forwarded across service boundaries. Ensure HTTP headers, gRPC metadata, and message queue attributes carry traceparent. Validate propagation in integration tests.
6. Treating Observability as a One-Time Setup
Telemetry drifts as services evolve. New endpoints, database queries, and external calls generate blind spots. Implement automated instrumentation audits and CI checks that verify span coverage for critical paths.
7. Neglecting Retention and Lifecycle Policies
Storing everything forever wastes budget and degrades query performance. Define clear retention tiers. Use log rotation, metric downsampling, and trace expiration. Automate lifecycle rules in your collector or backend configuration.
Best Practices from Production:
- Implement SLO-driven alerting with multi-window burn rate strategies
- Use structured logging with schema validation
- Apply adaptive sampling based on error rates
- Enforce cardinality budgets via metric naming conventions
- Test observability pipelines in staging with synthetic traffic
- Document correlation strategies in runbooks
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Early-stage startup | OTel + managed APM (Datadog/New Relic) | Fastest time-to-value, minimal ops overhead | Higher per-GB cost, predictable pricing |
| Mid-scale engineering team | OTel + open-source stack (Prometheus/Loki/Tempo) | Full control, customizable retention, lower storage costs | Requires collector maintenance, moderate infra cost |
| Enterprise compliance | OTel + self-hosted + audit logging + RBAC | Data sovereignty, audit trails, centralized governance | High initial setup, lower long-term marginal cost |
| Cost-constrained workloads | Adaptive sampling + metric aggregation + cold storage | Reduces ingestion volume while preserving SLO visibility | Query latency increases for historical data |
Configuration Template
OpenTelemetry Collector (otel-collector-config.yaml)
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
send_batch_max_size: 1000
memory_limiter:
check_interval: 1s
limit_mib: 1024
spike_limit_mib: 256
probabilisticsampler:
sampling_percentage: 20
attributes:
actions:
- key: environment
value: production
action: upsert
exporters:
prometheus:
endpoint: "0.0.0.0:8889"
namespace: backend
loki:
endpoint: "http://loki:3100/loki/api/v1/push"
otlp:
endpoint: "tempo:4317"
tls:
insecure: true
service:
pipelines:
metrics:
receivers: [otlp]
processors: [batch, memory_limiter]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [batch, attributes]
exporters: [loki]
traces:
receivers: [otlp]
processors: [probabilisticsampler, batch]
exporters: [otlp]
Node.js OTel Initialization (instrumentation.ts)
import { NodeSDK } from '@opentelemetry/sdk-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-http';
import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({ url: 'http://otel-collector:4318/v1/traces' }),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: 'http://otel-collector:4318/v1/metrics' }),
exportIntervalMillis: 10000
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: 'backend-api'
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown().catch(console.error));
Quick Start Guide
- Install dependencies:
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-trace-otlp-http winston
- Add instrumentation file: Create
instrumentation.ts with the OTel SDK initialization code. Import it at the very top of your entry point (import './instrumentation';).
- Deploy collector: Run
docker run -p 4317:4317 -p 4318:4318 -v $(pwd)/otel-collector-config.yaml:/etc/otel-collector-config.yaml otel/opentelemetry-collector-contrib --config /etc/otel-collector-config.yaml
- Validate telemetry: Generate traffic, then query
http://localhost:8889/metrics for Prometheus metrics and check your log output for trace_id fields. Verify spans appear in your trace backend within 5–10 seconds.