--- name: opentelemetry description: Instrument applications and infrastructure with OpenTelemetry for unified traces, metrics, and logs. Use when implementing distributed tracing, service-level troubleshooting, or vendor-neutral observability. license: MIT metadata: author: devops-skills version: "1.0" --- # OpenTelemetry Adopt vendor-neutral telemetry with consistent instrumentation across services. ## When to Use This Skill - Debugging latency across microservices - Standardizing observability data model and naming - Sending telemetry to Prometheus, Grafana, Datadog, or OTLP backends - Building SLO dashboards with trace-to-log correlation - Instrumenting Python or Node.js applications with tracing and metrics - Setting up auto-instrumentation for existing services without code changes ## Prerequisites - Application services running in containers or on VMs - Backend for traces (Jaeger, Tempo, Datadog, or any OTLP receiver) - Backend for metrics (Prometheus, Mimir, or OTLP receiver) - Kubernetes cluster (for collector deployment) or VM with systemd - Network access from services to collector, and collector to backends ## Core Workflow 1. Define semantic conventions for services, environments, and versions. 2. Add SDK or auto-instrumentation in each service. 3. Run an OpenTelemetry Collector to receive, transform, and export telemetry. 4. Validate cardinality and sampling to control cost. 5. Create golden signals dashboards and alerting from collected data. ## Collector Production Configuration ```yaml # otel-collector-config.yaml receivers: otlp: protocols: grpc: endpoint: 0.0.0.0:4317 http: endpoint: 0.0.0.0:4318 # Scrape Prometheus endpoints prometheus: config: scrape_configs: - job_name: "kubernetes-pods" kubernetes_sd_configs: - role: pod relabel_configs: - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape] action: keep regex: "true" - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_port] action: replace target_label: __address__ regex: (.+) replacement: $$1 # Host metrics for infrastructure monitoring hostmetrics: collection_interval: 30s scrapers: cpu: {} memory: {} disk: {} network: {} load: {} processors: batch: send_batch_size: 1024 timeout: 5s memory_limiter: check_interval: 1s limit_mib: 512 spike_limit_mib: 128 attributes: actions: - key: deployment.environment value: production action: upsert # Drop high-cardinality attributes to control cost filter/drop-debug: traces: span: - 'attributes["http.request.header.x-debug"] == "true"' # Reduce cardinality on URL paths transform/normalize-routes: trace_statements: - context: span statements: - replace_pattern(attributes["url.path"], "/users/[0-9]+", "/users/{id}") - replace_pattern(attributes["url.path"], "/orders/[0-9]+", "/orders/{id}") # Resource detection for cloud environments resourcedetection: detectors: [env, system, gcp, aws, azure] timeout: 5s exporters: # Send traces to Tempo/Jaeger otlp/traces: endpoint: tempo:4317 tls: insecure: true # Send metrics to Prometheus via remote write prometheusremotewrite: endpoint: http://mimir:9009/api/v1/push tls: insecure: true # Send logs to Loki otlp/logs: endpoint: loki:4317 tls: insecure: true # Debug exporter for development debug: verbosity: basic service: telemetry: logs: level: info metrics: address: 0.0.0.0:8888 pipelines: traces: receivers: [otlp] processors: [memory_limiter, resourcedetection, transform/normalize-routes, batch, attributes] exporters: [otlp/traces] metrics: receivers: [otlp, prometheus, hostmetrics] processors: [memory_limiter, resourcedetection, batch, attributes] exporters: [prometheusremotewrite] logs: receivers: [otlp] processors: [memory_limiter, resourcedetection, batch, attributes] exporters: [otlp/logs] ``` ## Collector Kubernetes Deployment ```yaml # otel-collector-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: otel-collector namespace: observability spec: replicas: 2 selector: matchLabels: app: otel-collector template: metadata: labels: app: otel-collector spec: containers: - name: collector image: otel/opentelemetry-collector-contrib:0.98.0 args: ["--config=/etc/otel/config.yaml"] ports: - containerPort: 4317 name: otlp-grpc - containerPort: 4318 name: otlp-http - containerPort: 8888 name: metrics resources: requests: cpu: 200m memory: 256Mi limits: cpu: "1" memory: 512Mi volumeMounts: - name: config mountPath: /etc/otel livenessProbe: httpGet: path: / port: 13133 readinessProbe: httpGet: path: / port: 13133 volumes: - name: config configMap: name: otel-collector-config --- apiVersion: v1 kind: Service metadata: name: otel-collector namespace: observability spec: selector: app: otel-collector ports: - name: otlp-grpc port: 4317 targetPort: 4317 - name: otlp-http port: 4318 targetPort: 4318 - name: metrics port: 8888 targetPort: 8888 ``` ## Python SDK Instrumentation ```python # tracing_setup.py """Initialize OpenTelemetry tracing and metrics for a Python service.""" from opentelemetry import trace, metrics from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader from opentelemetry.sdk.resources import Resource from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter from opentelemetry.instrumentation.requests import RequestsInstrumentor from opentelemetry.instrumentation.flask import FlaskInstrumentor from opentelemetry.instrumentation.sqlalchemy import SQLAlchemyInstrumentor import os def init_telemetry(service_name: str, service_version: str): """Initialize OTel SDK with traces and metrics.""" resource = Resource.create({ "service.name": service_name, "service.version": service_version, "deployment.environment": os.getenv("DEPLOY_ENV", "development"), }) # Traces trace_exporter = OTLPSpanExporter( endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317"), insecure=True, ) tracer_provider = TracerProvider(resource=resource) tracer_provider.add_span_processor(BatchSpanProcessor(trace_exporter)) trace.set_tracer_provider(tracer_provider) # Metrics metric_exporter = OTLPMetricExporter( endpoint=os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT", "http://otel-collector:4317"), insecure=True, ) metric_reader = PeriodicExportingMetricReader(metric_exporter, export_interval_millis=15000) meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader]) metrics.set_meter_provider(meter_provider) # Auto-instrument common libraries RequestsInstrumentor().instrument() SQLAlchemyInstrumentor().instrument() return trace.get_tracer(service_name), metrics.get_meter(service_name) # Usage example tracer, meter = init_telemetry("order-service", "1.2.0") # Custom span with tracer.start_as_current_span("process_order") as span: span.set_attribute("order.id", order_id) span.set_attribute("order.total", total) # ... business logic ... # Custom metric request_counter = meter.create_counter( "app.requests", description="Total application requests", ) request_counter.add(1, {"route": "/api/orders", "method": "POST"}) ``` ## Node.js SDK Instrumentation ```javascript // tracing.js // Initialize OpenTelemetry for a Node.js service. // Load this file BEFORE any other imports: node -r ./tracing.js app.js const { NodeSDK } = require("@opentelemetry/sdk-node"); const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc"); const { OTLPMetricExporter } = require("@opentelemetry/exporter-metrics-otlp-grpc"); const { PeriodicExportingMetricReader } = require("@opentelemetry/sdk-metrics"); const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node"); const { Resource } = require("@opentelemetry/resources"); const { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } = require("@opentelemetry/semantic-conventions"); const resource = new Resource({ [ATTR_SERVICE_NAME]: process.env.SERVICE_NAME || "node-service", [ATTR_SERVICE_VERSION]: process.env.SERVICE_VERSION || "1.0.0", "deployment.environment": process.env.DEPLOY_ENV || "development", }); const sdk = new NodeSDK({ resource, traceExporter: new OTLPTraceExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://otel-collector:4317", }), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter({ url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT || "http://otel-collector:4317", }), exportIntervalMillis: 15000, }), instrumentations: [ getNodeAutoInstrumentations({ "@opentelemetry/instrumentation-http": { ignoreIncomingPaths: ["/health", "/ready"], }, "@opentelemetry/instrumentation-express": { enabled: true }, "@opentelemetry/instrumentation-pg": { enabled: true }, "@opentelemetry/instrumentation-redis": { enabled: true }, }), ], }); sdk.start(); process.on("SIGTERM", () => sdk.shutdown()); ``` ## Auto-Instrumentation with Kubernetes Operator ```yaml # otel-auto-instrumentation.yaml # Install the OTel Operator first: # helm install opentelemetry-operator open-telemetry/opentelemetry-operator \ # --namespace observability --create-namespace # Define instrumentation for Python services apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: python-instrumentation namespace: default spec: exporter: endpoint: http://otel-collector.observability:4317 propagators: - tracecontext - baggage sampler: type: parentbased_traceidratio argument: "0.25" python: image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-python:0.44b0 env: - name: OTEL_PYTHON_LOG_CORRELATION value: "true" --- # Define instrumentation for Node.js services apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: nodejs-instrumentation namespace: default spec: exporter: endpoint: http://otel-collector.observability:4317 propagators: - tracecontext - baggage sampler: type: parentbased_traceidratio argument: "0.25" nodejs: image: ghcr.io/open-telemetry/opentelemetry-operator/autoinstrumentation-nodejs:0.49.1 ``` To instrument a pod, add the annotation: ```yaml # For Python: metadata: annotations: instrumentation.opentelemetry.io/inject-python: "true" # For Node.js: metadata: annotations: instrumentation.opentelemetry.io/inject-nodejs: "true" ``` ## Sampling Strategies ```yaml # Tail-based sampling config (in collector) processors: tail_sampling: decision_wait: 10s num_traces: 100000 policies: # Always keep error traces - name: errors type: status_code status_code: status_codes: [ERROR] # Always keep slow traces (> 2s) - name: slow-traces type: latency latency: threshold_ms: 2000 # Sample 10% of successful traces - name: normal-traffic type: probabilistic probabilistic: sampling_percentage: 10 # Always keep traces with specific attributes - name: important-users type: string_attribute string_attribute: key: user.tier values: [enterprise, premium] # Rate limit per service to prevent one service from dominating - name: rate-limit type: rate_limiting rate_limiting: spans_per_second: 500 ``` ## Best Practices - Use tail-based sampling for high-volume production traces. - Tag telemetry with `service.name`, `service.version`, and `deployment.environment`. - Drop noisy attributes early in the collector. - Keep metric label cardinality low for stable query performance. - Use resource detectors to automatically populate cloud metadata. - Separate collector pools for traces vs metrics if volume requires it. - Set memory_limiter on every collector pipeline to prevent OOM. - Use the contrib collector image for production (includes more receivers/exporters). ## Troubleshooting | Symptom | Check | Fix | |---------|-------|-----| | No traces arriving at backend | Collector logs for export errors | Verify endpoint URL and network policy | | Missing spans in a trace | Propagation headers stripped by proxy | Configure proxy to pass `traceparent` header | | High memory on collector | Too many in-flight traces for tail sampling | Reduce `num_traces` or increase memory limit | | Metric cardinality explosion | Unbounded label values (user IDs, URLs) | Add transform processor to normalize values | | Auto-instrumentation not working | Pod annotation missing or operator not running | Verify operator is healthy and annotation is correct | | Duplicate metrics | Both SDK and auto-instrumentation active | Use only one instrumentation method per signal | ## Related Skills - [prometheus-grafana](../prometheus-grafana/) - Dashboarding and alerting - [datadog](../datadog/) - Managed observability backend - [alerting-oncall](../alerting-oncall/) - On-call routing and escalation - [rag-observability-evals](../../ai/rag-observability-evals/) - RAG-specific observability - [agent-observability](../../ai/agent-observability/) - AI agent tracing