Protoflow integrates with OpenTelemetry for distributed tracing across services and message brokers.
Protoflow's tracing support:
- Propagates trace context via CloudEvents extensions
- Maps to W3C Trace Context standard
- Works across retries and delayed delivery
- Integrates with existing OTEL infrastructure
| CloudEvents Extension | W3C Header | Description |
|---|---|---|
pf_trace_id |
traceparent (trace-id part) |
32-character hex trace ID |
pf_parent_id |
traceparent (parent-id part) |
16-character hex span ID |
import (
"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.17.0"
)
func initTracer() (*sdktrace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(context.Background(),
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("my-service"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}svc := protoflow.NewService(cfg, logger, ctx, protoflow.ServiceDependencies{})
// TracerMiddleware is included in DefaultMiddlewares()
// It automatically creates spans for message processing// When publishing, set trace context
evt := protoflow.NewCloudEvent("order.created", "order-service", data)
protoflow.SetTraceID(&evt, "0af7651916cd43dd8448eb211c80319c")
protoflow.SetParentID(&evt, "b7ad6b7169203331")
// Or extract from current span
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
protoflow.SetTraceID(&evt, span.SpanContext().TraceID().String())
protoflow.SetParentID(&evt, span.SpanContext().SpanID().String())
}
err := svc.PublishEvent(ctx, evt)svc.ConsumeEvents("order.created", func(ctx context.Context, evt protoflow.Event) error {
// Get trace context from event
traceID := protoflow.GetTraceID(evt)
parentID := protoflow.GetParentID(evt)
// Create child span
tracer := otel.Tracer("order-handler")
ctx, span := tracer.Start(ctx, "process-order")
defer span.End()
// Add event attributes
span.SetAttributes(
attribute.String("event.id", evt.ID),
attribute.String("event.type", evt.Type),
)
// Process...
return nil
})The TracerMiddleware automatically:
- Extracts trace context from incoming messages
- Creates a processing span
- Adds standard attributes
- Records errors and exceptions
// Span attributes added automatically:
// - messaging.system: transport name
// - messaging.destination: topic/queue
// - messaging.message_id: event ID
// - messaging.operation: "process"Trace context is preserved across retries:
svc.ConsumeEvents("payment.process", func(ctx context.Context, evt protoflow.Event) error {
// Same trace_id across all retry attempts
traceID := protoflow.GetTraceID(evt)
attempt := protoflow.GetAttempt(evt)
log.Printf("Processing attempt %d, trace: %s", attempt, traceID)
if shouldRetry {
return protoflow.ErrRetryAfter(5*time.Second, err)
}
return nil
})When publishing response events, copy the trace context:
svc.ConsumeEvents("order.created", func(ctx context.Context, evt protoflow.Event) error {
// Process order...
// Create response event with same trace context
responseEvt := protoflow.NewCloudEvent("order.confirmed", "order-service", data)
protoflow.CopyTracingContext(evt, &responseEvt)
return svc.PublishEvent(ctx, responseEvt)
})In addition to trace IDs, use correlation IDs for business-level request tracking:
// At API gateway
evt := protoflow.NewCloudEvent("order.created", "api-gateway", data)
protoflow.SetCorrelationID(&evt, requestID) // From HTTP header
protoflow.SetTraceID(&evt, traceID) // From OTEL
// In downstream handlers
correlationID := protoflow.GetCorrelationID(evt)
log.Printf("Processing request: %s", correlationID)# docker-compose.yml
services:
jaeger:
image: jaegertracing/all-in-one:latest
ports:
- "16686:16686" # UI
- "14268:14268" # Collector HTTP
- "4317:4317" # OTLP gRPC// Configure OTEL to export to Jaeger
exporter, err := otlptracegrpc.New(context.Background(),
otlptracegrpc.WithEndpoint("localhost:4317"),
otlptracegrpc.WithInsecure(),
)import "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry"
func initTracer() {
provider := opentelemetry.NewTracerProvider()
otel.SetTracerProvider(provider)
}| Transport | Trace Propagation |
|---|---|
| Kafka | Message headers |
| RabbitMQ | Message properties |
| NATS/JetStream | Message headers |
| AWS SQS | Message attributes |
| SQLite/Postgres | JSON metadata |
- Always propagate trace context when publishing response events
- Use correlation IDs for business-level tracking alongside trace IDs
- Add meaningful attributes to spans (order ID, customer ID, etc.)
- Record errors with appropriate error types and messages
- Sample appropriately in production to control costs
// Example: Adding business context to spans
span.SetAttributes(
attribute.String("order.id", order.ID),
attribute.String("customer.id", order.CustomerID),
attribute.Float64("order.amount", order.Amount),
)
// Record errors with context
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return protoflow.ErrRetryAfter(5*time.Second, err)
}Enable verbose logging to see trace propagation:
logger := protoflow.NewSlogServiceLogger(slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
})))Look for log entries with trace_id and span_id fields.