Service Mesh Observability: The Unified Sidecar Playbook for Traces, Metrics, and Logs
1. Introduction: Why the Mesh Layer Is the Best Entry Point for Observability
Microservice architecture splits a monolith into dozens or hundreds of services, and with it the call chain grows longer and fault localization becomes harder. The traditional approach is to instrument each business service, log, and report metrics, but this has three unavoidable problems:
- High invasiveness: Every service needs to introduce an SDK, and with different languages and frameworks, instrumentation methods vary widely.
- Incomplete coverage: Old services, third-party services, and services in heterogeneous languages often lack instrumentation, and the chain breaks mid-way.
- Difficult upgrades: Upgrading the instrumentation SDK requires business teams to cooperate on release, with long cycles and high resistance.
Service Mesh strips inter-service communication out of the business process and hands it to a sidecar proxy (e.g., Envoy) for unified handling. This means all traffic entering and leaving a service passes through the proxy, and the proxy naturally has positional advantage for collecting observability data. This article revolves around the three main threads of distributed tracing, metrics, and logs, and explains how to build a unified observability solution at the mesh layer.
The diagram below summarizes how the three main threads converge into one unified observability solution:
flowchart LR
subgraph MESH["Mesh data plane (Envoy Sidecar)"]
T["Distributed tracing<br/>Trace Context propagation"]
M["Metrics<br/>RED methodology"]
L["Access logs<br/>Trace ID injection"]
end
T --> T2["Tempo / Jaeger"]
M --> M2["Prometheus"]
L --> L2["Loki / ES"]
T2 --> G["Grafana unified view"]
M2 --> G
L2 --> G
G --> A["Metrics find problems → Tracing locates the chain → Logs restore details"]
2. The Unique Value of Mesh-Level Observability: A Full View of Inter-Service Communication
2.1 From an "Application View" to a "Network View"
Application-layer instrumentation sees "what requests my service handled," while the mesh layer sees "what happened between service A and service B." The latter can answer many questions the former cannot:
- How much time did the request spend on the network? Is the service slow to process or is the network slow?
- Was the connection reset? Did retries trigger? How many retries?
- Is the upstream 5xx a business error or a proxy-layer error (e.g., upstream connection timeout)?
- Did the mutual TLS handshake succeed? Is the mTLS certificate about to expire?
2.2 Zero Invasiveness and Language Independence
The sidecar proxy runs as an independent process, fully decoupled from the business language. As long as Java, Go, Python, and Node.js services are connected to the mesh, they automatically gain unified metrics, tracing, and access log capabilities without changing a single line of business code.
2.3 Unified Data Semantics
All service traffic passes through the same kind of proxy, which means metric names, label dimensions, and log formats are naturally unified. This lays the foundation for subsequent aggregation analysis, cross-service comparison, and SLO calculation; no more headaches over "every team names metrics differently."
2.4 The Boundary of the Full View
It must be stated objectively that the mesh layer cannot see logic inside the service. When a request calls a database, cache, or downstream HTTP interface inside a service, only application-layer instrumentation can cover those details. Therefore the best practice is to combine the mesh layer with the application layer: the mesh layer handles the full picture of inter-service communication, the application layer handles intra-service details, and the two are connected through a unified Trace Context.
The following diagram compares the observation scope of the application layer and the mesh layer, and how they are connected via Trace Context:
flowchart TD
subgraph APP["Application layer (inside service)"]
A1["Business logic"]
A2["Database / cache calls"]
A3["Downstream HTTP interfaces"]
end
subgraph MESH["Mesh layer (inter-service communication)"]
B1["Inbound request span"]
B2["Outbound request span"]
B3["Retry / timeout / mTLS"]
end
A1 --> A2
A1 --> A3
B1 --> A1
A3 --> B2
B1 -.->|"Trace Context linking"| B2
3. OpenTelemetry and Mesh Integration: Trace Context Propagation Across Proxies
3.1 The Core of Distributed Tracing: Context Propagation
The essence of distributed tracing is linking every step a request goes through into a tree. The link is Trace Context; a set of identifiers passed along with request headers:
traceparent: W3C standard header, format00-<trace-id>-<parent-id>-<flags>;tracestate: optional vendor extension information;- Compatible formats: B3 (Zipkin), Jaeger, etc.
As long as every link in the chain can read and pass through these headers correctly, the trace connects.
The propagation of Trace Context in a cross-service call can be understood as follows:
sequenceDiagram
participant C as "Client"
participant E1 as "Envoy (frontend)"
participant S1 as "frontend service"
participant E2 as "Envoy (order)"
participant S2 as "order service"
C->>E1: "Request (no traceparent)"
E1->>E1: "Generate trace-id, create inbound span"
E1->>S1: "Inject traceparent then forward"
S1->>E2: "Call with traceparent"
E2->>E2: "Read traceparent, create child span"
E2->>S2: "Pass through traceparent"
S2-->>E2: "Response"
E2-->>S1: "Response"
S1-->>E1: "Response"
E1-->>C: "Response"
3.2 Envoy's Tracing Capabilities
Envoy has built-in tracing support, configured via the tracing block to specify the reporting cluster and sampling policy:
tracing:
http:
name: envoy.tracers.opentelemetry
typed_config:
"@type": type.googleapis.com/envoy.config.trace.v3.OpenTelemetryConfig
grpc_service:
envoy_grpc:
cluster_name: otel_collector
service_name: mesh-proxy
The key point: Envoy reads the upstream traceparent on inbound requests, generates its own span, and injects the new context into the request headers on outbound requests. This way, even if business code has zero instrumentation, the call relationships between services are fully recorded.
3.3 Pitfalls in Cross-Proxy Propagation
Several traps to watch for in practice:
- Header allow-list: Some mesh implementations (e.g., Istio) clean unrecognized request headers by default; confirm that
traceparentandtracestateare in the pass-through allow-list. - Sampling consistency: If each proxy samples independently, upstream might sample while downstream does not, breaking the chain. It is recommended to make sampling decisions uniformly at the entry and pass them down via the flags bit of
traceparent. - Application and proxy layer handoff: If business services also use the OpenTelemetry SDK, ensure the SDK and Envoy use the same propagation format, otherwise you get two disconnected chains.
- Async message scenarios: Context propagation for message brokers like Kafka and RabbitMQ requires extra configuration; you cannot rely only on HTTP headers.
3.4 A Typical Trace Shape
flowchart LR
A["Client"] --> B["Envoy (frontend)"]
B --> C["frontend service"]
C --> D["Envoy (order)"]
D --> E["order service"]
E --> F["Envoy (payment)"]
F --> G["payment service"]
In this chain, each Envoy produces both inbound and outbound spans; if the business service also has instrumentation, it adds internal spans. What you see in Tempo/Jaeger is a complete call tree that clearly distinguishes "network time" from "service processing time."
4. Metric Collection: From Envoy Metrics to the RED Methodology
4.1 What Metrics Envoy Exposes
Envoy's metric system is divided into three layers:
- Downstream: Inbound direction, describing requests the proxy receives, e.g.,
envoy_http_downstream_rq_total,envoy_http_downstream_rq_time; - Upstream: Outbound direction, describing requests the proxy sends upstream, e.g.,
envoy_cluster_upstream_rq_total,envoy_cluster_upstream_rq_time; - Server: The proxy's own state, e.g., memory, connections, threads.
These metrics are exposed by default in Prometheus format on the admin port (usually :15090/stats/prometheus) and can be scraped directly by Prometheus.
4.2 The RED Methodology
RED is a service-oriented metric methodology, with the three letters standing for:
- Rate: request rate (QPS);
- Errors: error rate;
- Duration: response time distribution.
The correspondence between the three RED dimensions and Envoy metrics and observation goals is as follows:
flowchart LR
R["Rate<br/>Request rate"] --> R1["envoy_http_downstream_rq_total"]
E["Errors<br/>Error rate"] --> E1["envoy_http_downstream_rq_xx"]
D["Duration<br/>Response time"] --> D1["envoy_http_downstream_rq_time_bucket"]
R1 --> Q["QPS dashboard"]
E1 --> Q2["Error rate dashboard"]
D1 --> Q3["P99 latency dashboard"]
Compared to the USE (resource-oriented) methodology, RED suits inter-service communication observability better. Based on Envoy metrics, you can construct queries like:
# Rate: requests per second
sum(rate(envoy_http_downstream_rq_total[1m])) by (destination_service)
# Errors: 5xx error rate
sum(rate(envoy_http_downstream_rq_xx{response_code_class="5"}[1m])) by (destination_service)
/
sum(rate(envoy_http_downstream_rq_total[1m])) by (destination_service)
# Duration: P99 response time
histogram_quantile(0.99,
sum(rate(envoy_http_downstream_rq_time_bucket[1m])) by (le, destination_service)
)
4.3 Planning Metric Labels
The value of metrics depends heavily on label dimensions. At minimum, keep the following labels:
| Label | Description | Purpose |
|---|---|---|
source_workload |
Caller workload | Locate problem source |
destination_workload |
Callee workload | Locate problem target |
destination_service |
Target service name | Service-level aggregation |
response_code |
HTTP status code | Error analysis |
response_flags |
Envoy response flags | Distinguish timeout/reset/no healthy upstream |
Among these, response_flags is especially useful: UF indicates upstream connection failure, UO indicates upstream overflow, NR indicates no route. These flags quickly break down "5xx" into specific causes.
4.4 Metric Collection Architecture
flowchart TD
A["Envoy Sidecar"] -->|"scrape :15090"| B["Prometheus"]
B --> C["Recording Rules"]
C --> D["Grafana dashboard"]
C --> E["Alert rules Alertmanager"]
For large clusters, use Prometheus federation or solutions like Thanos/VictoriaMetrics for long-term storage and cross-cluster aggregation to avoid excessive load on a single instance.
5. Log Correlation: Linking Trace ID with Access Logs
5.1 The Value of Access Logs
Metrics tell you "there is a problem," tracing tells you "which chain the problem is on," and logs tell you "what actually happened." Envoy's access logs record detailed information about each request, including response code, duration, upstream address, response flags, and more.
5.2 Injecting Trace ID into Access Logs
This is the key step for linking logs and traces. Envoy supports using %REQ(X-REQUEST-ID)% or extracting the trace ID via %DYNAMIC_METADATA% in the access log format:
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: /dev/stdout
log_format:
json_format:
timestamp: "%START_TIME%"
method: "%REQ(:METHOD)%"
path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%"
protocol: "%PROTOCOL%"
response_code: "%RESPONSE_CODE%"
response_flags: "%RESPONSE_FLAGS%"
duration_ms: "%DURATION%"
upstream_host: "%UPSTREAM_HOST%"
trace_id: "%REQ(X-B3-TRACEID)%"
span_id: "%REQ(X-B3-SPANID)%"
Note: different tracing formats use different header names. When using the W3C standard, traceparent is a combined header and requires extra parsing; with B3 format, you can directly extract X-B3-TraceId.
5.3 Log Collection and Correlated Queries
Logs are collected by collectors such as Fluent Bit / Vector and written to Loki or Elasticsearch. The key to correlation analysis is using the same Trace ID as the query condition:
- Discover a P99 spike for some service on the Grafana dashboard;
- Click to drill down, jump to Tempo to view slow traces in that period;
- Select a slow trace and get the
trace_id; - Query all related logs for that request in Loki with
{app="order"} |= "<trace_id>".
This path connects "metrics find problem → tracing locates chain → logs restore details" into a closed loop.
The complete correlation troubleshooting loop is shown below:
flowchart TD
A["Grafana dashboard sees P99 spike"] --> B["Drill down to Tempo"]
B --> C["View slow traces in period"]
C --> D["Select trace, get trace_id"]
D --> E["Loki query logs by trace_id"]
E --> F["Restore request details, locate root cause"]
F --> G["Return to dashboard to verify fix"]
G --> A
5.4 Log Field Standardization Suggestions
For efficient correlation queries, standardize log fields:
trace_id,span_id: required, for correlation;service_name,pod_name: for filtering;response_flags: for quickly filtering abnormal requests;duration_ms: for sorting to find slow requests.
At the same time, control log volume: full logging of access logs is costly at high QPS. Record errors and slow requests in full, and sample normal requests proportionally.
6. Practical: A Mesh Observability Stack Based on Prometheus + Grafana + Tempo
6.1 Overall Architecture
flowchart TD
subgraph MESH["Service Mesh data plane"]
A1["Envoy Sidecar A"]
A2["Envoy Sidecar B"]
end
A1 -->|"OTLP traces"| C["OpenTelemetry Collector"]
A2 -->|"OTLP traces"| C
A1 -->|"metrics :15090"| D["Prometheus"]
A2 -->|"metrics :15090"| D
A1 -->|"access log"| E["Fluent Bit"]
A2 -->|"access log"| E
C --> F["Tempo"]
E --> G["Loki"]
D --> H["Grafana"]
F --> H
G --> H
6.2 Deploying OpenTelemetry Collector
The Collector handles receiving, batching, and forwarding trace data:
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
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp:
endpoint: tempo:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp]
6.3 Configuring Prometheus to Scrape Envoy Metrics
scrape_configs:
- job_name: 'envoy-sidecars'
metrics_path: /stats/prometheus
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_container_name]
action: keep
regex: istio-proxy
- source_labels: [__meta_kubernetes_pod_label_app]
target_label: destination_workload
- source_labels: [__address__]
regex: '(.+):\d+'
target_label: __address__
replacement: '${1}:15090'
6.4 Grafana Dashboard Design
A practical mesh observability dashboard usually includes four sections:
- Global overview: overall QPS, error rate, P99 latency, grouped by service;
- Service details: after selecting a service, show its upstream/downstream call relationships and status code distribution;
- Exception drill-down: error trends categorized by
response_flags, clickable to jump to Tempo; - Log panel: embed a Loki query to show error logs in the current time window.
Grafana's Exemplar feature can attach trace_id directly to metric charts; clicking a data point jumps to the corresponding trace, which is the most elegant way to bridge metrics and traces.
6.5 Alert Rule Examples
groups:
- name: mesh-slo
rules:
- alert: HighErrorRate
expr: |
sum(rate(envoy_http_downstream_rq_xx{response_code_class="5"}[5m])) by (destination_service)
/
sum(rate(envoy_http_downstream_rq_total[5m])) by (destination_service)
> 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Service {{ $labels.destination_service }} 5xx error rate exceeds 5%"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
sum(rate(envoy_http_downstream_rq_time_bucket[5m])) by (le, destination_service)
) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "Service {{ $labels.destination_service }} P99 latency exceeds 1s"
6.6 Lessons from Implementation
- Layer sampling strategy: full sampling at the entry, proportional sampling internally, ensuring critical traces are complete while controlling cost.
- Control metric cardinality: avoid using high-cardinality fields like
pathanduser_idas labels; otherwise Prometheus memory grows quickly. - Keep log and trace storage separate: Tempo stores traces, Loki stores logs, correlated via
trace_id; do not try to solve everything with a single storage. - Build dashboards before alerting: without a stable dashboard baseline, alert thresholds are hard to set accurately, leading to many false positives.
7. Summary
Service Mesh provides a unified, zero-intrusion data collection layer for observability. Through OpenTelemetry's Trace Context for cross-proxy propagation, through Envoy metrics implementing the RED methodology, and through Trace ID linking access logs with traces, the Prometheus + Grafana + Tempo stack forms a complete closed loop of "metrics find problems, tracing locates the chain, logs restore details."
It is worth emphasizing that mesh-level observability is not a replacement for application-level observability but a complement. Only by combining the two with consistent semantics can you truly achieve comprehensive control over a distributed system.