Microservices Observability with OpenTelemetry 2026: Tracing, Metrics, Logs
Complete guide to implementing observability in microservices using OpenTelemetry in 2026. Distributed tracing, metrics collection, structured logging, and production-ready setup with Jaeger, Prometheus, and Grafana.
Microservices Observability with OpenTelemetry 2026: Tracing, Metrics, Logs
When a request fails in a monolith, you look at one set of logs. When a request fails across 12 microservices, youβre in a different kind of pain. Observability β the ability to understand what your system is doing from the outside β is what separates teams that debug incidents in 10 minutes from teams that spend 4 hours on bridges.
OpenTelemetry (OTel) has become the standard for cloud-native observability. In 2026, itβs the right default for any microservices architecture.
The Three Pillars of Observability
Traces track a single requestβs journey across all services. A trace is composed of spans β each span represents one operation (an HTTP call, a database query, a cache lookup).
Metrics are time-series numbers: request rates, error rates, latencies, queue depths. They answer βhow is the system performing right now?β
Logs are timestamped records of events. In a distributed system, logs must be structured (JSON) and correlated to traces by trace ID.
Together, these three give you the full picture. Traces show you where time is spent. Metrics alert you when thresholds are exceeded. Logs give you the detail to understand why.
OpenTelemetry Architecture
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Your Applications β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Service A β β Service B β β Service C β β
β β OTel SDK β β OTel SDK β β OTel SDK β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
βββββββββββΌβββββββββββββββββΌβββββββββββββββββΌβββββββββββββ
β β β
ββββββββββββββββββ΄βββββββββββββββββ
β
OTLP Protocol
β
ββββββββΌβββββββ
β OTel β
β Collector β
ββββββββ¬βββββββ
β
ββββββββββββββββββΌβββββββββββββββββ
βΌ βΌ βΌ
ββββββββββββ ββββββββββββ ββββββββββββ
β Jaeger β βPrometheusβ β Loki β
β (traces) β β(metrics) β β (logs) β
ββββββββββββ ββββββββββββ ββββββββββββ
β
ββββββββΌβββββββ
β Grafana β
β Dashboard β
βββββββββββββββ
The OTel Collector is the key component. Your services send telemetry data to the Collector via OTLP (OpenTelemetry Protocol), and the Collector forwards to your storage backends. This decouples your application code from the observability backend β you can swap Jaeger for Tempo without touching application code.
Instrumenting a Node.js Service
Install Dependencies
npm install @opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http
Setup (tracing.js β load before everything else)
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME || 'unknown-service',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.SERVICE_VERSION || '0.0.1',
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV || 'development',
}),
traceExporter: new OTLPTraceExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || 'http://otel-collector:4318/v1/metrics',
}),
exportIntervalMillis: 30000, // Export every 30 seconds
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-fs': { enabled: false }, // Too noisy
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true },
'@opentelemetry/instrumentation-redis': { enabled: true },
}),
],
});
sdk.start();
process.on('SIGTERM', () => {
sdk.shutdown().then(() => process.exit(0));
});
Start with tracing
// package.json
{
"scripts": {
"start": "node --require ./tracing.js src/server.js"
}
}
The auto-instrumentations-node package automatically traces:
- Incoming and outgoing HTTP requests
- Express route handlers
- PostgreSQL queries
- Redis operations
- MongoDB operations
- gRPC calls
You get distributed tracing with zero code changes to your application logic.
Custom Spans and Attributes
Auto-instrumentation handles infrastructure. For business logic, add custom spans:
const { trace, context, SpanStatusCode } = require('@opentelemetry/api');
const tracer = trace.getTracer('order-service', '1.0.0');
async function processOrder(orderId) {
return tracer.startActiveSpan('process-order', async (span) => {
try {
// Add context to the span
span.setAttributes({
'order.id': orderId,
'order.source': 'web',
});
const order = await db.findOrder(orderId);
span.setAttribute('order.total', order.total);
span.setAttribute('order.items', order.items.length);
await validateInventory(order);
await chargePayment(order);
await scheduleShipment(order);
span.setStatus({ code: SpanStatusCode.OK });
return order;
} catch (error) {
span.recordException(error);
span.setStatus({
code: SpanStatusCode.ERROR,
message: error.message,
});
throw error;
} finally {
span.end();
}
});
}
Custom Metrics
const { metrics } = require('@opentelemetry/api');
const meter = metrics.getMeter('order-service', '1.0.0');
// Counter β only goes up
const ordersProcessed = meter.createCounter('orders.processed', {
description: 'Total number of orders processed',
});
// Histogram β for latencies and sizes
const orderProcessingTime = meter.createHistogram('order.processing.duration', {
description: 'Time to process an order (ms)',
unit: 'ms',
});
// Gauge β can go up or down
const activeOrders = meter.createObservableGauge('orders.active', {
description: 'Number of orders currently being processed',
});
activeOrders.addCallback((result) => {
result.observe(getActiveOrderCount());
});
// Usage
async function processOrder(orderId) {
const startTime = Date.now();
try {
const order = await doProcess(orderId);
ordersProcessed.add(1, { status: 'success', channel: order.channel });
return order;
} catch (error) {
ordersProcessed.add(1, { status: 'error', error: error.code });
throw error;
} finally {
orderProcessingTime.record(Date.now() - startTime, { orderId });
}
}
Structured Logging with Trace Correlation
Logs need trace IDs to be useful in distributed systems:
const { context, trace } = require('@opentelemetry/api');
function getTraceContext() {
const span = trace.getActiveSpan();
if (!span) return {};
const spanContext = span.spanContext();
return {
traceId: spanContext.traceId,
spanId: spanContext.spanId,
};
}
// Custom logger that automatically injects trace context
const logger = {
info: (message, data = {}) => {
console.log(JSON.stringify({
level: 'info',
timestamp: new Date().toISOString(),
message,
service: process.env.SERVICE_NAME,
...getTraceContext(),
...data,
}));
},
error: (message, error, data = {}) => {
console.error(JSON.stringify({
level: 'error',
timestamp: new Date().toISOString(),
message,
error: {
name: error.name,
message: error.message,
stack: error.stack,
},
service: process.env.SERVICE_NAME,
...getTraceContext(),
...data,
}));
},
};
// Usage
logger.info('Order processed', { orderId, total });
Now when you see an error in Loki, you can copy the traceId and find the full request trace in Jaeger.
OTel Collector Configuration
# otel-collector-config.yaml
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 1s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
filter:
traces:
span:
- 'attributes["http.route"] == "/health"' # Drop health check spans
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
prometheus:
endpoint: 0.0.0.0:8889
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, filter, batch]
exporters: [jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheus]
logs:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [loki]
Docker Compose Setup for Development
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.97.0
command: ["--config=/etc/otel-collector-config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus metrics
jaeger:
image: jaegertracing/all-in-one:1.56
ports:
- "16686:16686" # UI
environment:
COLLECTOR_OTLP_ENABLED: true
prometheus:
image: prom/prometheus:v2.51.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
loki:
image: grafana/loki:2.9.5
ports:
- "3100:3100"
grafana:
image: grafana/grafana:10.4.0
ports:
- "3001:3000"
environment:
GF_AUTH_ANONYMOUS_ENABLED: true
GF_AUTH_ANONYMOUS_ORG_ROLE: Admin
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
Essential Grafana Dashboards
For microservices observability, configure dashboards for:
Service RED metrics (Rate, Errors, Duration):
- Request rate per service
- Error rate (4xx, 5xx separately)
- P50, P95, P99 latency histograms
Service dependency map:
- Which services call which other services
- Error rates on each dependency edge
- Latency of each dependency call
Infrastructure:
- CPU and memory per pod
- Pod restart count
- Node saturation
The DevPlaybook observability tools page has ready-to-use dashboard JSON configs.
OpenTelemetry Python (FastAPI Example)
# tracing.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.httpx import HTTPXClientInstrumentor
from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor
def setup_tracing(app, service_name: str):
provider = TracerProvider()
exporter = OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
provider.add_span_processor(BatchSpanProcessor(exporter))
trace.set_tracer_provider(provider)
FastAPIInstrumentor.instrument_app(app)
HTTPXClientInstrumentor().instrument()
SQLAlchemyInstrumentor().instrument()
# main.py
from fastapi import FastAPI
from tracing import setup_tracing
app = FastAPI()
setup_tracing(app, "user-service")
Conclusion
OpenTelemetry in 2026 is mature, well-supported, and the correct default for any microservices observability strategy. The key decisions are:
- Start with auto-instrumentation β get traces flowing before writing any custom instrumentation
- Use the OTel Collector β never export directly from your application to the backend
- Correlate logs with traces β inject traceId into every log line
- Add custom spans for business operations β auto-instrumentation misses the semantics of your domain
- Alert on RED metrics β Rate, Errors, Duration per service
The setup cost is one afternoon. The debugging time saved in your first production incident pays it back.
Related tools:
50 CLI Commands That Save Hours Every Week
The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.
- Git mastery (10 cmds)
- File & search wizardry
- Docker & DevOps tricks
- Process & network tools
- Productivity boosters
- macOS & Linux install guide
No spam. 1 email/week max. Unsubscribe anytime.
Found this guide useful? Check out our free developer tools.
Affiliate disclosure: Some links below are affiliate links β we may earn a small commission at no extra cost to you. Learn more.
Recommended Tools & Resources
DigitalOcean
$200 credit for new users. Simple, affordable cloud hosting for developers.
GitHub Student Pack
Free access to 100+ developer tools. Perfect for students and new devs.
Vercel
Deploy frontend apps instantly. Free tier is generous for side projects.
DevPlaybook Products
Boilerplates, scripts & AI toolkits to 10x your dev workflow.