Build a production observability stack. Covers OpenTelemetry instrumentation, Prometheus metrics, distributed tracing, log aggregation, and alerting strategies.
This reference explains how to implement observability across traces, metrics, and logs in systems that serve real users at scale. It focuses on concrete mechanisms, not abstract concepts, and covers the specific configuration, data flow, and failure modes you must understand when deploying observability tools. This matters when systems begin to serve traffic beyond local development, especially if they are distributed or stateful.
Trace propagation requires a consistent header format across services. Use traceparent as the standard header name, following the W3C Trace Context specification.
Example trace context header:
traceparent: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
In Go, enable trace propagation with go.opentelemetry.io/otel/propagation:
import "go.opentelemetry.io/otel/propagation"
var propagator = propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
Traces are expensive to store. Implement sampling in your tracing agent or library.
Example configuration for OpenTelemetry Collector:
receivers:
otlp:
protocols:
grpc:
exporters:
jaeger:
endpoint: "jaeger-collector:14250"
processors:
batch:
memory_limiter:
limit_mib: 100
spike_limit_mib: 50
check_interval: 1s
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [jaeger]
Trace IDs are not globally unique across services. They may be generated locally and must be correlated by a shared root ID.
Failure mode: A service logs trace_id=abc123 while another service logs trace_id=abc123 for different requests, causing false correlation.
Metrics are often exposed at /metrics and scraped by Prometheus or similar systems.
Example in Python using prometheus_client:
from prometheus_client import Counter, Histogram, start_http_server
request_count = Counter('http_requests_total', 'Total HTTP requests', ['method', 'endpoint'])
request_duration = Histogram('http_request_duration_seconds', 'HTTP request duration')
@app.route('/api/users')
def get_users():
with request_duration.time():
# Your logic here
return jsonify({"users": []})
Choose between counters, gauges, histograms, and summaries based on use case.
Counter for cumulative counts:
rate(http_requests_total[5m])
Gauge for instantaneous values:
go_memstats_alloc_bytes
Histogram for request duration distribution:
histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))
Too many unique label combinations cause performance degradation and storage bloat.
Example of problematic metrics:
http_requests_total{user_id="12345", session_id="abcde", request_id="xyz"}
Each unique combination creates a new time series. Avoid labels with high cardinality, such as user IDs or UUIDs, unless strictly necessary.
Use structured logging with consistent keys. JSON is the preferred format for machine processing.
Example log line:
{
"timestamp": "2023-10-10T12:34:56Z",
"level": "error",
"message": "Database connection failed",
"service": "user-service",
"trace_id": "0af7651916cd43dd8448eb211c80319c",
"span_id": "b7ad6b7169203331"
}
For containerized environments, log to stdout/stderr rather than files.
Example Dockerfile:
FROM alpine:latest
COPY app /app
CMD ["/app"]
The container runtime captures stdout and stderr automatically. Logging to files inside containers is not scalable.
Set up log rotation on the host or in the container environment.
Example systemd service configuration:
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
Logs are rotated by systemd-journald by default, using configured limits.
Use trace IDs to correlate logs, metrics, and traces in a single request flow.
Example: When a request fails, the error log entry includes trace_id so a developer can search for all related spans and metrics from that trace.
Failure mode: A system has separate trace ID generation and does not propagate it through the entire call chain. Logs may contain unrelated trace IDs, making correlation impossible.
Use tools like Fluentd or Vector to collect logs from services and ship them to a central store.
Example Vector configuration:
[sources.in]
type = "stdin"
[transforms.add_metadata]
type = "remap"
inputs = ["in"]
source = '''
.timestamp = now()
.service = "user-service"
'''
[sinks.out]
type = "elasticsearch"
inputs = ["add_metadata"]
endpoint = "http://elasticsearch:9200"
index = "logs-{date}"
Logs should be concise and include only essential information.
Failure mode: Logging full request bodies or responses for every API call causes disk space issues and log processing overhead.
Configure observability components using environment variables.
Example:
export OTEL_SERVICE_NAME="user-service"
export OTEL_EXPORTER_OTLP_ENDPOINT="http://otel-collector:4317"
export OTEL_TRACES_SAMPLER="parentbased_traceidratio"
Deploy tracing, metrics, and logging agents as independent services.
Example Kubernetes deployment for OpenTelemetry Collector:
apiVersion: apps/v1
kind: Deployment
metadata:
name: otel-collector
spec:
replicas: 1
selector:
matchLabels:
app: otel-collector
template:
metadata:
labels:
app: otel-collector
spec:
containers:
- name: collector
image: otel/opentelemetry-collector:0.87.0
args: ["--config=/etc/otel-collector-config.yaml"]
Ensure that observability components expose liveness and readiness endpoints.
Example health check in Go:
http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
Check if the tracing agent is receiving data. A missing traceparent header or misconfigured exporter can silently drop traces.
Prometheus requires a /metrics endpoint that returns valid Prometheus format. If it returns HTML or an error, scraping fails.
Example of a broken metrics endpoint:
curl http://localhost:8080/metrics
# Returns HTML instead of text/plain
Log parsing in tools like Elasticsearch or Loki depends on consistent log formats and patterns. A change in structure breaks existing queries.
If memory or CPU limits are too tight, tracing libraries may drop spans silently. Monitor otel_exporter_otlp_traces_dropped_spans metrics.
A label like user_id with 10 million unique values can cause Prometheus to use excessive memory. Use histogram_quantile with appropriate buckets to avoid this.
If logs do not include trace context, debugging becomes impossible. Verify that every log line includes the current trace and span IDs.
Set up alerts on metrics like:
rate(otel_exporter_otlp_traces_dropped_spans[5m]) > 0
This detects when spans are being dropped due to overload or misconfiguration.
Watch memory and CPU usage of observability agents.
Example alert:
rate(container_cpu_usage_seconds_total{container="otel-collector"}[5m]) > 0.8
Observability requires coordination between tracing, metrics, and logs. Use consistent headers for trace propagation, structured logging, and appropriate metric types. Avoid high-cardinality labels, ensure components are properly configured, and monitor the observability stack itself. The key is not just collecting data, but making it usable in production.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy