OpenTelemetry works with three main telemetry signals. A trace follows causally related operations, a metric records measurements over time, and a log records an event. Shared resource attributes and trace identifiers can help a backend correlate them.
| Field | Type | Description |
|---|---|---|
name | string | Human-readable operation name |
trace_id | 16-byte ID | Globally unique trace identifier |
span_id | 8-byte ID | Unique span within the trace |
parent_span_id | 8-byte ID | Parent span ID (empty for root) |
start_time / end_time | Timestamp | Wall-clock start and end |
kind | SpanKind | server, client, producer, consumer, internal |
status | Status | unset, ok, error |
attributes | Map[string, Value] | Key-value pairs describing the span |
events | []SpanEvent | Timestamped log messages during the span |
links | []SpanLink | Links to other spans (potentially from other traces) |
| Kind | Meaning | Visual |
|---|---|---|
server | Incoming request handler | ←—— arrow in |
client | Outgoing request to a dependency | ——→ arrow out |
producer | Message sent to queue (no immediate response) | ——↗ arrow to queue |
consumer | Message received from queue | ↘—— arrow from queue |
internal | Internal operation (default) | no arrow |
| Instrument | Sync/Async | Use |
|---|---|---|
| Counter | Sync | Additive values (requests served, bytes sent) |
| UpDownCounter | Sync | Non-additive (active connections, queue depth) |
| Histogram | Sync | Distribution of values (request latencies, payload sizes) |
| ObservableCounter | Async (callback) | System metrics from APIs (CPU usage) |
| ObservableUpDownCounter | Async | Gauge-like additive metrics |
| ObservableGauge | Async | Point-in-time values (temperature, queue depth) |
| Field | Description |
|---|---|
timestamp | When the event occurred |
severity | Log level: TRACE(5), DEBUG(10), INFO(20), WARN(30), ERROR(40) |
body | Log message |
resource | Attributes of the emitting entity (service.name, etc.) |
attributes | Structured key-value pairs |
trace_id, span_id | If emitted within a traced context (correlation key) |
The OpenTelemetry Collector runs telemetry pipelines outside your application. Receivers accept supported protocols, processors transform or batch the data, and exporters send it to one or more destinations.
Prometheus · Kafka · filelog
filter · tail_sampling · k8sattributes
Loki · Datadog · AWS X-Ray
| Receiver | Protocol | Signal |
|---|---|---|
otlp | gRPC / HTTP | traces, metrics, logs |
jaeger | Thrift / gRPC | traces |
zipkin | HTTP | traces |
prometheus | HTTP pull | metrics |
prometheusremotewrite | HTTP remote write | metrics |
hostmetrics | System calls | metrics |
kafka | Kafka | traces, metrics, logs |
filelog | File tail | logs |
syslog | Syslog | logs |
| Processor | Function |
|---|---|
batch | Batches spans/metrics/logs to reduce export calls |
memory_limiter | Rejects data when memory is high (OOM protection) |
transform | Modify attributes using OTTL (OTel Transformation Language) |
filter | Filter spans/metrics/logs by criteria |
resource | Add/modify resource attributes |
attributes | Add/modify span/log attributes |
probabilistic_sampler | Sample X% of traces |
tail_sampling | Sample based on policies (error, latency, SLO) |
routing | Route to different exporters based on criteria |
k8sattributes | Inject Kubernetes metadata (pod, namespace, etc.) |
| Exporter | Backend |
|---|---|
otlp | Any OTel-native backend |
otlphttp | Any backend via HTTP |
jaeger | Jaeger |
zipkin | Zipkin |
prometheus | Prometheus (pull or remote_write) |
loki | Grafana Loki (logs) |
datadog | Datadog |
awsxray | AWS X-Ray |
awsemf | AWS CloudWatch EMF (metrics) |
logging | Stdout (debug) |
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
memory_limiter:
limit_mib: 512
check_interval: 1s
exporters:
otlp:
endpoint: http://tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch, memory_limiter]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [batch, memory_limiter]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch, memory_limiter]
exporters: [otlp]processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
expected_new_traces_per_sec: 100
policies:
- name: errors-policy
type: status_code
status_code: {status_codes: [ERROR]}
- name: slow-traces-policy
type: latency
latency: {threshold_ms: 1000}
- name: probabilistic-policy
type: probabilistic
probabilistic: {sampling_percentage: 10}
- name: latency-slo-policy
type: and
and: {and_policy_requirements:
- policy: latency
latency: {threshold_ms: 100}
- policy: status_code
status_code: {status_codes: [OK]}
}connectors:
spanmetrics:
metrics_exporter: prometheus
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp, spanmetrics]
metrics:
receivers: [spanmetrics]
exporters: [prometheus]When one service calls another, the trace must cross the process boundary with the request. A propagator injects trace context into an HTTP header, message metadata, or another carrier; the receiver extracts it and uses it as the parent context for new spans.
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
│ │ │ │
│ trace_id (32 hex chars) │ │ │
│ │ │ └── flags (2 hex chars)
│ │ └─────────── span_id (16 hex chars)
│ └────────────── version (2 hex chars)
└────────────────────────────────────────────────── version prefix| Field | Length | Description |
|---|---|---|
version | 2 hex | Protocol version (currently 00) |
trace_id | 32 hex | 16-byte global trace ID |
parent_id (span_id) | 16 hex | 8-byte span ID of the parent |
flags | 2 hex | Options (bit 0 = sampled: 01 = sampled, 00 = not) |
tracestate: congo=t61rcWkgMzE,rojo=00f067aa0ba902b7
Format: key=value,key=value (max 32 pairs, 256 chars total). Carries vendor-specific or cross-cutting metadata. Optional — traceparent is mandatory.
The Propagators API performs those two operations: inject the current context into an outgoing carrier, then extract it from the incoming carrier. Both services must agree on a propagation format for the trace to remain connected.
| Propagator | traceparent | tracestate | Baggage | Notes |
|---|---|---|---|---|
TraceContext | W3C standard | W3C standard | No | Default |
Baggage | No | No | W3C standard | Must be combined with TraceContext |
W3C | W3C standard | W3C standard | No | Alias for TraceContext |
B3 | B3 single header | N/A | Via bkvr | Legacy Zipkin format |
AWS X-Ray | AWS format | N/A | No | AWS-specific |
Jaeger | Jaeger headers | N/A | No | Legacy Jaeger format |
import "go.opentelemetry.io/otel/propagation" // Register a composite propagator (trace context + baggage) otel.SetTextMapPropagator(propagation.NewCompositePropagator( propagation.TraceContext{}, // W3C Trace Context propagation.Baggage{}, // W3C Baggage ))
from opentelemetry import propagate from opentelemetry.propagate import set_global_textmap from opentelemetry.sdk.trace.propagation.tracecontext import TraceContextPropagator set_global_textmap(TraceContextPropagator())
// Inject: extract context from span and inject into HTTP headers func makeHTTPRequest(ctx context.Context, url string) (*http.Response, error) { req, _ := http.NewRequest("GET", url, nil) propagator := propagation.TraceContext{} propagator.Inject(ctx, req.Header, propagation.HeaderCarrier(req.Header)) return http.DefaultClient.Do(req) } // Extract: extract trace context from incoming HTTP headers func handleHTTPRequest(w http.ResponseWriter, r *http.Request) { propagator := propagation.TraceContext{} ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) ctx, span := tracer.Start(ctx, "handler") defer span.End() }
Baggage carries key-value context across service boundaries alongside the trace context. A baggage item is not automatically a span attribute; instrumentation must copy it deliberately when that is useful. Because downstream services receive it, avoid secrets and unbounded values.
// Via tracestate (preferred — forwarded by more proxies) tracestate: otel.baggagegage="key1=value1,key2=value2" // Or via dedicated header (less common) baggage: key1=value1, key2=value2
import "go.opentelemetry.io/otel/baggage" // Add baggage b, _ := baggage.NewMember("tenant.id", "acme-corp") m, _ := baggage.NewMember("user.role", "admin") bag, _ := baggage.New(b, m) ctx := baggage.ContextWithBaggage(ctx, bag) // Read baggage anywhere in the trace baggage := baggage.FromContext(ctx) if val, ok := baggage.Member("tenant.id"); ok { span.SetAttributes(attribute.String("tenant.id", val)) }
# Publishing: inject trace context into message headers from opentelemetry.propagate import inject headers = {} inject(headers) # injects traceparent + baggage into headers producer.send("my-topic", value=data, headers=headers) # Consuming: extract context from message and create linked span from opentelemetry.propagate import extract ctx = extract(message.headers) with tracer.start_as_current_span("process-message", context=ctx) as span: # span is linked to the producer span pass
span_id assigned
parent_span_id set
start_time set
events added
child spans created
span recorded
batched → exported
// 1. Create TracerProvider (once at startup) tp := trace.NewTracerProvider( trace.WithResource(resource.New(ctx, resource.WithAttributes( attribute.String("service.name", "order-service"), ), )), trace.WithSampler(trace.AlwaysSample()), ) // 2. Register globally otel.SetTracerProvider(tp) // 3. Get a Tracer tracer := tp.Tracer("order-service") // 4. Start a span ctx, span := tracer.Start(ctx, "handleOrders") defer span.End() // 5. Add attributes (metadata) span.SetAttributes( attribute.String("order.id", orderID), attribute.Float64("order.amount", amount), attribute.String("http.method", "POST"), ) // 6. Add an event (a log point in time) span.AddEvent("order validated") span.AddEvent("invoice response received", trace.WithAttributes( attribute.Int("http.status_code", 201), )) // 7. Mark error if needed span.SetStatus(codes.Error, "failed to call invoice service")
from opentelemetry import trace from opentelemetry.trace import Status, StatusCode # 1. Get the global tracer tracer = trace.get_tracer("invoice-service") # 2. Start a span (context manager auto-ends) with tracer.start_as_current_span("generate_invoice") as span: span.set_attribute("invoice.order_id", str(order_id)) span.set_attribute("invoice.amount", amount) span.add_event("invoice generation started") invoice = generate_invoice(order_id, amount) span.set_status(Status(StatusCode.OK)) # or: span.set_status(Status(StatusCode.ERROR, "reason"))
ctx carries the current trace context. Start a span with it — children automatically link as children.otelhttp.NewClient(). Child span automatically links to parent.otelhttp.NewHandler(). All routes automatically create spans with HTTP attributes.| Sampler | Behavior |
|---|---|
AlwaysOn | Every span recorded (dev) |
AlwaysOff | No spans recorded (perf testing) |
TraceIdRatio | Sample X% of root spans; all children follow |
ParentBased | Child follows parent's sampling decision |
// 10% of traces; if parent was sampled → sample everything sampler := trace.ParentBased( trace.TraceIDRatioBased(0.1), )
| Status | Code | When to use |
|---|---|---|
Unset | 0 | Default — no status set. Treated as Ok. Backends typically don't display. |
Ok | 1 | Span completed successfully. Set explicitly when you want guaranteed visibility. |
Error | 2 | Span ended in failure. Surfaces in error-focused views. |
Ok explicitly only when you need guaranteed status display in backends that filter by status. Otherwise Unset is fine. Always set Error on failures.
OTel defines 6 instruments in 3 categories. Sync instruments: your code calls .Add() or .Record() directly. Async instruments: OTel SDK calls your callback periodically.
// Create at startup counter, err := meter.Int64Counter( "http_requests_total", metric.WithDescription("Total HTTP requests received"), metric.WithUnit("requests"), ) // Record — always Add() with a positive value for counters counter.Add(ctx, 1, metric.WithAttributes( attribute.String("method", "GET"), attribute.String("path", "/orders"), attribute.String("status", "200"), ), )
// Create at startup histogram, err := meter.Float64Histogram( "order_processing_duration_ms", metric.WithDescription("Order processing time in milliseconds"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries( 5.0, 10.0, 25.0, 50.0, 100.0, 250.0, 500.0, 1000.0, 2500.0, 5000.0, 10000.0, ), ) // Record a measurement histogram.Record(ctx, 127.5, metric.WithAttributes( attribute.String("method", "POST"), attribute.String("path", "/orders"), ), )
var currentQueueSize int64 _, err := meter.Int64ObservableGauge( "queue_size", metric.WithDescription("Current number of items in queue"), metric.WithCallback(func(_ context.Context, o metric.Int64Observer) error { o.Observe(currentQueueSize) return nil }), )
# Create at startup duration_histogram = meter.create_histogram( name="order_processing_duration_ms", description="Order processing time in milliseconds", unit="ms", ) # Record duration_histogram.record(127.5, {"method": "POST", "path": "/orders"})
Attributes divide metric measurements into time series. Each distinct attribute set can create another series in the backend, so values such as user IDs or request IDs can produce unbounded cardinality and high storage cost.
attribute.String("request_id", unique_id) creates a new time series for every request. Keep attribute values low cardinality (≤ 100 unique values).
# Good — low cardinality counter.add(1, {"customer_tier": "premium"}) # Bad — high cardinality (one time series per unique value) counter.add(1, {"request_id": "req-abc-123"})
| Temporality | What it means | Use case |
|---|---|---|
| Cumulative (default) | Each export contains all values since app start | General use |
| Delta | Each export is only the delta since last export | Prometheus remote write (reduces cardinality) |
An exemplar attaches a representative trace and span identifier to a metric measurement, such as one observation contributing to a histogram bucket. A compatible backend can use it to open the trace behind an unusual latency value.
span_id=def456
value=447ms
.Record() call runs inside an active trace context. Enable exemplars in your SDK config.
func initMeter(ctx context.Context) (func(), error) { exporter, err := otlpmetricgrpc.New(ctx) if err != nil { return nil, err } res, err := resource.New(ctx, resource.WithAttributes( attribute.String("service.name", "order-service"), ), ) reader := metric.NewPeriodicBatchReader(exporter, metric.WithInterval(10 * time.Second), ) mp := metric.NewMeterProvider( metric.WithResource(res), metric.WithReader(reader), ) otel.SetMeterProvider(mp) return func() { mp.Shutdown(ctx) }, nil }
An OpenTelemetry LogRecord carries a timestamp, observed time, severity, body, resource, and attributes. When code emits the record inside an active span, it can also carry the trace and span identifiers needed for correlation.
timestamp, severity (TRACE/DEBUG/INFO/WARN/ERROR), body, resource, attributes, trace_id, span_idfrom opentelemetry import trace tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("process-order") as span: # Log within this span — trace_id/span_id auto-attached span.add_event("log", attributes={ "message": "order processing started", "order_id": order_id, }) # Span events are logs within a trace context # Use span.add_event() for in-trace logging
import logging from opentelemetry.sdk._logs import LoggingHandler # Bridge Python stdlib logging → OTel SDK otel_handler = LoggingHandler() logging.root.addHandler(otel_handler) # All logging.info(), logging.error() calls now flow through OTel logging.info("payment processed", extra={"order_id": "12345"}) # These carry trace_id/span_id when called within a traced context
import "go.opentelemetry.io/otel/log" // Create a logger (once at startup) logger := mp.Logger("order-service") // Emit a log record within a trace context logger.Info(ctx, "order processed", log.WithAttributes( attribute.String("order.id", orderID), attribute.String("status", "completed"), ), ) // ctx carries trace context — log record gets trace_id + span_id auto
| OTel Severity | Numeric | Typical Mapping |
|---|---|---|
TRACE | 5 | Most verbose — stack traces, full debug dumps |
DEBUG | 10 | Debug info, variable dumps |
INFO | 20 | Normal operations, state transitions |
WARN | 30 | Unexpected but handled (retries, fallbacks) |
ERROR | 40 | Failures requiring attention |
filelog receiver and use resource attributes (service.name, etc.) for correlation.
helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts helm install otel-collector open-telemetry/opentelemetry-collector \ --set mode=daemonset \ --set config.receivers.otlp.protocols.grpc.endpoint=0.0.0.0:4317 \ --set config.receivers.otlp.protocols.http.endpoint=0.0.0.0:4318
helm install otel-collector-gateway open-telemetry/opentelemetry-collector \ --set mode=deployment \ --set config.receivers.otlp.protocols.grpc.endpoint=0.0.0.0:4317 \ --set config.exporters.otlp.endpoint=http://tempo:4317
Use the OpenTelemetry Operator with the Instrumentation CR to auto-inject OTel SDK into pods without modifying application code.
# Install OpenTelemetry Operator kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml # Create Instrumentation resource (per namespace) apiVersion: opentelemetry.io/v1alpha1 kind: Instrumentation metadata: name: my-instrumentation namespace: default spec: exporter: endpoint: http://otel-collector:4317 propagators: - tracecontext - baggage resource: addK8sUID: true addAttributes: true
# Annotate pod to enable auto-instrumentation
apiVersion: v1
kind: Pod
metadata:
annotations:
instrumentation.opentelemetry.io/inject-sdk: "true"
instrumentation.opentelemetry.io/inject-tracer: "opentelemetry-auto"
spec:
containers:
- name: my-app
image: my-app:latestThe k8sattributes processor can associate telemetry with Kubernetes workloads and add selected metadata. Its result depends on the Collector's deployment mode, permissions, association rules, and processor configuration.
processors:
k8sattributes:
extract:
metadata:
- k8s.namespace.name
- k8s.deployment.name
- k8s.pod.name
- k8s.pod.uid
- k8s.pod.start_time
- k8s.container.name
- k8s.container.restart_count
filter:
node: ".*worker.*" # Only pods on worker nodesmemory_limiter config. Target 512–1024Mi for agents, 1–2Gi for gateways under load.ballast extension only if not using GOMEMLIMIT env var (Collector v0.91+).env: with GOMEMLIMIT: 400MiB.