Skip to content

gRPC Tracing — Distributed Tracing with OpenTelemetry for gRPC Services

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about grpc tracing. We cover key concepts, practical examples, and best practices to help you master this topic.

gRPC distributed tracing with OpenTelemetry captures the full lifecycle of RPC calls across service boundaries, providing visibility into latency breakdowns, error origins, and dependency graphs in microservice architectures.

What You'll Learn

  • OpenTelemetry instrumentation for gRPC
  • Trace propagation via gRPC metadata
  • Span attributes for RPC metadata
  • Server and client interceptor-based tracing
  • Trace sampling strategies
  • Analyzing traces with Jaeger or Datadog

Why It Matters

Without tracing, finding the root cause of a slow request in a 50-service architecture is nearly impossible. Distributed traces show exactly which service, which method, and which database call caused the slowdown. DodaTech's Durga Antivirus Pro traces every gRPC call across 50+ Microservices, with 1% sampling storing 100 million traces per day for analysis.

Real-World Use

A user reports that threat analysis takes 10 seconds. The trace shows: 1s in API Gateway, 0.5s in auth service, 7s in ML analysis service (a new model version has a bug), and 1.5s in database. The team identifies the ML service as the bottleneck and rolls back the model.

sequenceDiagram
    participant Client
    participant Gateway
    participant Auth
    participant ML
    participant DB
    Client->>Gateway: AnalyzeThreat
    activate Gateway
    Gateway->>Auth: ValidateToken
    activate Auth
    Auth-->>Gateway: OK (50ms)
    deactivate Auth
    Gateway->>ML: AnalyzeThreat
    activate ML
    ML->>DB: Query models
    activate DB
    DB-->>ML: Model (100ms)
    deactivate DB
    ML-->>Gateway: Result (7000ms)
    deactivate ML
    Gateway-->>Client: Response (7200ms)
    deactivate Gateway
    Note over Client,Gateway: Trace: total=7.2s, ML=7.0s (97%)

Code Examples

Example 1: OpenTelemetry gRPC Instrumentation in Go

package main

import (
    "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
    "go.opentelemetry.io/otel/sdk/trace"
    "google.golang.org/grpc"
)

func initTracer() {
    exporter, _ := otlptrace.New(context.Background())
    tp := trace.NewTracerProvider(
        trace.WithBatcher(exporter),
        trace.WithSampler(trace.ProbabilityBased(0.1)), // 10% sampling
    )
    otel.SetTracerProvider(tp)
}

func main() {
    initTracer()
    
    // Server-side instrumentation
    s := grpc.NewServer(
        grpc.StatsHandler(otelgrpc.NewServerHandler()),
    )
    
    // Client-side instrumentation
    conn, _ := grpc.Dial("localhost:50051",
        grpc.WithInsecure(),
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    )
}

// Add custom span attributes in resolvers
func (s *server) ReportThreat(ctx context.Context,
    req *pb.ThreatRequest) (*pb.ThreatResponse, error) {
    
    tracer := otel.Tracer("threat-service")
    ctx, span := tracer.Start(ctx, "ReportThreat")
    defer span.End()
    
    // Add custom attributes
    span.SetAttributes(
        attribute.String("threat.name", req.ThreatName),
        attribute.String("device.id", req.DeviceId),
        attribute.String("severity", req.Severity.String()),
    )
    
    result, err := s.processThreat(ctx, req)
    if err != nil {
        span.RecordError(err)
        span.SetStatus(codes.Error, err.Error())
        return nil, err
    }
    
    span.SetAttributes(
        attribute.String("result.status",
            result.Status.String()),
    )
    return result, nil
}

Example 2: Python OpenTelemetry Tracing

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter \
    import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.instrumentation.grpc import GrpcInstrumentor

# Initialize tracing
provider = TracerProvider()
processor = BatchSpanProcessor(OTLPSpanExporter())
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)

# Instrument gRPC automatically
GrpcInstrumentor().instrument()

# Manual tracing in resolvers
tracer = trace.get_tracer(__name__)

class ThreatService(pb.ThreatServiceServicer):
    def ReportThreat(self, request, context):
        with tracer.start_as_current_span("ReportThreat") as span:
            span.set_attribute("threat.name", request.threat_name)
            span.set_attribute("device.id", request.device_id)
            
            # Trace downstream calls
            with tracer.start_as_current_span(
                "validate_device") as validate_span:
                device = self.device_service.get_device(
                    request.device_id)
                validate_span.set_attribute(
                    "device.found", str(device is not None))
            
            with tracer.start_as_current_span(
                "analyze_threat") as analyze_span:
                result = self.analyzer.analyze(
                    request.threat_name)
                analyze_span.set_attribute(
                    "severity", result.severity)
            
            return pb.ThreatResponse(status=result.status)

Example 3: Trace Propagation in JavaScript

const grpc = require('@grpc/grpc-js');
const { diag, DiagConsoleLogger, DiagLogLevel } = require('@opentelemetry/api');
const { GrpcInstrumentation } = require('@opentelemetry/instrumentation-grpc');
const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');

// Setup tracing
const provider = new NodeTracerProvider();
const jaegerExporter = new JaegerExporter({
  endpoint: 'http://jaeger:14268/api/traces',
});

provider.addSpanProcessor(new BatchSpanProcessor(jaegerExporter));
provider.register();

// Instrument gRPC
const grpcInstrumentation = new GrpcInstrumentation({
  // Add custom attributes from metadata
  metadataToSpanAttributes: {
    client: {
      'authorization': 'auth.token',
      'x-correlation-id': 'correlation.id',
    },
    server: {
      'x-region': 'request.region',
    },
  },
});

grpcInstrumentation.setConfig({
  // Capture gRPC status code
  responseHook: (span, response) => {
    if (response.code) {
      span.setAttribute('grpc.status_code', response.code);
    }
  },
});

// Access the tracer in resolvers
const tracer = trace.getTracer('threat-service');

function reportThreat(call, callback) {
  const span = tracer.startSpan('process_threat', {
    attributes: {
      'threat.name': call.request.name,
    },
  });
  
  // Process...
  span.end();
  callback(null, response);
}

Common Mistakes

  1. Sampling too aggressively — 100% sampling generates huge data volumes. Start with 1-5% sampling for high-throughput services, 100% for low-traffic services.
  2. Not propagating trace context — without propagation, each service starts a new trace, losing the connection between calls. Use gRPC metadata to propagate trace headers.
  3. Adding too many span attributes — hundreds of attributes increase trace size and cost. Add only attributes useful for debugging: method name, latency, error code, user ID.
  4. Not setting span status on error — without explicit error status, an errored span looks the same as a successful one. Set span status to Error when the RPC fails.
  5. Tracing in the hot path synchronously — sending traces synchronously adds latency. Use batch span processors that send traces asynchronously in the background.

Practice Questions

  1. How does distributed tracing differ from logging and metrics?
  2. How are trace contexts propagated across gRPC service boundaries?
  3. What is the purpose of trace sampling?
  4. How do you identify the root cause of latency from a trace?
  5. Why should you set span attributes for errors?

Challenge: Implement distributed tracing for a chain of 3 gRPC services where each service: receives the trace context from upstream, adds its own span with service-specific attributes, calls downstream with propagated context, and reports errors with appropriate span status.

Mini Project

Build a distributed tracing system for a gRPC microservice architecture with: OpenTelemetry instrumentation on all services, trace propagation via gRPC metadata, 1% sampling with ability to force 100% for specific traces, Jaeger or Datadog visualization, and latency breakdown alerts.

FAQ

What is the overhead of OpenTelemetry tracing?

With batch processing, the overhead is 1-5% of CPU. The main cost is storage. At 1% sampling, a service handling 10K req/s generates ~100 traces/s, manageable for most systems.

How do I force 100% sampling for specific users?

Use a sampler that checks for a 'force-trace' header. If present, sample at 100%. This lets you debug specific users without sampling everything.

What should I include in span attributes?

Include: service name, method name, user ID, device ID, latency, request size, response size, error code, and any IDs needed for debugging.

How long should traces be retained?

Store raw traces for 7-30 days for debugging. Store aggregated metrics (p50/p95/p99 per method) indefinitely for trend analysis.

Can I trace gRPC calls that cross process boundaries?

Yes. gRPC metadata carries trace context (trace ID, span ID, sampling flag). The receiving service extracts and continues the trace.

What's Next

Learn about gRPC logging

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro