Created
July 9, 2026 01:00
-
-
Save mohashari/b08c8bc8ebfd11358530e40c7d4c63f4 to your computer and use it in GitHub Desktop.
Tracing Distributed Transactions Across Async Queue Boundaries with OpenTelemetry Context Propagation — code snippets
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| "github.com/IBM/sarama" | |
| "go.opentelemetry.io/otel/propagation" | |
| ) | |
| // SaramaProducerCarrier injects trace context into Sarama producer message headers. | |
| type SaramaProducerCarrier struct { | |
| Headers *[]sarama.RecordHeader | |
| } | |
| // Get returns the value associated with the passed key. | |
| func (c SaramaProducerCarrier) Get(key string) string { | |
| if c.Headers == nil { | |
| return "" | |
| } | |
| for _, h := range *c.Headers { | |
| if string(h.Key) == key { | |
| return string(h.Value) | |
| } | |
| } | |
| return "" | |
| } | |
| // Set sets the key-value pair in the message headers. | |
| func (c SaramaProducerCarrier) Set(key, val string) { | |
| for i, h := range *c.Headers { | |
| if string(h.Key) == key { | |
| (*c.Headers)[i].Value = []byte(val) | |
| return | |
| } | |
| } | |
| *c.Headers = append(*c.Headers, sarama.RecordHeader{ | |
| Key: []byte(key), | |
| Value: []byte(val), | |
| }) | |
| } | |
| // Keys lists the keys in the carrier. | |
| func (c SaramaProducerCarrier) Keys() []string { | |
| if c.Headers == nil { | |
| return nil | |
| } | |
| keys := make([]string, len(*c.Headers)) | |
| for i, h := range *c.Headers { | |
| keys[i] = string(h.Key) | |
| } | |
| return keys | |
| } | |
| // Ensure interface compatibility at compile time. | |
| var _ propagation.TextMapCarrier = (*SaramaProducerCarrier)(nil) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| "context" | |
| "fmt" | |
| "github.com/IBM/sarama" | |
| "go.opentelemetry.io/otel" | |
| "go.opentelemetry.io/otel/attribute" | |
| "go.opentelemetry.io/otel/trace" | |
| ) | |
| const TracerName = "order-service-producer" | |
| type OrderPublisher struct { | |
| producer sarama.SyncProducer | |
| tracer trace.Tracer | |
| } | |
| func NewOrderPublisher(p sarama.SyncProducer) *OrderPublisher { | |
| return &OrderPublisher{ | |
| producer: p, | |
| tracer: otel.Tracer(TracerName), | |
| } | |
| } | |
| func (op *OrderPublisher) PublishOrderCreated(ctx context.Context, orderID string, payload []byte) error { | |
| // 1. Start a producer span using the appropriate span kind | |
| ctx, span := op.tracer.Start(ctx, "orders.publish_created", | |
| trace.WithSpanKind(trace.SpanKindProducer), | |
| trace.WithAttributes( | |
| attribute.String("messaging.system", "kafka"), | |
| attribute.String("messaging.destination", "orders"), | |
| attribute.String("messaging.destination_kind", "topic"), | |
| attribute.String("messaging.kafka.message_key", orderID), | |
| ), | |
| ) | |
| defer span.End() | |
| // 2. Prepare the Kafka message headers | |
| headers := []sarama.RecordHeader{ | |
| {Key: []byte("content-type"), Value: []byte("application/json")}, | |
| } | |
| // 3. Inject the span context into the headers using the custom carrier | |
| carrier := SaramaProducerCarrier{Headers: &headers} | |
| otel.GetTextMapPropagator().Inject(ctx, carrier) | |
| msg := &sarama.ProducerMessage{ | |
| Topic: "orders", | |
| Key: sarama.StringEncoder(orderID), | |
| Value: sarama.ByteEncoder(payload), | |
| Headers: headers, | |
| } | |
| // 4. Send the message | |
| partition, offset, err := op.producer.SendMessage(msg) | |
| if err != nil { | |
| span.RecordError(err) | |
| return fmt.Errorf("failed to send kafka message: %w", err) | |
| } | |
| span.SetAttributes( | |
| attribute.Int32("messaging.kafka.partition", partition), | |
| attribute.Int64("messaging.kafka.offset", offset), | |
| ) | |
| return nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| "github.com/IBM/sarama" | |
| "go.opentelemetry.io/otel/propagation" | |
| ) | |
| // SaramaConsumerCarrier extracts trace context from Sarama consumer message headers. | |
| type SaramaConsumerCarrier struct { | |
| Headers []*sarama.RecordHeader | |
| } | |
| // Get returns the value associated with the passed key. | |
| func (c SaramaConsumerCarrier) Get(key string) string { | |
| for _, h := range c.Headers { | |
| if h != nil && string(h.Key) == key { | |
| return string(h.Value) | |
| } | |
| } | |
| return "" | |
| } | |
| // Set sets the key-value pair in the message headers. | |
| func (c SaramaConsumerCarrier) Set(key, val string) { | |
| for i, h := range c.Headers { | |
| if h != nil && string(h.Key) == key { | |
| c.Headers[i].Value = []byte(val) | |
| return | |
| } | |
| } | |
| c.Headers = append(c.Headers, &sarama.RecordHeader{ | |
| Key: []byte(key), | |
| Value: []byte(val), | |
| }) | |
| } | |
| // Keys lists the keys in the carrier. | |
| func (c SaramaConsumerCarrier) Keys() []string { | |
| keys := make([]string, len(c.Headers)) | |
| for i, h := range c.Headers { | |
| if h != nil { | |
| keys[i] = string(h.Key) | |
| } | |
| } | |
| return keys | |
| } | |
| var _ propagation.TextMapCarrier = (*SaramaConsumerCarrier)(nil) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| "context" | |
| "encoding/json" | |
| "log" | |
| "github.com/IBM/sarama" | |
| "go.opentelemetry.io/otel" | |
| "go.opentelemetry.io/otel/attribute" | |
| "go.opentelemetry.io/otel/trace" | |
| ) | |
| const ConsumerTracerName = "payment-service-consumer" | |
| type OrderConsumer struct { | |
| tracer trace.Tracer | |
| } | |
| func NewOrderConsumer() *OrderConsumer { | |
| return &OrderConsumer{ | |
| tracer: otel.Tracer(ConsumerTracerName), | |
| } | |
| } | |
| func (oc *OrderConsumer) HandleMessage(msg *sarama.ConsumerMessage) { | |
| // 1. Extract context from incoming message headers | |
| carrier := SaramaConsumerCarrier{Headers: msg.Headers} | |
| ctx := otel.GetTextMapPropagator().Extract(context.Background(), carrier) | |
| // 2. Start a consumer span, linked to the extracted parent context | |
| ctx, span := oc.tracer.Start(ctx, "orders.consume_created", | |
| trace.WithSpanKind(trace.SpanKindConsumer), | |
| trace.WithAttributes( | |
| attribute.String("messaging.system", "kafka"), | |
| attribute.String("messaging.destination", msg.Topic), | |
| attribute.String("messaging.destination_kind", "topic"), | |
| attribute.Int32("messaging.kafka.partition", msg.Partition), | |
| attribute.Int64("messaging.kafka.offset", msg.Offset), | |
| attribute.String("messaging.kafka.message_key", string(msg.Key)), | |
| ), | |
| ) | |
| defer span.End() | |
| // 3. Process message payload | |
| var order struct { | |
| ID string `json:"order_id"` | |
| Total float64 `json:"total"` | |
| } | |
| if err := json.Unmarshal(msg.Value, &order); err != nil { | |
| span.RecordError(err) | |
| return | |
| } | |
| span.SetAttributes(attribute.String("order.id", order.ID)) | |
| // Execute actual business logic (e.g., process payment) | |
| if err := oc.processPayment(ctx, order.ID, order.Total); err != nil { | |
| span.RecordError(err) | |
| log.Printf("Failed to process payment for order %s: %v", order.ID, err) | |
| } | |
| } | |
| func (oc *OrderConsumer) processPayment(ctx context.Context, orderID string, total float64) error { | |
| // Inner database or HTTP call span will inherit the context and properly nest | |
| _, span := oc.tracer.Start(ctx, "payment.execute") | |
| defer span.End() | |
| // Payment gateway business logic goes here | |
| return nil | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| amqp "github.com/rabbitmq/amqp091-go" | |
| "go.opentelemetry.io/otel/propagation" | |
| ) | |
| // AmqpHeadersCarrier propagates trace context inside RabbitMQ AMQP message headers. | |
| type AmqpHeadersCarrier struct { | |
| Table amqp.Table | |
| } | |
| // Get returns the value associated with the passed key. | |
| func (c AmqpHeadersCarrier) Get(key string) string { | |
| if c.Table == nil { | |
| return "" | |
| } | |
| val, ok := c.Table[key] | |
| if !ok { | |
| return "" | |
| } | |
| strVal, ok := val.(string) | |
| if !ok { | |
| return "" | |
| } | |
| return strVal | |
| } | |
| // Set sets the key-value pair in the RabbitMQ table. | |
| func (c AmqpHeadersCarrier) Set(key, val string) { | |
| if c.Table == nil { | |
| return | |
| } | |
| c.Table[key] = val | |
| } | |
| // Keys returns all keys in the carrier. | |
| func (c AmqpHeadersCarrier) Keys() []string { | |
| keys := make([]string, 0, len(c.Table)) | |
| for k := range c.Table { | |
| keys = append(keys, k) | |
| } | |
| return keys | |
| } | |
| var _ propagation.TextMapCarrier = (*AmqpHeadersCarrier)(nil) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| package tracing | |
| import ( | |
| "context" | |
| "log" | |
| "go.opentelemetry.io/otel" | |
| "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" | |
| "go.opentelemetry.io/otel/propagation" | |
| "go.opentelemetry.io/otel/sdk/resource" | |
| sdktrace "go.opentelemetry.io/otel/sdk/trace" | |
| semconv "go.opentelemetry.io/otel/semconv/v1.24.0" | |
| ) | |
| // InitTracerProvider initializes an OTLP-backed Tracer Provider with custom sampling rules. | |
| func InitTracerProvider(ctx context.Context, serviceName string, otlpEndpoint string, sampleRatio float64) (*sdktrace.TracerProvider, error) { | |
| exporter, err := otlptracegrpc.New(ctx, | |
| otlptracegrpc.WithInsecure(), | |
| otlptracegrpc.WithEndpoint(otlpEndpoint), | |
| ) | |
| if err != nil { | |
| return nil, err | |
| } | |
| res, err := resource.New(ctx, | |
| resource.WithAttributes( | |
| semconv.ServiceNameKey.String(serviceName), | |
| semconv.DeploymentEnvironmentKey.String("production"), | |
| ), | |
| ) | |
| if err != nil { | |
| return nil, err | |
| } | |
| // ParentBased sampler respects the parent's sampling decision. | |
| // If no parent is present, it falls back to the specified TraceIDRatioBased sampler. | |
| sampler := sdktrace.ParentBased( | |
| sdktrace.TraceIDRatioBased(sampleRatio), | |
| ) | |
| tp := sdktrace.NewTracerProvider( | |
| sdktrace.WithSampler(sampler), | |
| sdktrace.WithBatcher(exporter), | |
| sdktrace.WithResource(res), | |
| ) | |
| otel.SetTracerProvider(tp) | |
| // Register the global propagator for tracing and baggage | |
| otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator( | |
| propagation.TraceContext{}, | |
| propagation.Baggage{}, | |
| )) | |
| log.Printf("Successfully initialized OTel Tracer Provider for %s with sample ratio %.2f", serviceName, sampleRatio) | |
| return tp, nil | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment