OpenTelemetry: Unified Observability
Instrument applications with OpenTelemetry to emit traces, metrics, and logs in a vendor-neutral format. Learn the OTel data model, collectors, and backends.
OpenTelemetry (OTel) is the open standard for producing and collecting telemetry—traces, metrics, and logs—from applications and infrastructure. It replaces a fragmented landscape of vendor-specific SDKs with one unified API.
Learning outcomes
By the end you can:
- understand the OTel data model (traces, spans, metrics, logs)
- instrument a Node.js or Python application with the OTel SDK
- run the OTel Collector to receive and route signals
- export to Jaeger (traces) and Prometheus (metrics)
1) The three pillars — and how OTel unifies them
| Signal | What it tells you | OTel component |
|---|---|---|
| Traces | End-to-end path of a request across services | Spans + Trace IDs |
| Metrics | Numeric measurements over time | Instruments (counter, gauge, histogram) |
| Logs | Structured event records | Log Records with trace context |
The key insight: OTel correlates all three.
A log record carries the same trace_id and span_id as the trace for that request, letting you jump from a log to the full trace in one click.
2) Core concepts
- Tracer: creates and manages spans
- Span: a single unit of work (e.g., one HTTP handler call)
- Trace: a tree of spans for one end-to-end request
- Context propagation: passes trace IDs between services via HTTP headers (
traceparent) - OTLP: OpenTelemetry Protocol—the wire format for all signals
- Collector: a proxy that receives, processes, and exports telemetry
3) OTel Collector — receive and route
The Collector receives OTLP data from your apps and routes it to backends.
# docker-compose.yml
version: "3.9"
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.101.0
volumes:
- ./otel-collector.yml:/etc/otel/config.yml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
- "8889:8889" # Prometheus metrics exporter
command: ["--config=/etc/otel/config.yml"]
jaeger:
image: jaegertracing/all-in-one:1.57
ports:
- "16686:16686" # Jaeger UI
prometheus:
image: prom/prometheus:v2.52.0
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
# otel-collector.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 1s
send_batch_size: 1024
resource:
attributes:
- key: deployment.environment
value: development
action: insert
exporters:
jaeger:
endpoint: jaeger:14250
tls:
insecure: true
prometheus:
endpoint: "0.0.0.0:8889"
namespace: myapp
logging:
verbosity: normal
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, resource]
exporters: [jaeger, logging]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheus, logging]
logs:
receivers: [otlp]
processors: [batch]
exporters: [logging]
4) Instrument a Node.js application
Install the OTel SDK:
npm install \
@opentelemetry/sdk-node \
@opentelemetry/auto-instrumentations-node \
@opentelemetry/exporter-trace-otlp-http \
@opentelemetry/exporter-metrics-otlp-http
Create tracing.js and load it before your app:
// tracing.js
const { NodeSDK } = require('@opentelemetry/sdk-node');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-trace-otlp-http');
const { OTLPMetricExporter } = require('@opentelemetry/exporter-metrics-otlp-http');
const { PeriodicExportingMetricReader } = require('@opentelemetry/sdk-metrics');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { Resource } = require('@opentelemetry/resources');
const { SEMRESATTRS_SERVICE_NAME } = require('@opentelemetry/semantic-conventions');
const sdk = new NodeSDK({
resource: new Resource({
[SEMRESATTRS_SERVICE_NAME]: 'my-node-service',
}),
traceExporter: new OTLPTraceExporter({
url: 'http://localhost:4318/v1/traces',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({
url: 'http://localhost:4318/v1/metrics',
}),
exportIntervalMillis: 10_000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
}),
],
});
sdk.start();
process.on('SIGTERM', () => sdk.shutdown());
Start with:
node -r ./tracing.js server.js
Auto-instrumentation automatically traces all HTTP requests to/from your Express app without any code changes.
5) Manual spans — trace custom code
const opentelemetry = require('@opentelemetry/api');
const tracer = opentelemetry.trace.getTracer('my-service');
async function processOrder(orderId) {
// Create a span for this operation
const span = tracer.startSpan('processOrder', {
attributes: {
'order.id': orderId,
'order.source': 'web',
},
});
try {
await validateOrder(orderId); // child spans created here
await chargePayment(orderId);
await fulfillOrder(orderId);
span.setStatus({ code: opentelemetry.SpanStatusCode.OK });
} catch (err) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: err.message,
});
span.recordException(err);
throw err;
} finally {
span.end();
}
}
6) Instrument a Python application
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap --action=install
Auto-instrument without code changes:
OTEL_SERVICE_NAME=my-python-service \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
opentelemetry-instrument python app.py
Manual spans in Python:
from opentelemetry import trace
tracer = trace.get_tracer(__name__)
def process_payment(payment_id: str):
with tracer.start_as_current_span("process_payment") as span:
span.set_attribute("payment.id", payment_id)
try:
result = charge_card(payment_id)
span.set_attribute("payment.status", "success")
return result
except Exception as e:
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR, str(e))
raise
7) View traces in Jaeger
Open Jaeger UI at http://localhost:16686. Select your service name and click Find Traces. Click a trace to see the full span tree—each span shows its duration, attributes, and any recorded exceptions.
Next steps
- Alertmanager: routing OTel-based Prometheus alerts to Slack and PagerDuty
- Incident management: using traces to speed up root cause analysis
- Production OTel: sampling strategies to reduce cost at scale