OpenTelemetry Observability in Practice: 6 Core Components for Unified Microservice Monitoring

DevOps

Microservices' Darkest Hour: When Troubleshooting Is Pure Guesswork

2 AM, alerts are flashing wildly. Users report order failures, but logs are scattered across 12 services' Elasticsearch indices. Trace IDs break between services and can't be linked together. Key metrics are missing, making it impossible to pinpoint bottlenecks. The team spent 3 hours digging through logs before discovering the payment service's database connection pool was exhausted — a conclusion that should have taken 5 minutes.

This isn't an isolated case. Scattered logs, broken traces, missing metrics, lost context, and alert fatigue — these are the five pain points of microservice observability. When a system is split from a monolith into dozens of microservices, traditional log-based debugging completely fails. OpenTelemetry, as the CNCF observability standard, provides a unified collection, processing, and export solution. This article covers 6 core components to help you build a complete microservice observability system.


Core Concepts Reference

Concept Description Core Role
Observability Ability to infer internal system state from external outputs Evolve from "monitoring known issues" to "exploring unknown ones"
OpenTelemetry (OTel) CNCF observability framework, unified Traces/Metrics/Logs collection Vendor-neutral APIs, SDKs, and tooling
Distributed Tracing Cross-service request call chain tracking Locate cross-service latency bottlenecks and root causes
Metrics Quantified measurement data of the system Performance monitoring, capacity planning, trend analysis
Logs Discrete event records Error investigation, auditing, context enrichment
OTel Collector Data processing hub: receive → process → export Decouple collection from backends, unified data processing
Context Propagation Propagating TraceID and other context across services W3C Trace Context standard, linking call chains

Problem Analysis: 5 Challenges of Microservice Observability

Challenge 1: Data Silos. Traces in Jaeger, Metrics in Prometheus, Logs in Elasticsearch — three types of data exist in isolation, impossible to correlate. A single request's Trace, Metric, and Log are scattered across three systems, requiring manual switching during troubleshooting.

Challenge 2: Context Breakage. Lack of unified TraceID propagation between services. Context information in HTTP headers, message queues, and gRPC metadata is lost, causing broken call chains. Async messaging scenarios (like Kafka) make context propagation even harder.

Challenge 3: Sampling Strategy. Full Trace collection generates massive data volumes. A service with 100K QPS produces billions of Spans daily. Too aggressive sampling loses critical error information; too conservative sampling makes storage costs unsustainable.

Challenge 4: Storage Costs. Traces, Metrics, and Logs generate TB-level storage daily. Elasticsearch cluster costs are high, long-term retention policies conflict with compliance requirements, and hot-cold data separation is complex.

Challenge 5: Alert Fatigue. Complex microservice dependencies mean one underlying failure triggers alerts from dozens of upstream services. Hundreds of alerts daily, but fewer than 5 require actual action, leading to team desensitization.


Component 1: Distributed Tracing Integration

Distributed tracing is the core of observability. OpenTelemetry collects Trace data through both auto-instrumentation and manual instrumentation.

Go Microservice Auto-Instrumentation

package main

import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

func initTracer() (*sdktrace.TracerProvider, error) {
    exporter, err := otlptracegrpc.New(context.Background(),
        otlptracegrpc.WithEndpoint("otel-collector:4317"),
        otlptracegrpc.WithInsecure(),
    )
    if err != nil {
        return nil, err
    }

    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(resource.NewWithAttributes(
            semconv.SchemaURL,
            semconv.ServiceNameKey.String("order-service"),
            semconv.ServiceVersionKey.String("1.0.0"),
        )),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
}

otlptracegrpc.New creates a gRPC exporter connecting to the OTel Collector on port 4317. sdktrace.WithBatcher enables batch export to reduce network overhead. resource.NewWithAttributes sets the service name and version, identifying call chain nodes in Jaeger. semconv uses OpenTelemetry semantic conventions to ensure attribute naming follows standards.

HTTP Middleware Auto-Tracing

import (
    "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
    "net/http"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/orders", handleOrders)

    wrappedHandler := otelhttp.NewHandler(mux, "order-service",
        otelhttp.WithMessageEvents(otelhttp.Read, otelhttp.Write),
    )

    http.ListenAndServe(":8080", wrappedHandler)
}

otelhttp.NewHandler wraps the HTTP Handler, automatically creating a Span for each request and recording HTTP method, status code, latency, etc. WithMessageEvents records request and response body read/write events for troubleshooting data transfer issues.


Component 2: Metrics Collection with Prometheus

OpenTelemetry Metrics provides Counter, Histogram, and Gauge instruments with seamless Prometheus integration.

OTel Metrics Export

import (
    "go.opentelemetry.io/otel/exporters/prometheus"
    "go.opentelemetry.io/otel/metric"
    sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)

func initMeter() (metric.MeterProvider, error) {
    exporter, err := prometheus.New()
    if err != nil {
        return nil, err
    }

    mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(exporter))
    otel.SetMeterProvider(mp)
    return mp, nil
}

func registerMetrics(mp metric.MeterProvider) {
    meter := mp.Meter("order-service")

    requestCounter, _ := meter.Int64Counter(
        "order.requests.total",
        metric.WithDescription("Total order requests"),
    )

    latencyHistogram, _ := meter.Float64Histogram(
        "order.request.duration",
        metric.WithDescription("Order request duration"),
        metric.WithUnit("ms"),
    )

    activeOrders, _ := meter.Int64ObservableGauge(
        "order.active.count",
        metric.WithDescription("Active order count"),
    )

    _ = activeOrders
    _ = requestCounter
    _ = latencyHistogram
}

prometheus.New() creates a Prometheus exporter, automatically exposing OTel metrics at the :8889/metrics endpoint. Int64Counter is for request counts, monotonically increasing. Float64Histogram is for latency distribution — Prometheus can compute P99 via histogram_quantile. Int64ObservableGauge is for current instantaneous values (like active connections) obtained via callbacks.

Business Metrics Instrumentation

func processOrder(ctx context.Context, req *OrderRequest) {
    startTime := time.Now()

    requestCounter.Add(ctx, 1,
        attribute.String("order.type", req.Type),
        attribute.String("order.channel", req.Channel),
    )

    result, err := doProcessOrder(ctx, req)

    duration := float64(time.Since(startTime).Milliseconds())
    latencyHistogram.Record(ctx, duration,
        attribute.String("order.type", req.Type),
        attribute.Bool("order.success", err == nil),
    )
}

Each metric carries business dimension labels (order.type, order.channel), enabling Prometheus aggregation by label. Latency metrics distinguish success and failure, helping detect anomalies like "successful requests are fast but failed ones are slow."


Component 3: Logs Correlation with Trace ID

Logs correlation is the key to connecting Traces, Metrics, and Logs. By injecting TraceID and SpanID into logs, you can jump from a Trace to related Logs with one click.

Structured Logging with TraceID Injection

import (
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/log"
    "go.opentelemetry.io/otel/log/global"
)

func setupLogger() {
    logger := global.LoggerProvider().Logger("order-service")

    span := otel.Tracer("order-service").SpanFromContext(context.Background())
    spanCtx := span.SpanContext()

    logger.Emit(context.Background(), log.Record{
        Body: log.StringValue("Order created successfully"),
        Attributes: []log.KeyValue{
            log.String("trace_id", spanCtx.TraceID().String()),
            log.String("span_id", spanCtx.SpanID().String()),
            log.String("order.id", "ORD-20260618-001"),
            log.Int("order.items", 3),
            log.Float64("order.amount", 299.99),
        },
        Severity: log.SeverityInfo,
    })
}

Zap Logger Integration with OTel

import (
    "go.opentelemetry.io/contrib/bridges/otelzap"
    "go.uber.org/zap"
)

func initZapLogger() *zap.Logger {
    core := otelzap.NewCore("order-service",
        otelzap.WithLoggerProvider(global.GetLoggerProvider()),
    )

    zapCore := zapcore.NewTee(
        zapcore.NewCore(
            zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
            zapcore.AddSync(os.Stdout),
            zapcore.DebugLevel,
        ),
        core,
    )

    return zap.New(zapCore)
}

otelzap.NewCore bridges Zap logs to the OpenTelemetry Logs pipeline, automatically carrying TraceID and SpanID. zapcore.NewTee outputs to both stdout and the OTel pipeline, ensuring both local debugging and remote collection work. The JSON encoder outputs structured logs that Elasticsearch can index directly.

Correlating Logs with Traces

In Kibana/Grafana Loki, query by TraceID for correlation:

# Find the slow request's TraceID in Jaeger
TraceID: 4bf92f3577b34da6a3ce929d0e0e4736

# Query all logs for this Trace in Elasticsearch
GET otel-logs/_search
{
  "query": {
    "term": {
      "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
    }
  },
  "sort": [{ "timestamp": "asc" }]
}

From Jaeger's Trace detail page, click "View Logs" to automatically jump to Elasticsearch and query all logs for that TraceID, achieving seamless Trace-Log correlation.


Component 4: OTel Collector Deployment

The OTel Collector is the data processing hub, supporting receiving, processing, and exporting all three signals — the core of the entire observability architecture.

Collector Pipeline Configuration

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318

processors:
  batch:
    timeout: 5s
    send_batch_size: 1024
  tail_sampling:
    decision_wait: 10s
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes:
            - ERROR
      - name: slow
        type: latency
        latency:
          threshold_ms: 1000

exporters:
  prometheus:
    endpoint: "0.0.0.0:8889"
  otlp/jaeger:
    endpoint: jaeger:4317
    tls:
      insecure: true
  elasticsearch:
    endpoints:
      - http://elasticsearch:9200
    logs_index: otel-logs

service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch, tail_sampling]
      exporters: [otlp/jaeger]
    metrics:
      receivers: [otlp]
      processors: [batch]
      exporters: [prometheus]
    logs:
      receivers: [otlp]
      processors: [batch]
      exporters: [elasticsearch]

Three pipelines process Traces, Metrics, and Logs separately. The batch processor sends data in batches to reduce network requests. tail_sampling ensures errors and slow requests are 100% collected. The prometheus exporter exposes Metrics on port 8889 for Prometheus scraping. The elasticsearch exporter writes Logs to ES with index name otel-logs.

Kubernetes DaemonSet Deployment

apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: otel-collector
  namespace: observability
spec:
  selector:
    matchLabels:
      app: otel-collector
  template:
    metadata:
      labels:
        app: otel-collector
    spec:
      containers:
        - name: collector
          image: otel/opentelemetry-collector-contrib:0.96.0
          ports:
            - containerPort: 4317
            - containerPort: 4318
            - containerPort: 8889
          volumeMounts:
            - name: config
              mountPath: /etc/otelcol
          resources:
            limits:
              memory: "512Mi"
              cpu: "500m"
      volumes:
        - name: config
          configMap:
            name: otel-collector-config

DaemonSet mode deploys one Collector instance per Node. Applications send data via localhost, reducing network hops. Resource limits of 512Mi memory and 500m CPU prevent the Collector from consuming too many resources and affecting business Pods.


Component 5: Sampling Strategy and Cost Control

Sampling strategy is key to balancing observability and cost. Head-based sampling is simple and efficient but may lose critical information. Tail-based sampling ensures 100% collection of errors but requires waiting for the Trace to complete.

Tail-Based Sampling Configuration

processors:
  tail_sampling:
    decision_wait: 10s
    num_traces: 100000
    expected_new_traces_per_sec: 100
    policies:
      - name: errors
        type: status_code
        status_code:
          status_codes:
            - ERROR
      - name: slow-requests
        type: latency
        latency:
          threshold_ms: 1000
      - name: critical-service
        type: string_attribute
        string_attribute:
          key: service.name
          values:
            - payment-service
            - order-service
      - name: health-check-exclude
        type: and
        and:
          sub_policies:
            - name: not-health
              type: string_attribute
              string_attribute:
                key: http.route
                values:
                  - /health
                  - /ready
                invert_match: true
      - name: fallback
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

Five sampling policies evaluated top to bottom: errors 100% → slow requests 100% → critical services 100% → exclude health checks → remaining 10% probability. decision_wait: 10s waits 10 seconds to collect the complete Trace before deciding, ensuring both root and child Spans are evaluated. num_traces: 100000 limits the number of Traces cached in memory to prevent OOM.

Cost Estimation

Sampling Strategy Data Volume Monthly Storage Cost Notes
Full collection 100% $7,000 100K QPS, 1B Spans/day
Head-based 10% 10% $700 May lose critical errors
Tail-based (errors+slow+10%) ~15% $1,050 Recommended: ensures key info visible
Custom (errors+critical+5%) ~8% $560 Fine-grained control for large scale

Component 6: Alerting and SLO Monitoring

SLO (Service Level Objective) is a quantified target for measuring service reliability. Combined with OpenTelemetry metrics and Prometheus alerting rules, it enables SLO-based intelligent alerting.

SLO Definition and Alerting Rules

groups:
  - name: order-service-slo
    rules:
      - record: order:slo:availability:ratio
        expr: |
          sum(rate(http_server_request_duration_seconds_count{service="order-service",status_code!~"5.."}[5m]))
          /
          sum(rate(http_server_request_duration_seconds_count{service="order-service"}[5m]))

      - record: order:slo:latency:p99
        expr: |
          histogram_quantile(0.99,
            sum(rate(http_server_request_duration_seconds_bucket{service="order-service"}[5m]))
            by (le)
          )

      - alert: OrderServiceSLOAvailabilityBurnRate
        expr: |
          (
            1 - order:slo:availability:ratio
          ) > (1 - 0.999) * 14.4
        for: 5m
        labels:
          severity: critical
          team: order
        annotations:
          summary: "Order service availability SLO burn rate too high"
          description: "Availability below 99.9% for the past 5 minutes, burn rate exceeds 14.4x, 30-day SLO budget will be exhausted in 2 days"

      - alert: OrderServiceSLOLatencyBurnRate
        expr: |
          order:slo:latency:p99 > 0.5
        for: 5m
        labels:
          severity: warning
          team: order
        annotations:
          summary: "Order service P99 latency exceeds 500ms SLO"
          description: "Current P99 latency is {{ $value }}s, exceeding SLO threshold of 0.5s"

order:slo:availability:ratio calculates the availability ratio (non-5xx request percentage). Burn Rate is the ratio of SLO consumption speed to budget — 14.4x means the 30-day SLO budget will be exhausted in 2 days. for: 5m avoids transient spikes triggering alerts; sustained violations for 5 minutes are required.

Multi-Level Alerting Strategy

Level Condition Notification Response Time
P0 Critical Availability <99%, sustained 5min Phone+SMS+Slack Within 5 minutes
P1 Warning P99 latency >1s, sustained 5min SMS+Slack Within 15 minutes
P2 Info Error rate >0.1%, sustained 15min Slack Within 1 hour
P3 Low Resource usage >80%, sustained 30min Email Next business day

Pitfall Guide: 5 Common Traps

❌ Trap 1: Only collecting Traces, ignoring Metrics and Logs ✅ All three observability pillars are essential. Traces identify "where it's slow," Metrics quantify "how slow," and Logs explain "why it's slow." Only correlating all three enables complete troubleshooting.

❌ Trap 2: Single-point Collector deployment ✅ Production environments should deploy at least 2 Collector instances (DaemonSet or Gateway mode) to prevent Collector failure from causing total data loss.

❌ Trap 3: Full sampling without cost control ✅ Use tail-based sampling: errors and slow requests at 100%, normal requests sampled proportionally. Otherwise storage costs scale linearly with traffic.

❌ Trap 4: Not injecting TraceID into logs ✅ Every log entry must include trace_id and span_id fields. Without them, logs can't be correlated with Traces, requiring manual time-window searches.

❌ Trap 5: Overly sensitive alerting rules ✅ Use burn rate instead of simple threshold alerts. Set for duration to avoid transient spikes. Multi-level alerting prevents P0 alert floods.


Error Troubleshooting: 10 Common Errors

Error Symptom Possible Cause Diagnostic Command Solution
Traces break between services Context propagation not configured Check if HTTP headers carry traceparent Enable otelhttp middleware for auto-propagation
Collector connection refused gRPC port not open or wrong address telnet collector 4317 Check Collector address and port config
Metrics not appearing in Prometheus Exporter port not exposed curl localhost:8889/metrics Check prometheus exporter config and port mapping
Logs missing TraceID Logger not integrated with OTel Check if log output contains trace_id field Use otelzap to bridge Zap to OTel
Tail sampling OOM num_traces set too large Check Collector memory usage Reduce num_traces or increase memory limit
High Span data latency Batch processor timeout too long Check Collector processing latency Reduce timeout to 1-2 seconds
Elasticsearch write failure Index template not created or insufficient permissions Check ES errors in Collector logs Create index template and configure correct credentials
Kafka messages lose Trace context Producer/Consumer not configured with propagator Check if message headers contain traceparent Use OTel Kafka auto-instrumentation
Alert storm Rules too sensitive, missing for duration Check Alertmanager silence rules Add burn rate alerts and duration filtering
Collector CPU usage too high Data volume exceeds processing capacity Check Collector's own Metrics Add instances or enable filter processor to remove useless data

Advanced Optimization Tips

1. Frontend RUM Integration. Use @opentelemetry/sdk-trace-web to collect frontend performance data. Fetch/XHR requests automatically carry traceparent headers, enabling end-to-end tracing from browser to backend.

2. Multi-Cluster Collector Federation. Deploy DaemonSet Collectors per cluster with a Gateway Collector aggregating at the upper layer for unified multi-cluster observability. The Gateway layer handles global sampling and routing.

3. Exemplar-Based Metrics-Traces Correlation. Prometheus Exemplars link Metric data points to specific Traces. In Grafana, click on a Metrics chart to jump directly to Jaeger and view the corresponding Trace.

4. Custom Processor Extensions. Use the Collector's transform processor to add business attributes (like environment, team) and the filter processor to remove useless Spans like health checks, reducing storage overhead.

5. SLO Burn Rate Multi-Window Alerting. Calculate burn rates for both 1-hour and 5-minute windows simultaneously. Short windows detect sudden failures; long windows detect chronic degradation, avoiding false positives and missed alerts from a single window.


Comparison: OpenTelemetry vs Jaeger vs Zipkin vs SkyWalking

Feature OpenTelemetry Jaeger Zipkin SkyWalking
Positioning Collection standard + SDK Tracing backend storage Tracing backend storage Full-stack APM solution
Signal Types Traces+Metrics+Logs Traces Traces Traces+Metrics+Logs
Vendor Neutral ✅ CNCF standard ❌ Tied to Jaeger storage ❌ Tied to Zipkin storage ❌ Tied to SkyWalking
Auto-Instrumentation Multi-language SDK + Java Agent Java Agent Limited Java Agent
Collector OTel Collector (feature-rich) Jaeger Collector No standalone Collector OAP Server
Sampling Strategy Head + Tail sampling Head + Adaptive sampling Head sampling Head sampling
Metrics Integration Native support Requires Prometheus Not supported Native support
Logs Correlation Native support Requires ELK Not supported Native support
Alerting Requires Prometheus Requires Alertmanager Not supported Built-in
Community Activity ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
Production Readiness ⭐⭐⭐⭐⭐ ⭐⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐

  • JSON Formatter — Format OTel Collector configs and Kubernetes YAML, quickly troubleshoot Pipeline definition issues
  • Hash Calculator — Calculate TraceID and SpanID hashes, verify context propagation correctness
  • cURL to Code Converter — Convert OTLP API debug commands to Go/Python code, accelerate Collector integration development

Summary and Outlook

The core of microservice observability isn't tool stacking, but the implementation of three principles: unified Traces/Metrics/Logs signal collection, TraceID as the linking thread throughout the chain, and precise sampling for cost control. The 6 core components — distributed tracing integration, metrics collection with Prometheus, logs correlation with TraceID, OTel Collector deployment, sampling strategy and cost control, alerting and SLO monitoring — cover the complete pipeline from data collection to processing/export to intelligent alerting. Remember: all three signals are indispensable, TraceID is the correlation bond, sampling is the cost valve — only then can you build a truly effective microservice observability system. In 2026, with OpenTelemetry Logs signal officially stable, the three pillars are truly unified, and observability enters a new era of "one SDK solves everything."


Further Reading

Try these browser-local tools — no sign-up required →

#OpenTelemetry可观测性#可观测性三支柱#OpenTelemetry部署#分布式追踪#指标监控#2026#日志关联