From 77394ebfdd4672942422703d83eeff22f960acbd Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 28 Mar 2026 18:53:19 +0800 Subject: [PATCH 1/2] docs: add distributed trace propagation section to tracing guide --- howto/Tracing.md | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/howto/Tracing.md b/howto/Tracing.md index d564d3e..e22b9d4 100644 --- a/howto/Tracing.md +++ b/howto/Tracing.md @@ -197,6 +197,58 @@ This means a single trace ID connects your logs and error reports — you can se export TRACE_HEADER_NAME=x-request-id # Use a different header ``` +## Distributed Trace Propagation (W3C) + +In addition to ColdBrew's application-level trace ID, OpenTelemetry propagates **W3C trace context** (`traceparent`/`tracestate` headers) for distributed tracing across services. This is what links spans together in your tracing backend (Jaeger, Tempo, etc.). + +### What's automatic + +ColdBrew handles these flows without any code: + +| Flow | Propagation | How | +|------|------------|-----| +| **Incoming gRPC** | Extracted from gRPC metadata | `otelgrpc` stats handler | +| **Incoming HTTP** | Extracted from `traceparent` header | `tracingWrapper` middleware | +| **HTTP → gRPC gateway** | Parent span linked to child | Context propagation via W3C propagator | +| **gRPC server → client** | Injected into outgoing metadata | `otelgrpc` stats handler | + +### Outgoing HTTP calls + +When calling external HTTP services, use `NewHTTPExternalSpan` to create a span and inject trace headers: + +```go +span, ctx := tracing.NewHTTPExternalSpan(ctx, "other-service", "/api/data", nil) +defer span.End() + +req, _ := http.NewRequestWithContext(ctx, "GET", "https://other-service/api/data", nil) +// Inject trace headers into the outgoing request +otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + +resp, err := http.DefaultClient.Do(req) +``` + +Or pass the headers directly to let `NewHTTPExternalSpan` inject for you: + +```go +hdr := make(http.Header) +span, ctx := tracing.NewHTTPExternalSpan(ctx, "other-service", "/api/data", hdr) +defer span.End() + +req, _ := http.NewRequestWithContext(ctx, "GET", "https://other-service/api/data", nil) +req.Header = hdr +resp, err := http.DefaultClient.Do(req) +``` + +{: .important } +If you make HTTP calls without `NewHTTPExternalSpan`, trace context is **not** propagated automatically. You must inject it manually using `otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))`. + +### Verifying propagation + +To confirm trace context is flowing correctly, check your tracing backend for: +- A single trace ID connecting HTTP → gRPC → downstream spans +- Parent-child relationships between service boundaries +- The `traceparent` header in outgoing requests + --- [TraceId interceptor]: https://pkg.go.dev/github.com/go-coldbrew/interceptors#TraceIdInterceptor From e60663fa26f3039e7eeec8bb157a63f2e9567b4b Mon Sep 17 00:00:00 2001 From: Ankur Shrivastava Date: Sat, 28 Mar 2026 22:19:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20improve=20propagation=20examples=20?= =?UTF-8?q?=E2=80=94=20error=20handling,=20consistent=20URLs,=20clearer=20?= =?UTF-8?q?descriptions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- howto/Tracing.md | 45 +++++++++++++++++++++++++-------------------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/howto/Tracing.md b/howto/Tracing.md index e22b9d4..5e8917f 100644 --- a/howto/Tracing.md +++ b/howto/Tracing.md @@ -207,45 +207,50 @@ ColdBrew handles these flows without any code: | Flow | Propagation | How | |------|------------|-----| -| **Incoming gRPC** | Extracted from gRPC metadata | `otelgrpc` stats handler | -| **Incoming HTTP** | Extracted from `traceparent` header | `tracingWrapper` middleware | +| **Incoming gRPC** | Extracted from gRPC metadata | OTEL gRPC stats handler | +| **Incoming HTTP** | Extracted from `traceparent` header | HTTP gateway tracing middleware | | **HTTP → gRPC gateway** | Parent span linked to child | Context propagation via W3C propagator | -| **gRPC server → client** | Injected into outgoing metadata | `otelgrpc` stats handler | +| **gRPC server → client** | Injected into outgoing metadata | OTEL gRPC stats handler | ### Outgoing HTTP calls -When calling external HTTP services, use `NewHTTPExternalSpan` to create a span and inject trace headers: +When calling external HTTP services, use `NewHTTPExternalSpan` to create a span and inject trace headers. Pass an `http.Header` to have headers injected automatically: ```go -span, ctx := tracing.NewHTTPExternalSpan(ctx, "other-service", "/api/data", nil) +hdr := make(http.Header) +span, ctx := tracing.NewHTTPExternalSpan(ctx, "payment-service", "https://payment-service/api/charge", hdr) defer span.End() -req, _ := http.NewRequestWithContext(ctx, "GET", "https://other-service/api/data", nil) -// Inject trace headers into the outgoing request -otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) +req, err := http.NewRequestWithContext(ctx, "POST", "https://payment-service/api/charge", body) +if err != nil { + return err +} +req.Header = hdr resp, err := http.DefaultClient.Do(req) +if err != nil { + span.SetError(err) + return err +} +defer resp.Body.Close() ``` -Or pass the headers directly to let `NewHTTPExternalSpan` inject for you: +{: .important } +If you make HTTP calls without `NewHTTPExternalSpan`, trace context is **not** propagated automatically. You must inject it manually: ```go -hdr := make(http.Header) -span, ctx := tracing.NewHTTPExternalSpan(ctx, "other-service", "/api/data", hdr) -defer span.End() - -req, _ := http.NewRequestWithContext(ctx, "GET", "https://other-service/api/data", nil) -req.Header = hdr -resp, err := http.DefaultClient.Do(req) +req, err := http.NewRequestWithContext(ctx, "GET", url, nil) +if err != nil { + return err +} +otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) +resp, err := client.Do(req) ``` -{: .important } -If you make HTTP calls without `NewHTTPExternalSpan`, trace context is **not** propagated automatically. You must inject it manually using `otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))`. - ### Verifying propagation To confirm trace context is flowing correctly, check your tracing backend for: -- A single trace ID connecting HTTP → gRPC → downstream spans +- A single OpenTelemetry trace (same W3C trace ID) connecting HTTP → gRPC → downstream spans - Parent-child relationships between service boundaries - The `traceparent` header in outgoing requests