Skip to main content
Monitoring & Observability advanced Lesson 4 of 5

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

SignalWhat it tells youOTel component
TracesEnd-to-end path of a request across servicesSpans + Trace IDs
MetricsNumeric measurements over timeInstruments (counter, gauge, histogram)
LogsStructured event recordsLog 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

Frequently Asked Questions

What problem does OpenTelemetry solve compared to using Prometheus + Jaeger separately?
OpenTelemetry provides a single, vendor-neutral SDK and wire format (OTLP) for all three signals—traces, metrics, and logs. Instead of integrating separate SDKs for each backend, you instrument once and route signals to any backend through the OTel Collector.
Do I need to change my existing Prometheus setup to use OpenTelemetry?
No. The OTel Collector can export metrics in Prometheus format, so you can adopt OTel incrementally. Existing Prometheus scrapers keep working while you migrate instrumentation to the OTel SDK.