otel

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 4, 2026 License: MIT Imports: 14 Imported by: 0

Documentation

Overview

Package otel adds optional OpenTelemetry tracing and metrics to a babelqueue producer or consumer. It lives in its own module so the zero-dependency core never imports OpenTelemetry — wiring a TracerProvider (and a MeterProvider) is opt-in.

Span shape (ADR-0038, semantic conventions v1.44.0): a PRODUCER span "send <queue>" and a CONSUMER span "process <queue>", with messaging.system set by WithSystem (the broker, e.g. "kafka"), messaging.operation.type / .name, the babelqueue.* identity attributes, and error.type on a failure. The URN is an attribute, never part of the span name.

Metrics (ADR-0039): pass a meter with WithMeter and the wrappers record messaging.process.duration, messaging.client.sent.messages, messaging.client.consumed.messages and babelqueue.retries; NewDeadLetterObserver adds babelqueue.dlq.messages. Without a meter nothing is recorded.

Cross-hop trace propagation works at two levels, layered:

  • trace_id ↔ TraceID (ADR-0025, v0.1): the envelope's trace_id maps 1:1 to an OTel TraceID, so every hop that shares a trace_id shares one trace — correlation and per-hop timing with zero wire/transport change.
  • W3C traceparent (ADR-0028, v0.2): the producer also injects the active span context as a traceparent transport header (beside the frozen envelope, never in it), so the consumer starts its span as a true child of the producer span — real cross-hop parent-child linkage. This rides the out-of-band HeaderPublisher / ReceivedMessage.Headers seam (ADR-0027) and is available on any transport that carries headers. When no traceparent header is present it falls back to the v0.1 trace_id behaviour — a strict, backward- compatible upgrade.

Index

Constants

View Source
const (
	// MetricProcessDuration is the handler's wall time in seconds, recorded once per invocation,
	// success or failure (histogram, unit "s").
	MetricProcessDuration = "messaging.process.duration"
	// MetricSentMessages counts publish attempts, success or failure (counter, unit "{message}").
	MetricSentMessages = "messaging.client.sent.messages"
	// MetricConsumedMessages counts the deliveries that enter the consumer wrapper (counter).
	MetricConsumedMessages = "messaging.client.consumed.messages"
	// MetricRetries counts the deliveries that enter the wrapper with top-level attempts > 0 (counter).
	MetricRetries = "babelqueue.retries"
	// MetricDeadLetters counts the dead-letters the SDK carried out (counter), fed by the
	// observer from [NewDeadLetterObserver].
	MetricDeadLetters = "babelqueue.dlq.messages"
)

The standard metric set (ADR-0039). Names, units and kinds are identical in every BabelQueue SDK.

View Source
const (
	HeaderTraceparent = "traceparent"
	HeaderTracestate  = "tracestate"
)

HeaderTraceparent / HeaderTracestate are the out-of-band transport headers that carry W3C Trace Context across a hop (ADR-0028, implementing ADR-0025 Option 2). They ride beside the frozen envelope on the transport's per-message metadata channel — the same out-of-band seam as the replay-bypass marker (ADR-0027) — so a consumer can start its span as a true child of the producer's span, not merely share the trace_id-derived trace. The envelope is never touched (GR-1).

View Source
const (
	// SemconvVersion is the OpenTelemetry semantic-conventions release this module targets
	// (ADR-0038 section 1).
	SemconvVersion = "1.44.0"

	// SchemaURL is the schema URL of [SemconvVersion]. This module does not create the tracer or
	// the meter, so create them with it:
	//
	//	tp.Tracer("my-app", trace.WithSchemaURL(otel.SchemaURL))
	//	mp.Meter("my-app", metric.WithSchemaURL(otel.SchemaURL))
	SchemaURL = "https://opentelemetry.io/schemas/" + SemconvVersion
)

Variables

This section is empty.

Functions

func NewDeadLetterObserver added in v0.4.0

func NewDeadLetterObserver(opts ...Option) babelqueue.DeadLetterObserver

NewDeadLetterObserver returns the babelqueue.DeadLetterObserver that feeds babelqueue.dlq.messages (ADR-0039 section 5): +1 per dead-letter the App carried out, with messaging.system from WithSystem, messaging.operation.name "process" and messaging.destination.name the source queue when known. Register it on the runtime:

app := babelqueue.NewApp(transport,
    babelqueue.WithDeadLetterObserver(otel.NewDeadLetterObserver(
        otel.WithSystem("kafka"), otel.WithMeter(meter))))

Without WithMeter the observer records nothing.

func Publish

func Publish(
	ctx context.Context,
	tracer trace.Tracer,
	app *babelqueue.App,
	urn string,
	data map[string]any,
	opts ...babelqueue.Option,
) (string, error)

Publish is Publisher.Publish with the default options: the deprecated messaging.system "babelqueue" and no metrics. Use NewPublisher to pass WithSystem or WithMeter.

func TraceIDOf

func TraceIDOf(traceID string) trace.TraceID

TraceIDOf maps an envelope trace_id to a deterministic OTel TraceID: a UUID maps to its 16 raw bytes; any other string is hashed (SHA-256, first 16 bytes). The inverse of UUIDOf for the UUID case.

func UUIDOf

func UUIDOf(t trace.TraceID) string

UUIDOf formats an OTel TraceID (16 bytes) as a canonical UUID string — the form a producer stamps into the message's trace_id so a consumer can recover the same TraceID.

func WrapHandler

func WrapHandler(tracer trace.Tracer, handler babelqueue.Handler, opts ...Option) babelqueue.Handler

WrapHandler returns handler decorated to emit a CONSUMER span "process <queue>" per message (ADR-0038), recording the handler's error/status, and, when WithMeter is given, the consumer metrics (ADR-0039). Register it like any handler: app.Handle(urn, otel.WrapHandler(tracer, handler, otel.WithSystem("kafka"))).

The span name and messaging.destination.name come from the delivered envelope's meta.queue; when it is absent or blank the span is just "process" and carries no destination. The options are additive: WrapHandler(tracer, handler) keeps working and writes the deprecated default messaging.system "babelqueue".

Parent selection (ADR-0028 / ADR-0025 Option 2): when the producer carried a W3C traceparent on the transport (surfaced by the runtime via babelqueue.HeadersFromContext), the span is started as a true child of the producer's span — real cross-hop parent→child linkage with per-hop span timing. When no traceparent header is present, it falls back to the v0.1 behaviour: a remote parent derived from the envelope's trace_id (ADR-0025 Option 1), which shares the trace but not exact span linkage. So enabling traceparent propagation is a strict, backward-compatible upgrade — no regression for messages produced without it. A trace_id that is not a UUID is preserved untouched and mapped to a TraceID by hashing; that is logged as a warning, once per distinct value.

Types

type Extracted added in v0.3.0

type Extracted struct {
	// Source is "traceparent" when a structurally valid header was delivered, else
	// "trace_id" (the v0.1 fallback of ADR-0025).
	Source string
	// TraceIDHex is the header's 32-hex trace id, or the envelope trace_id without hyphens.
	TraceIDHex string
	// ParentSpanID (16 hex) and Flags (2 hex) are set only when Source is "traceparent".
	ParentSpanID string
	Flags        string
}

Extracted describes where a consumer's trace identity came from and its raw hex parts. It is the pure, header-level view of the ADR-0028 extraction contract that the cross-SDK conformance section `traceparent` pins; WrapHandler performs the same decision with the OTel propagator and live span contexts.

func ExtractTraceparent added in v0.3.0

func ExtractTraceparent(traceparent, envelopeTraceID string) Extracted

ExtractTraceparent resolves the trace identity for a delivered message from the out-of-band traceparent header value (empty when none was delivered) and the envelope trace_id. It is a thin wrapper over [remoteParentFromHeaders], the exact extraction WrapHandler performs at runtime, so what it reports is what a consumer span really gets: a header the W3C propagator rejects (malformed, all-zero ids, uppercase hex, ...) is ignored — it never fails — and behaves exactly like no header. The envelope trace_id is never modified (GR-4).

type Option added in v0.4.0

type Option func(*config)

Option configures WrapHandler, NewPublisher and NewDeadLetterObserver.

func WithConsumerGroup added in v0.4.0

func WithConsumerGroup(group string) Option

WithConsumerGroup sets messaging.consumer.group.name on the consumer span (ADR-0038 section 4), intended for Kafka. It is written whenever given and non-empty; it is never a metric attribute.

func WithMeter added in v0.4.0

func WithMeter(m metric.Meter) Option

WithMeter turns on the standard metrics (ADR-0039): the instruments are created from m once per wrapper. Without it the wrappers record no metric. Create the meter with metric.WithSchemaURL(SchemaURL).

func WithSystem added in v0.4.0

func WithSystem(system string) Option

WithSystem sets messaging.system to the broker name (ADR-0038 section 3): "redis", "rabbitmq", "aws_sqs", "servicebus", "pulsar", "kafka" or "activemq". Any other value is written as given, unvalidated. An empty value counts as absent, which keeps the deprecated default "babelqueue".

type Publisher added in v0.4.0

type Publisher struct {
	// contains filtered or unexported fields
}

Publisher publishes through a babelqueue.App under a PRODUCER span "send <queue>" (ADR-0038) and, when WithMeter is given, counts the attempt (ADR-0039). Build it once and reuse it: the metric instruments are created per Publisher, not per message.

func NewPublisher added in v0.4.0

func NewPublisher(tracer trace.Tracer, app *babelqueue.App, opts ...Option) *Publisher

NewPublisher returns a Publisher over app. The options are those of WrapHandler; the consumer-only WithConsumerGroup is accepted and ignored.

func (*Publisher) Publish added in v0.4.0

func (p *Publisher) Publish(ctx context.Context, urn string, data map[string]any, opts ...babelqueue.Option) (string, error)

Publish starts a PRODUCER span "send <queue>" and publishes via app, propagating the trace to the downstream consumer two ways:

  • It injects the active span context as a W3C traceparent (and tracestate) onto the outgoing transport headers, so a consumer can start its span as a true child of this producer span — real cross-hop parent→child linkage (ADR-0028 / ADR-0025 Option 2). The header rides beside the frozen envelope, never in it (GR-1), via babelqueue.App.PublishEnvelopeWithHeaders; if the transport can't carry headers it degrades to a plain publish (the traceparent is simply not propagated — no error).
  • It also carries the active trace's TraceID into the message's trace_id (the v0.1 behaviour, ADR-0025 Option 1), so even a consumer that ignores the header — or a transport that drops it — still recovers the same trace (correlation without exact span linkage).

The span starts as the bare "send": the queue is only known from the envelope the App builds (a per-call babelqueue.WithQueue or the app's default queue), so once the envelope has been handed to the transport the span is renamed "send <queue>" and gets messaging.destination.name, messaging.message.id and messaging.message.conversation_id. When no envelope is ever handed to the transport (the build or the encode failed) the span stays "send" and carries none of those, never a guessed destination.

It otherwise behaves like babelqueue.App.Publish, returning the message id.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL