← Back to Guide
Observability L2 · PRACTICAL ~60 min

Correlate Traces and Logs with OpenTelemetry

Deploy OpenTelemetry Collector in gateway mode and instrument a demo application to emit traces. Correlate a failing request trace with its associated logs in Loki using the trace ID to perform end-to-end root cause analysis.

Objective

The OpenTelemetry Collector receives traces, metrics, and logs from applications and routes them to backends (Tempo for traces, Loki for logs). When logs include the trace ID, you can jump from a slow/failing trace directly to the logs produced during that exact request — eliminating the need to grep through timestamps. This exercise wires the complete pipeline.

Prerequisites

Steps

01

Install Grafana Tempo (trace backend)

# Add Grafana Helm charts
helm repo add grafana https://grafana.github.io/helm-charts
helm repo update

# Install Tempo in monolithic mode (development setup)
helm install tempo grafana/tempo \
  --namespace monitoring \
  --set tempo.storage.trace.backend=local \
  --wait

# Install Loki for logs
helm install loki grafana/loki \
  --namespace monitoring \
  --set loki.commonConfig.replication_factor=1 \
  --set loki.storage.type=filesystem \
  --wait

kubectl get pods -n monitoring | grep -E "tempo|loki"
02

Deploy the OpenTelemetry Collector

# Install OTel Operator first
helm install opentelemetry-operator \
  open-telemetry/opentelemetry-operator \
  --namespace monitoring

# Create Collector configuration
cat << 'EOF' | kubectl apply -f -
apiVersion: opentelemetry.io/v1alpha1
kind: OpenTelemetryCollector
metadata:
  name: otel-gateway
  namespace: monitoring
spec:
  mode: Deployment
  config: |
    receivers:
      otlp:
        protocols:
          grpc:
            endpoint: 0.0.0.0:4317
          http:
            endpoint: 0.0.0.0:4318

    processors:
      batch:
        timeout: 1s
      memory_limiter:
        check_interval: 1s
        limit_mib: 400

    exporters:
      otlp/tempo:
        endpoint: tempo.monitoring:4317
        tls:
          insecure: true
      loki:
        endpoint: http://loki.monitoring:3100/loki/api/v1/push
        default_labels_enabled:
          exporter: false
          job: true

    service:
      pipelines:
        traces:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [otlp/tempo]
        logs:
          receivers: [otlp]
          processors: [memory_limiter, batch]
          exporters: [loki]
EOF
03

Deploy the OpenTelemetry demo application

# Deploy OTel demo app (astronomy shop)
helm install otel-demo open-telemetry/opentelemetry-demo \
  --namespace otel-demo \
  --create-namespace \
  --set components.frontendProxy.service.type=ClusterIP \
  --set opentelemetry-collector.enabled=false \
  --set "default.env[0].name=OTEL_EXPORTER_OTLP_ENDPOINT" \
  --set "default.env[0].value=http://otel-gateway-collector.monitoring:4317" \
  --wait --timeout 5m

kubectl get pods -n otel-demo
04

Configure Grafana data sources

# Add Tempo data source to Grafana via ConfigMap
cat << 'EOF' | kubectl apply -f -
apiVersion: v1
kind: ConfigMap
metadata:
  name: grafana-datasources-tracing
  namespace: monitoring
  labels:
    grafana_datasource: "1"
data:
  tracing-datasources.yaml: |
    apiVersion: 1
    datasources:
    - name: Tempo
      type: tempo
      url: http://tempo.monitoring:3100
      jsonData:
        tracesToLogsV2:
          datasourceUid: loki
          filterByTraceID: true
          filterBySpanID: false
        serviceMap:
          datasourceUid: prometheus
    - name: Loki
      type: loki
      url: http://loki.monitoring:3100
      jsonData:
        derivedFields:
        - name: TraceID
          matcherRegex: '"traceId":"(\w+)"'
          url: '$${__value.raw}'
          datasourceUid: tempo
EOF
05

Generate traffic and find a failing trace

# Port-forward to the demo app frontend
kubectl port-forward svc/otel-demo-frontendproxy \
  -n otel-demo 8080:8080 &

# Generate some traffic including errors
for i in $(seq 1 50); do
  curl -s http://localhost:8080/api/products >/dev/null &
done
wait

# In Grafana → Explore → Tempo data source
# Search for: Service = "frontend" with status = "error"
# Or run TraceQL query:
# { .http.status_code = 500 }

# Find a failing trace and note the TraceID
# Example: 4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d
06

Correlate the trace with logs in Loki

# In Grafana Tempo trace view:
# 1. Click on a span in the trace
# 2. Click "Logs for this span" (appears when Loki is configured)
# 3. Grafana auto-queries Loki using the traceId

# Alternatively, search Loki directly with the TraceID:
# In Grafana → Explore → Loki data source
# Query: {job="otel-demo/frontend"} | json | traceId="4a3b2c1d..."

# Via API:
TRACE_ID="your-trace-id-here"
kubectl exec -n monitoring \
  $(kubectl get pod -l app=loki -n monitoring -o name | head -1) \
  -- wget -qO- "http://localhost:3100/loki/api/v1/query_range?query=\
{job%3D\"otel-demo%2Ffrontend\"}+|+json+|+traceId%3D%22${TRACE_ID}%22&limit=50"

# The logs should show all log lines emitted during that specific
# request, correlated by the shared traceId field
The key to trace-log correlation is the traceId field in structured JSON logs. The OTel SDK automatically injects the current trace context into log records when you use the SDK's logging bridge.

Success Criteria

Further Reading