opentelemetry-expert
Expert OpenTelemetry coverage including SDK architecture, semantic conventions, W3C context propagation, auto vs manual instrumentation, OTel Collector pipeline configuration, sampling strategies (head/tail/parent-based), OTLP protocol, resource attributes, and backend integration
OpenTelemetry Expert
OpenTelemetry is the CNCF standard for generating, collecting, and exporting telemetry data. It solves
the vendor lock-in problem that plagued observability for a decade: instrument once with the OTel SDK,
then route to any backend (Datadog, New Relic, Jaeger, Tempo, Zipkin) by changing collector config.
Core Mental Model
OTel has three planes: instrumentation (SDK in your app), collection (OTel Collector as agent/gateway),
and backends (where data lives and is queried). The SDK emits signals (traces, metrics, logs) using the
OTLP protocol. The Collector receives, processes, and fans out to backends. This separation means you
instrument once and change backends without touching application code. Semantic conventions are the contract
between your instrumentation and your dashboards — violate them and your out-of-the-box dashboards break.
OTel Architecture
┌─────────────────────────────────────────────────────────┐
│ Your Application │
│ │
│ OTel SDK │
│ ├── TracerProvider → creates Tracers → creates Spans │
│ ├── MeterProvider → creates Meters → creates Instruments│
│ └── LoggerProvider → creates Loggers → creates LogRecords│
│ │ │
│ │ OTLP (gRPC port 4317 / HTTP port 4318) │
└────────────────┼─────────────────────────────────────────┘
│
┌───────▼──────────┐
│ OTel Collector │ (agent on host OR gateway cluster)
│ │
│ Receivers │ OTLP, Jaeger, Zipkin, Prometheus
│ Processors │ batch, filter, transform, sample
│ Exporters │ Jaeger, Tempo, Prometheus, OTLP
└─────────┬─────────┘
│
┌──────────┼──────────┐
▼ ▼ ▼
Jaeger Tempo Prometheus
(traces) (traces) (metrics)
Semantic Conventions
Semantic conventions define the standard attribute names for common operations. Using them unlocks
automatic dashboards and correlations in observability backends.
HTTP Spans
# ✅ Standard HTTP server span attributes
span.set_attribute("http.method", "GET") # HTTP method
span.set_attribute("http.target", "/api/orders/123") # Request target
span.set_attribute("http.host", "api.example.com") # Host header
span.set_attribute("http.scheme", "https") # URL scheme
span.set_attribute("http.status_code", 200) # Response status
span.set_attribute("http.flavor", "1.1") # HTTP version
span.set_attribute("net.peer.ip", "10.0.1.5") # Client IP
# DB spans
span.set_attribute("db.system", "postgresql")
span.set_attribute("db.name", "orders")
span.set_attribute("db.operation", "SELECT")
span.set_attribute("db.statement", "SELECT * FROM orders WHERE id = $1")
span.set_attribute("net.peer.name", "db.internal")
span.set_attribute("net.peer.port", 5432)
# Messaging spans (Kafka, RabbitMQ, Pub/Sub)
span.set_attribute("messaging.system", "kafka")
span.set_attribute("messaging.destination", "orders-topic")
span.set_attribute("messaging.operation", "receive")
span.set_attribute("messaging.message_id", "msg-abc123")
# Custom app attributes use your namespace
span.set_attribute("app.order.id", order_id)
span.set_attribute("app.user.tier", "premium")
Span Naming Convention
# Service + operation pattern:
http: "GET /api/orders/{id}" (template, not actual ID)
db: "SELECT orders"
rpc: "OrderService/CreateOrder"
kafka: "orders process"
queue: "payment_queue receive"
# ❌ Wrong (too specific / high-cardinality):
"GET /api/orders/12345" # Contains actual order ID
"process order 12345" # Cardinality explosion
Context Propagation: W3C TraceContext
W3C TraceContext HTTP headers:
traceparent: 00-{trace-id}-{parent-span-id}-{flags}
tracestate: vendor-specific state
Example:
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
^^ ^^ flags (01=sampled)
version parent span ID
Baggage (cross-boundary user data):
baggage: userId=123, region=us-east-1, tenantId=acme
from opentelemetry import trace, baggage
from opentelemetry.propagate import inject, extract
from opentelemetry.baggage.propagation import W3CBaggagePropagator
# Inject context into outgoing HTTP request (FastAPI → downstream service)
headers = {}
inject(headers) # Adds traceparent and baggage headers automatically
async def call_payment_service(order_id: str):
headers = {"Content-Type": "application/json"}
inject(headers) # ← This propagates the current trace context
async with httpx.AsyncClient() as client:
return await client.post(
"http://payment-service/charge",
headers=headers,
json={"order_id": order_id}
)
# Extract context from incoming request (in middleware)
def get_context_from_request(request):
return extract(dict(request.headers))
# Set and read baggage
ctx = baggage.set_baggage("user.tier", "premium")
with trace.use_context(ctx):
# All spans created here will have access to baggage
user_tier = baggage.get_baggage("user.tier")
Instrumentation: Auto vs Manual
JavaScript/Express Auto-Instrumentation
// instrumentation.js — load before your app
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 { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const sdk = new NodeSDK({
resource: new Resource({
[SemanticResourceAttributes.SERVICE_NAME]: 'order-api',
[SemanticResourceAttributes.SERVICE_VERSION]: process.env.APP_VERSION,
[SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: process.env.NODE_ENV,
}),
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4317',
}),
metricReader: new PeriodicExportingMetricReader({
exporter: new OTLPMetricExporter({ url: 'http://otel-collector:4317' }),
exportIntervalMillis: 10000,
}),
instrumentations: [
getNodeAutoInstrumentations({
'@opentelemetry/instrumentation-http': { enabled: true },
'@opentelemetry/instrumentation-express': { enabled: true },
'@opentelemetry/instrumentation-pg': { enabled: true }, // PostgreSQL
'@opentelemetry/instrumentation-redis': { enabled: true },
}),
],
});
sdk.start();
// Run with: node -r ./instrumentation.js app.js
Python Manual Spans with Rich Attributes
from opentelemetry import trace
from opentelemetry.trace import SpanKind, Status, StatusCode
tracer = trace.get_tracer("order-service", "1.0.0")
async def fulfill_order(order_id: str) -> FulfillmentResult:
"""Full span lifecycle with proper error handling."""
with tracer.start_as_current_span(
"order.fulfill",
kind=SpanKind.INTERNAL,
attributes={
"app.order.id": order_id,
"app.service.component": "fulfillment",
}
) as span:
# Add events (timestamped annotations within span)
span.add_event("fulfillment.started", {"queue_depth": get_queue_depth()})
# Child span for DB operation
with tracer.start_as_current_span("db.get_order") as db_span:
db_span.set_attribute("db.system", "postgresql")
db_span.set_attribute("db.operation", "SELECT")
order = await db.fetch_order(order_id)
if not order:
span.set_attribute("app.order.found", False)
span.set_status(Status(StatusCode.ERROR, "Order not found"))
raise OrderNotFoundError(order_id)
span.set_attribute("app.order.found", True)
span.set_attribute("app.order.items_count", len(order.items))
# External service call
with tracer.start_as_current_span(
"warehouse.reserve_items",
kind=SpanKind.CLIENT
) as ext_span:
ext_span.set_attribute("rpc.system", "grpc")
ext_span.set_attribute("rpc.service", "WarehouseService")
ext_span.set_attribute("rpc.method", "ReserveItems")
reservation = await warehouse_client.reserve(order.items)
span.add_event("items.reserved", {
"reservation_id": reservation.id,
"items_count": len(order.items)
})
return FulfillmentResult(order=order, reservation=reservation)
OTel Collector: Full Production Config
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
# Prometheus metrics scraping
prometheus:
config:
global:
scrape_interval: 15s
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 (CPU, memory, disk, network)
hostmetrics:
collection_interval: 30s
scrapers:
cpu: {}
disk: {}
filesystem: {}
memory: {}
network: {}
processors:
# Resource detection: auto-populate cloud metadata
resourcedetection:
detectors: [env, gcp, aws, azure]
timeout: 5s
# Add k8s metadata to all telemetry
k8sattributes:
auth_type: "serviceAccount"
passthrough: false
extract:
metadata:
- k8s.pod.name
- k8s.pod.uid
- k8s.deployment.name
- k8s.namespace.name
- k8s.node.name
# Tail-based sampling
tail_sampling:
decision_wait: 10s
num_traces: 100000
policies:
- name: keep-errors
type: status_code
status_code: {status_codes: [ERROR]}
- name: keep-slow
type: latency
latency: {threshold_ms: 500}
- name: keep-10pct
type: probabilistic
probabilistic: {sampling_percentage: 10}
# Filter out health check noise
filter/health:
traces:
span:
- 'attributes["http.target"] == "/health"'
- 'attributes["http.target"] == "/metrics"'
batch:
timeout: 5s
send_batch_size: 512
memory_limiter:
limit_mib: 1024
spike_limit_mib: 256
check_interval: 5s
exporters:
otlp/jaeger:
endpoint: jaeger-collector:4317
tls:
insecure: true
otlp/tempo:
endpoint: tempo:4317
tls:
insecure: true
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
tls:
insecure_skip_verify: false
loki:
endpoint: http://loki:3100/loki/api/v1/push
labels:
resource:
service.name: "service_name"
k8s.namespace.name: "namespace"
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, k8sattributes, filter/health, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp, prometheus, hostmetrics]
processors: [memory_limiter, resourcedetection, k8sattributes, batch]
exporters: [prometheusremotewrite]
logs:
receivers: [otlp]
processors: [memory_limiter, resourcedetection, k8sattributes, batch]
exporters: [loki]
telemetry:
logs:
level: warn
metrics:
address: ":8888" # Collector's own metrics
Sampling Strategies
Head-based sampling (decision at trace start):
✅ Low overhead — decision made immediately
❌ Can't keep traces based on outcome (error discovered later)
Use: High-volume services where you can afford to drop non-errors
Types:
Always-on: sample every trace (dev/low-volume)
TraceID-ratio: sample X% deterministically by trace ID
Parent-based: inherit parent's sampling decision (distributed default)
Tail-based sampling (decision after trace complete):
✅ Can keep 100% of errors, slow traces, etc.
❌ Requires buffering all spans in collector (memory cost)
❌ Complex multi-collector setup (all spans for a trace must reach same collector)
Use: When error traces must be kept; when you have budget for collector memory
Recommended production setup:
SDK: ParentBased(root=TraceIdRatioBased(0.5)) ← 50% at SDK level
Collector: tail_sampling to further keep errors ← 100% of errors within the 50%
from opentelemetry.sdk.trace.sampling import (
ParentBased, TraceIdRatioBased, ALWAYS_ON, ALWAYS_OFF
)
# Production: 50% sampling, always keep if parent sampled
sampler = ParentBased(
root=TraceIdRatioBased(0.5),
remote_parent_sampled=ALWAYS_ON, # Always sample if parent did
remote_parent_not_sampled=ALWAYS_OFF,
)
tracer_provider = TracerProvider(sampler=sampler, resource=resource)
Exemplars: Linking Metrics to Traces
Exemplars are specific trace IDs embedded in metric data points, allowing you to jump from a latency
spike on a Grafana panel directly to the offending trace in Tempo/Jaeger.
from opentelemetry.metrics import MeterProvider
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import ConsoleMetricExporter
# Exemplars are enabled by default when there's an active span
# The SDK automatically attaches trace_id + span_id to histogram data points
meter = metrics.get_meter("order-service")
latency_histogram = meter.create_histogram(
name="order.processing.duration",
description="Time to process an order",
unit="ms",
)
async def process_order(order_id: str):
start = time.monotonic()
with tracer.start_as_current_span("order.process") as span:
result = await _do_process(order_id)
duration_ms = (time.monotonic() - start) * 1000
# Exemplar is auto-attached from current span context
latency_histogram.record(duration_ms, attributes={"order.type": result.type})
return result
Resource Attributes
from opentelemetry.sdk.resources import Resource, OTELResourceDetector
from opentelemetry.semconv.resource import ResourceAttributes as RA
resource = Resource.create({
# Required: service identification
RA.SERVICE_NAME: "order-api",
RA.SERVICE_VERSION: os.environ.get("APP_VERSION", "unknown"),
RA.SERVICE_NAMESPACE: "ecommerce",
RA.SERVICE_INSTANCE_ID: socket.gethostname(), # Pod name in k8s
# Deployment context
RA.DEPLOYMENT_ENVIRONMENT: os.environ.get("ENVIRONMENT", "development"),
# Cloud provider (auto-detected by resourcedetection processor, but can hardcode)
RA.CLOUD_PROVIDER: "gcp",
RA.CLOUD_REGION: "us-central1",
# Container info (auto-detected in k8s)
RA.CONTAINER_NAME: os.environ.get("HOSTNAME", "unknown"),
})
Anti-Patterns
❌ Putting user data in span names — span names become high-cardinality metric dimensions
❌ Setting span status to ERROR for business errors — only set ERROR for unexpected failures (exceptions)
❌ Using ALWAYS_ON sampling in high-volume production — will crush your tracing backend
❌ Missing service.name resource attribute — backends won't know which service the data came from
❌ OTel Collector without memory_limiter processor — collector will OOM under load
❌ Ignoring semantic conventions — db.system=postgres instead of db.system=postgresql breaks dashboards
❌ Recording secrets/PII in span attributes — spans are often stored unencrypted in backends
❌ tail_sampling on collector without enough memory — all unsampled spans are buffered in RAM
Quick Reference
Signal types:
Traces → SpanKind: SERVER, CLIENT, INTERNAL, PRODUCER, CONSUMER
Metrics → Counter, UpDownCounter, Histogram, ObservableGauge
Logs → SeverityText, SeverityNumber, Body, Attributes
OTel Collector ports:
4317 → OTLP gRPC
4318 → OTLP HTTP
8888 → Collector metrics (Prometheus)
55679 → zpages (debug UI)
13133 → health_check extension
OTLP environment variables (SDK auto-reads):
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
OTEL_SERVICE_NAME=my-service
OTEL_TRACES_SAMPLER=parentbased_traceidratio
OTEL_TRACES_SAMPLER_ARG=0.1
OTEL_RESOURCE_ATTRIBUTES=deployment.environment=production
Collector debug tips:
# Enable zpages extension to see trace/span summaries
extensions:
zpages:
endpoint: 0.0.0.0:55679
# Visit: http://collector:55679/debug/tracezSkill Information
- Source
- MoltbotDen
- Category
- DevOps & Cloud
- Repository
- View on GitHub
Related Skills
kubernetes-expert
Deploy, scale, and operate production Kubernetes clusters. Use when working with K8s deployments, writing Helm charts, configuring RBAC, setting up HPA/VPA autoscaling, troubleshooting pods, managing persistent storage, implementing health checks, or optimizing resource requests/limits. Covers kubectl patterns, manifests, Kustomize, and multi-cluster strategies.
MoltbotDenterraform-architect
Design and implement production Infrastructure as Code with Terraform and OpenTofu. Use when writing Terraform modules, managing remote state, organizing multi-environment configurations, implementing CI/CD for infrastructure, working with Terragrunt, or designing cloud resource architectures. Covers AWS, GCP, Azure providers with security and DRY patterns.
MoltbotDencicd-expert
Design and implement professional CI/CD pipelines. Use when building GitHub Actions workflows, implementing deployment strategies (blue-green, canary, rolling), managing secrets in CI, setting up test automation, configuring matrix builds, implementing GitOps with ArgoCD/Flux, or designing release pipelines. Covers GitHub Actions, GitLab CI, and cloud-native deployment patterns.
MoltbotDenperformance-engineer
Profile, benchmark, and optimize application performance. Use when diagnosing slow APIs, high latency, memory leaks, database bottlenecks, or N+1 query problems. Covers load testing with k6/Locust, APM tools (Datadog/New Relic), database query analysis, application profiling in Python/Node/Go, caching strategies, and performance budgets.
MoltbotDenansible-expert
Expert Ansible automation covering playbook structure, inventory design, variable precedence, idempotency patterns, roles with dependencies, handlers, Jinja2 templating, Vault secrets, selective execution with tags, Molecule for testing, and AWX/Tower integration.
MoltbotDen