No description
  • Elixir 37.2%
  • Rust 31.7%
  • Go 31.1%
Find a file
Michael Freeman 87efa7432b Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:23:32 -05:00
conformance Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
elixir Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
go Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
rust Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
.gitignore Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
CONTRACT.md Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
LICENSE Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00
README.md Scaffold serviceradar-sdk-otel: carrier contract, conformance fixtures, go/rust/elixir groundwork 2026-06-11 11:23:32 -05:00

serviceradar-sdk-otel

W3C trace-context propagation for messaging transports — small, per-language SDKs that keep your traces connected across NATS (and, next, Kafka) hops.

Why

OpenTelemetry auto-instrumentation covers HTTP and gRPC well: the client injects traceparent, the server extracts it, and the trace stays whole. Messaging hops are where traces silently fall apart — the producer's span context never makes it into the message, so every consumer starts a fresh root span and you get a pile of disconnected single-span traces instead of one causal story.

For NATS specifically there is no official OTel instrumentation in any major language — no upstream package injects into or extracts from NATS message headers for you. ServiceRadar hit exactly this inside its own platform (every internal NATS hop produced orphan root spans) and fixed it with a small propagation layer. This repository packages that capability as a supported, standalone SDK for Go, Rust, and Elixir, so application, add-on, and edge authors get the same fix without reinventing it.

The carrier API concept

The core of each package is transport-agnostic: inject the active span context into a string-keyed header map, extract a remote parent from one. That header map is the carrier. Transports differ only in where the carrier lives (NATS message headers, Kafka record headers, …), so adapters are thin wrappers over the same two operations plus optional producer/consumer span helpers with messaging.* semantic-convention attributes.

The exact cross-language behavior (header keys, no-op/graceful-degradation rules, span helper semantics) is specified in CONTRACT.md and enforced by the shared vectors in conformance/fixtures.json, which every language's test suite consumes — inject in language A, extract in language B, and the trace stays connected.

Status

Groundwork scaffold — APIs are usable but pre-release (0.1.0-dev), nothing is published to a registry yet.

Component Status
Behavior contract (CONTRACT.md) groundwork ✔
Conformance fixtures groundwork ✔
Go — otelmsg carrier core groundwork ✔
Go — natsotel NATS adapter groundwork ✔
Rust — carrier core groundwork ✔
Rust — nats feature (async-nats) groundwork ✔
Elixir — Propagation core groundwork ✔
Elixir — Gnat adapter groundwork ✔
Publishing pipelines (hex / crates.io / Go tags) not started
Kafka adapter not started (see roadmap)

Quickstarts

Go

Package otelmsg (carrier core) and natsotel (NATS adapter) live in one module, github.com/carverauto/serviceradar-sdk-otel/go; the NATS dependency is only pulled in if you import the adapter subpackage.

import (
    "github.com/carverauto/serviceradar-sdk-otel/go" // package otelmsg
    "github.com/carverauto/serviceradar-sdk-otel/go/natsotel"
    "github.com/nats-io/nats.go"
)

// Producer: span + inject + publish in one call.
msg := &nats.Msg{Subject: "events.created", Data: payload}
err := natsotel.PublishMsg(ctx, nc, msg)

// Consumer: extract + consumer span around your handler.
sub, err := nc.Subscribe("events.created", natsotel.WrapMsgHandler(
    func(ctx context.Context, msg *nats.Msg) error {
        // ctx carries the producer's trace as remote parent
        return handle(ctx, msg)
    }))

// Or transport-free, with any header map:
carrier := otelmsg.MapCarrier{}
otelmsg.Inject(ctx, carrier)            // adds traceparent/tracestate
ctx2 := otelmsg.Extract(ctx, carrier)   // remote parent or unchanged ctx

Rust

Crate serviceradar-sdk-otel; enable the nats feature for the async-nats adapter.

[dependencies]
serviceradar-sdk-otel = { version = "0.1.0-dev", features = ["nats"] }
use serviceradar_sdk_otel::{inject_context, extract, HeaderInjector, HeaderExtractor};
use serviceradar_sdk_otel::nats; // feature = "nats"

// Transport-free, over any HashMap<String, String>:
let mut headers = std::collections::HashMap::new();
inject_context(&cx, &mut HeaderInjector(&mut headers));
let parent_cx = extract(&HeaderExtractor(&headers));

// NATS adapter: async_nats::HeaderMap in/out + span builders.
let mut hm = async_nats::HeaderMap::new();
nats::inject_into_headers(&cx, &mut hm);
let parent_cx = nats::extract_from_headers(&hm);
let span = nats::producer_span_builder("events.created"); // messaging.* attrs preset

Elixir

Mix package serviceradar_sdk_otel. The Gnat adapter compiles only when :gnat is present in your app (the dependency is optional).

alias ServiceRadar.SDK.Otel.Propagation

# Producer: stamp traceparent/tracestate onto NATS headers.
headers = Propagation.inject_headers([])          # list carrier (Gnat-style)
:ok = Gnat.pub(conn, "events.created", payload, headers: headers)

# Consumer: attach the remote parent before starting your span.
:ok = Propagation.extract_context(message.headers)

# Map carriers work too:
headers_map = Propagation.inject_map(%{})

# Optional span helpers with messaging.* semconv attributes:
alias ServiceRadar.SDK.Otel.MessagingSpan
MessagingSpan.with_producer_span("events.created", fn -> Gnat.pub(...) end)
MessagingSpan.with_consumer_span("events.created", fn -> handle(message) end)

ServiceRadar-ready defaults

The SDKs only propagate context; export is whatever your OTel SDK is configured for. Against a ServiceRadar deployment, point your exporter at the central collector — OTLP/gRPC on 4317 or OTLP/HTTP on 4318 — or at the edge collector add-on's local endpoint (http://localhost:4318 on the node running the add-on). With that wired, a NATS producer/consumer pair using these SDKs shows up in the ServiceRadar trace detail view as one multi-span trace.

Roadmap: Kafka

The carrier API was designed so a Kafka adapter (record headers are also a string-keyed carrier) lands without breaking changes: same inject/extract core, a kafkaotel/kafka-feature adapter with messaging.system = "kafka" and topic-based destination naming. Tracked as a follow-on task in the OpenSpec change below; not started here.

Provenance

This repository implements the OpenSpec change add-otel-messaging-sdk in the ServiceRadar repository (openspec/changes/add-otel-messaging-sdk/ — proposal and sdk-otel spec). The Elixir core generalizes ServiceRadar's internal ServiceRadar.Otel.Propagation module, which is the production-proven reference implementation.

License

Apache-2.0 — see LICENSE.