Help us shape the future of Go! Take the 2026 Go Developer Survey

telemetry

package
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Nov 1, 2026 License: MIT Imports: 24 Imported by: 0

Documentation

Overview

Package telemetry provides a vendor-neutral observability contract: traces, metrics, and logs, for Arkoren packages and for application code. Core never imports a vendor SDK; drivers adapt OpenTelemetry, Prometheus, or the in-process memory driver (arkoren.dev/core/service/telemetry/memorytelemetry) to it.

Model

A Telemetry wraps a driver. Each signal is a capability interface that a driver implements or not: TraceDriver, MetricDriver, and LogDriver, all built on Lifecycle. Compose picks a driver per signal, for example OpenTelemetry for traces and Prometheus for metrics. A signal without a driver is a no-op.

t := telemetry.New(memorytelemetry.New())
if err := t.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
tracer := t.Tracer("example.com/billing")
meter := t.Meter("example.com/billing")

charges := meter.Counter("billing.charges",
	telemetry.Unit("{charge}"),
	telemetry.Keys("currency"),
)

ctx, span := tracer.Start(ctx, "charge", telemetry.Attrs(telemetry.String("currency", "EUR")))
charges.Add(ctx, 1, telemetry.String("currency", "EUR"))
span.End()
if err := t.Stop(ctx); err != nil {
	return err
}

The scope given to Tracer and Meter names the instrumentation, such as "arkoren.dev/core/service/cache" or the importing package's path.

Telemetry owns the behavior that must be identical for every driver: the lifecycle, attribute validation, declared-key filtering, and the non-recording fast path. Drivers only record. Start and Stop are serialized and backend callbacks must return; cancellation is cooperative, not forced. Composite startup rollback preserves the original startup context, including an expired deadline. Give normal exporter shutdown a separate application-owned budget.

Attributes

Attr is the attribute type: String, Int, Int64, Float64, Bool, Strings, and Duration (float seconds). It maps one to one onto OpenTelemetry attributes. Invalid attributes (empty key) are dropped. Span Attrs options snapshot their input containers. A single attribute is stored inline; constructing an option with multiple attributes copies its slice even if a later span start is disabled.

Metrics

Instruments are created once with their name, Unit, Description, declared Keys, and optional Buckets: Meter.Counter, Meter.UpDownCounter, Meter.Histogram, and Meter.Gauge. Prometheus needs label names at creation and OpenTelemetry does not; declaring keys works on both and guards against cardinality mistakes. Recordings keep only declared keys, in declaration order, and missing keys become "". Names follow OpenTelemetry semantic conventions exactly as written; drivers translate them.

Propagation

The driver owns the wire format (W3C Trace Context, B3, ...). Core calls Telemetry.Inject and Telemetry.Extract at process boundaries with a Carrier: HeaderCarrier for HTTP headers and MapCarrier for message or workflow metadata.

Propagated context is only as trustworthy as its sender, so both HTTP boundaries are gated. Middleware extracts a caller's trace context only when MiddlewareBuilder.TrustRemote allows it. The default, including nil, is TrustNone: incoming headers never supply the server span's parent. This replaces the former TrustInternal default; trusted services must explicitly opt in with TrustInternal, MiddlewareBuilder.TrustedNetworks, or TrustAll. A private proxy address does not authenticate browser-supplied headers passing through it. Keep TrustNone at a public edge unless the proxy strips untrusted context. X-Forwarded-For is never consulted. Transport injects headers, baggage included, only where TransportBuilder.Propagation allows; the default, PropagateInternal, covers private and loopback IPs, single-label names, and the suffixes .internal, .local, .svc, and .cluster.local. Add partner hosts with TransportBuilder.PropagationHosts or use PropagateAll. Spans and metrics are recorded either way.

Logs

Logs stay log/slog. Telemetry.Handler wraps a handler so that records logged with a context holding a span get trace_id and span_id attributes, and, if the driver implements LogDriver, are exported. Correlation works only with the context-aware methods (InfoContext, ErrorContext, ...), which the shared lint configuration enforces in Arkoren's own code.

HTTP

Telemetry.Middleware traces and measures incoming requests, and Telemetry.Transport traces, measures, and propagates outgoing ones. Middleware and Transport do the same for Default, resolved at request time. The middleware also exposes the trace ID on the W3C traceresponse header (optionally another header) and as a problem response member, so clients and support can quote it. Register it before other middleware.

r.Use(app.Telemetry().Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceHeader("X-Trace-ID")
}))

client := &http.Client{Transport: app.Telemetry().Transport(nil)}

Lifecycle

Construction never fails and does no I/O. Telemetry.Start validates and connects once; Telemetry.Stop flushes. Before Start and after Stop everything is a no-op; a second Start returns ErrStarted; Start after Stop returns ErrStopped; Stop without Start is safe. The application starts telemetry first and shuts it down last, so startup and shutdown logs and spans are still exported.

Defaults

Default and SetDefault mirror log/slog. The default is a no-op that generates no IDs and does not allocate. Components resolve Default when they use it, not when they are built, so SetDefault may come late.

Observing

Telemetry.Observe attaches an Observer at runtime. It receives every span (with the driver's IDs), log record, and measurement the telemetry produces, whatever the configured driver does with them, and an observer that is also a TraceDriver or MetricDriver fills in for a signal the telemetry has no driver for. See Telemetry.Observe for the rules. Capture stops admitting new notifications when telemetry stops, but Stop neither waits for nor interrupts a notification already admitted. FinishedSpan describes facade timing and backend-selected parent metadata; observers are not a native SDK export stream. Custom span recorders must implement SpanRecorder.Parent. Capture the effective parent before creating the child, return zero for roots and inactive spans, and forward Parent through decorators. Native parent precedence is unchanged. This required-method migration changes no JSON field names.

Instrumented components

The core database, cache, feature, storage, mail, queue, and broadcast facades take a Telemetry setting on their role-specific construction builder, as does the HTTP client, schedule, search and LLM components. LLM exposes separate ModelBuilder and EmbedderBuilder roles. Nil telemetry follows Default per operation. They keep their instruments in Instruments and check Telemetry.Enabled first, so a disabled telemetry costs no allocations. Each records under its own package path as scope, such as "arkoren.dev/core/service/database"; the HTTP Middleware and Transport record under "arkoren.dev/core/service/telemetry".

Privacy

Instrumentation records low-cardinality facts, never content. HTTP spans carry no URLs, query strings, headers, or bodies, and only 5xx responses are errors; error.type is a status code or Go type name, never error text. Database spans carry parameterised query text only. Cache spans omit keys and values. Storage spans carry the operation, store name, and object size, never paths or contents. Mail spans carry recipient and attachment counts, never addresses, subjects, or bodies. Schedule spans carry the entry name, kind, and outcome. Queue and broadcast spans carry the messaging system, destination (queue or channel), message ID, job, and attempt, never payloads. Errors of these components set error.type and the span status but are never passed to Span.RecordError, because their text can carry values. Span.RecordError is for unexpected errors only; error text is exported, so keep secrets and personal data out of it.

Example

Example demonstrates the package overview with an explicitly started, in-process backend. Ending the span before Stop makes its output observable.

package main

import (
	"context"
	"fmt"

	"arkoren.dev/core/service/telemetry"
	"arkoren.dev/core/service/telemetry/memorytelemetry"
)

func main() {
	ctx := context.Background()
	driver := memorytelemetry.New()
	service := telemetry.New(driver)

	if err := service.Start(ctx, telemetry.Setup{}); err != nil {
		fmt.Println(err)

		return
	}

	charges := service.Meter("example.com/billing").Counter("billing.charges", telemetry.Keys("currency"))
	ctx, span := service.Tracer("example.com/billing").Start(ctx, "charge",
		telemetry.Attrs(telemetry.String("currency", "EUR")))
	charges.Add(ctx, 1, telemetry.String("currency", "EUR"))
	span.End()

	if err := service.Stop(ctx); err != nil {
		fmt.Println(err)

		return
	}

	point, ok := driver.Metrics()[0].Point("EUR")
	fmt.Println("spans:", len(driver.Spans()))
	fmt.Println("charges:", point.Sum, "found:", ok)
}
Output:
spans: 1
charges: 1 found: true

Index

Examples

Constants

View Source
const (
	// LogTraceID is the log attribute that carries the trace ID.
	// Consumers can use the key without duplicating its spelling.
	//
	//	record.Attrs(func(attr slog.Attr) bool {
	//		if attr.Key == telemetry.LogTraceID {
	//			fmt.Println(attr.Value.String())
	//		}
	//		return true
	//	})
	LogTraceID = "trace_id"

	// LogSpanID is the log attribute that carries the span ID.
	// It identifies the span within the correlated trace, not a request ID.
	//
	//	record.Attrs(func(attr slog.Attr) bool {
	//		if attr.Key == telemetry.LogSpanID {
	//			fmt.Println(attr.Value.String())
	//		}
	//		return true
	//	})
	LogSpanID = "span_id"
)
View Source
const DefaultProblemKey = "trace_id"

DefaultProblemKey is the problem extension member that Middleware adds to error responses by default. It names the extension member, not the problem type or the response header.

var body map[string]any
if err := json.Unmarshal(responseBody, &body); err != nil {
	return err
}
fmt.Println(body[telemetry.DefaultProblemKey])
View Source
const DefaultTraceResponseHeader = "traceresponse"

DefaultTraceResponseHeader is the W3C Trace Context response header that Middleware sets by default. Read it on an HTTP response to correlate client reports with a server span.

value := response.Header.Get(telemetry.DefaultTraceResponseHeader)
fmt.Println(value)

Variables

View Source
var (
	// ErrStarted is returned by Start when the driver or [Telemetry] was
	// already started.
	//
	//	if err := service.Start(ctx, telemetry.Setup{}); err != nil {
	//		if errors.Is(err, telemetry.ErrStarted) {
	//			fmt.Println("telemetry was started already")
	//		}
	//		return err
	//	}
	ErrStarted = errors.New("telemetry already started")

	// ErrStopped is returned by Start when the driver or [Telemetry] was
	// already shut down. A shut-down telemetry cannot be restarted.
	//
	//	if err := service.Start(ctx, telemetry.Setup{}); err != nil {
	//		if errors.Is(err, telemetry.ErrStopped) {
	//			fmt.Println("construct a new telemetry service")
	//		}
	//		return err
	//	}
	ErrStopped = errors.New("telemetry already shut down")
)

Functions

func ContextWithSpanContext

func ContextWithSpanContext(ctx context.Context, spanContext SpanContext) context.Context

ContextWithSpanContext returns a context whose current span is a non-recording span with the given identity. Use it to adopt a span identity that did not come from Telemetry.Extract, for example one read from a workflow run. The supplied flags are preserved without validation or normalization; an invalid identity remains invalid. Adoption stores no recorder, so span mutation and End are no-ops, but a later real child can use the adopted parent.

ctx = telemetry.ContextWithSpanContext(ctx, spanContext)
Example

ExampleContextWithSpanContext demonstrates identity adoption without constructing a recording span or changing the supplied remote flag.

package main

import (
	"context"
	"fmt"

	"arkoren.dev/core/service/telemetry"
)

func main() {
	identity := telemetry.SpanContext{
		TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, Remote: true,
	}
	ctx := telemetry.ContextWithSpanContext(context.Background(), identity)
	span := telemetry.SpanFromContext(ctx)
	fmt.Println(span.Context().IsValid(), span.Context().Remote, span.IsRecording())
	span.End()
}
Output:
true true false

func Middleware

func Middleware(configure ...MiddlewareBuilderFunc) router.Middleware

Middleware is Telemetry.Middleware for the Default telemetry, resolved on every request, so it may be registered before SetDefault runs.

Example:

r.Use(telemetry.Middleware())

func PropagateAll

func PropagateAll(*http.Request) bool

PropagateAll is a TransportBuilder.Propagation predicate that injects trace context into every request, third parties included.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.Propagation(telemetry.PropagateAll)
})}
_ = httpClient

func PropagateInternal

func PropagateInternal(req *http.Request) bool

PropagateInternal is the default TransportBuilder.Propagation predicate. It accepts requests whose host is a loopback, private, or link-local IP literal, a single-label name without a dot (such as "users" or "localhost", as service discovery and Compose provide), or a name ending in ".internal", ".local", ".svc", or ".cluster.local" (case-insensitive, port ignored). Everything else, including public IPs and names that merely resolve to a private address, is external.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.Propagation(telemetry.PropagateInternal)
})}
_ = httpClient

func PropagateNone

func PropagateNone(*http.Request) bool

PropagateNone is a TransportBuilder.Propagation predicate that never injects trace context.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.Propagation(telemetry.PropagateNone)
})}
_ = httpClient

func SetDefault

func SetDefault(t *Telemetry)

SetDefault makes t the process-wide Telemetry. A nil t restores the no-op. It is safe for concurrent use. SetDefault does not start t.

telemetry.SetDefault(app.Telemetry())

func Transport

func Transport(base http.RoundTripper, configure ...TransportBuilderFunc) http.RoundTripper

Transport is Telemetry.Transport for the Default telemetry, resolved on every request. It takes the same role-specific construction callbacks.

Example:

client := &http.Client{Transport: telemetry.Transport(nil)}

func TrustAll

func TrustAll(*http.Request) bool

TrustAll is a MiddlewareBuilder.TrustRemote predicate that trusts every caller. Use it inside a service mesh or private network where only trusted workloads can reach the server.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TrustRemote(telemetry.TrustAll)
}))

func TrustInternal

func TrustInternal(r *http.Request) bool

TrustInternal is an opt-in MiddlewareBuilder.TrustRemote predicate. It trusts a request whose r.RemoteAddr is a loopback, private (RFC 1918 or IPv6 unique local fc00::/7), or link-local unicast address, and rejects public and unparseable addresses. It never reads X-Forwarded-For or similar headers, which the caller controls.

Behind a load balancer or reverse proxy that connects from a private address every request looks trusted, even when browser-supplied trace headers pass through unchanged. A private proxy address does not establish who supplied the headers. Keep TrustNone unless the proxy strips untrusted context and only forwards context from trusted workloads; restricting proxy addresses with MiddlewareBuilder.TrustedNetworks alone is not sufficient.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TrustRemote(telemetry.TrustInternal)
}))

func TrustNone

func TrustNone(*http.Request) bool

TrustNone is a MiddlewareBuilder.TrustRemote predicate that trusts no caller, so every request starts a new root trace. It is the default, including when TrustRemote is nil, and is suitable for a public edge.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TrustRemote(telemetry.TrustNone)
}))

Types

type Attr

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

Attr is an immutable key-value pair attached to spans, events, resources, and metric recordings. It is deliberately not log/slog.Attr: it has no log-only kinds (groups, lazy values, times) and it has typed slices, so a driver maps it onto its backend without conversion.

The zero Attr is invalid and is dropped by Telemetry. Attr values are safe to copy and share between goroutines.

attr := telemetry.String("region", "eu")
fmt.Println(attr.Key(), attr.AsString())

func Bool

func Bool(key string, value bool) Attr

Bool returns a boolean attribute.

telemetry.Bool("arkoren.cache.hit", true)

func Duration

func Duration(key string, value time.Duration) Attr

Duration returns a float attribute holding value in seconds, the unit OpenTelemetry semantic conventions use for durations. It has kind AttrFloat64; name the key accordingly, for example "retry.delay".

telemetry.Duration("retry.delay", 1500*time.Millisecond) // 1.5

func Float64

func Float64(key string, value float64) Attr

Float64 returns a 64-bit float attribute.

telemetry.Float64("sample.ratio", 0.25)

func Int

func Int(key string, value int) Attr

Int returns an integer attribute. It has kind AttrInt64.

telemetry.Int("http.response.status_code", 200)

func Int64

func Int64(key string, value int64) Attr

Int64 returns a 64-bit integer attribute.

telemetry.Int64("messaging.message.size", size)

func String

func String(key, value string) Attr

String returns a string attribute.

telemetry.String("http.request.method", "GET")

func Strings

func Strings(key string, values []string) Attr

Strings returns a string-slice attribute. The slice is copied, so later changes by the caller do not affect the attribute.

telemetry.Strings("roles", []string{"admin", "billing"})

func (Attr) AsBool

func (attr Attr) AsBool() bool

AsBool returns the value of an AttrBool attribute and false for other kinds.

attr := telemetry.Bool("cached", true)
fmt.Println(attr.AsBool()) // true

func (Attr) AsFloat64

func (attr Attr) AsFloat64() float64

AsFloat64 returns the value of an AttrFloat64 attribute and 0 for other kinds.

attr := telemetry.Duration("elapsed", 250*time.Millisecond)
fmt.Println(attr.AsFloat64()) // 0.25 seconds

func (Attr) AsInt64

func (attr Attr) AsInt64() int64

AsInt64 returns the value of an AttrInt64 attribute and 0 for other kinds.

attr := telemetry.Int64("size", 2048)
fmt.Println(attr.AsInt64()) // 2048

func (Attr) AsString

func (attr Attr) AsString() string

AsString returns the value of an AttrString attribute and "" for other kinds. Use Attr.Text for a string form of any kind.

attr := telemetry.String("region", "eu")
fmt.Println(attr.AsString()) // eu

func (Attr) AsStrings

func (attr Attr) AsStrings() []string

AsStrings returns a copy of the value of an AttrStrings attribute and nil for other kinds.

attr := telemetry.Strings("roles", []string{"reader"})
roles := attr.AsStrings()
roles[0] = "editor" // Does not change attr.
fmt.Println(attr.AsStrings())

func (Attr) Equal

func (attr Attr) Equal(other Attr) bool

Equal reports whether both attributes have the same key, kind, and value. String-slice values are compared by contents rather than slice identity.

left := telemetry.Strings("roles", []string{"reader"})
right := telemetry.Strings("roles", []string{"reader"})
fmt.Println(left.Equal(right)) // true

func (Attr) Key

func (attr Attr) Key() string

Key returns the attribute name. Use it when inspecting a snapshot without converting the typed value.

attr := telemetry.Int("attempt", 2)
fmt.Println(attr.Key()) // attempt

func (Attr) Kind

func (attr Attr) Kind() AttrKind

Kind returns the type of value the attribute holds. It lets consumers select the matching typed accessor without guessing.

attr := telemetry.Bool("cached", true)
if attr.Kind() == telemetry.AttrBool {
	fmt.Println(attr.AsBool())
}

func (Attr) String

func (attr Attr) String() string

String formats the attribute as key=value. It is diagnostic text, not an escaped wire encoding for propagation.

fmt.Println(telemetry.Int("attempt", 2).String()) // attempt=2

func (Attr) Text

func (attr Attr) Text() string

Text returns the value as a string: strings as they are, numbers in their shortest decimal form, booleans as true or false, and string slices joined by commas. Metric label values use this form.

attr := telemetry.Bool("cached", true)
fmt.Println(attr.Text()) // true

func (Attr) Valid

func (attr Attr) Valid() bool

Valid reports whether the attribute has a non-empty key and a value kind. The zero value and attributes with an empty key fail this check.

fmt.Println(telemetry.String("region", "eu").Valid()) // true
fmt.Println((telemetry.Attr{}).Valid())               // false

func (Attr) Value

func (attr Attr) Value() any

Value returns the value as string, int64, float64, bool, or []string according to Attr.Kind, and nil for an invalid attribute. A returned slice is a copy.

attr := telemetry.Int64("size", 2048)
if size, ok := attr.Value().(int64); ok {
	fmt.Println(size)
}
Example

ExampleAttr_Value demonstrates typed, copied attribute values used by public snapshot consumers. Editing the returned slice leaves the Attr unchanged.

package main

import (
	"fmt"

	"arkoren.dev/core/service/telemetry"
)

func main() {
	attr := telemetry.Strings("roles", []string{"reader"})

	if roles, ok := attr.Value().([]string); ok {
		roles[0] = "editor"
	}

	fmt.Println(attr.Key(), attr.Kind(), attr.Valid())
	fmt.Println(attr.AsStrings(), attr.Text(), attr.String())
	fmt.Println(attr.Equal(telemetry.Strings("roles", []string{"reader"})))
}
Output:
roles strings true
[reader] reader roles=reader
true

type AttrKind

type AttrKind string

AttrKind identifies the type of value an Attr holds. The kinds map one to one onto OpenTelemetry attribute types, without depending on SDK numbers. The empty kind identifies an invalid attribute, preserving zero Attr behavior.

if telemetry.String("method", "GET").Kind() == telemetry.AttrString {
	// Read the attribute with AsString.
}
const (
	// AttrInvalid is the kind of the zero [Attr]. Attributes of this kind are
	// dropped by [Telemetry].
	//
	//	valid := (telemetry.Attr{}).Kind() != telemetry.AttrInvalid // false
	AttrInvalid AttrKind = ""

	// AttrString holds a string.
	//
	//	kind := telemetry.String("method", "GET").Kind() // telemetry.AttrString
	AttrString AttrKind = "string"

	// AttrInt64 holds a 64-bit signed integer.
	//
	//	kind := telemetry.Int64("count", 1).Kind() // telemetry.AttrInt64
	AttrInt64 AttrKind = "int64"

	// AttrFloat64 holds a 64-bit float.
	//
	//	kind := telemetry.Float64("ratio", 0.5).Kind() // telemetry.AttrFloat64
	AttrFloat64 AttrKind = "float64"

	// AttrBool holds a boolean.
	//
	//	kind := telemetry.Bool("cached", true).Kind() // telemetry.AttrBool
	AttrBool AttrKind = "bool"

	// AttrStrings holds a slice of strings.
	//
	//	kind := telemetry.Strings("tags", []string{"fast"}).Kind() // telemetry.AttrStrings
	AttrStrings AttrKind = "strings"
)

type Carrier

type Carrier interface {
	// Get returns the value for key, or "" when absent.
	//
	//	var carrier telemetry.Carrier = telemetry.MapCarrier{"baggage": "region=eu"}
	//	fmt.Println(carrier.Get("missing")) // Empty string.
	Get(key string) string

	// Set stores value for key, replacing any existing value.
	//
	//	var carrier telemetry.Carrier = telemetry.MapCarrier{}
	//	carrier.Set("baggage", "region=eu")
	//	fmt.Println(carrier.Get("baggage"))
	Set(key, value string)

	// Keys returns the keys present in the carrier in any order.
	//
	//	var carrier telemetry.Carrier = telemetry.MapCarrier{"baggage": "region=eu"}
	//	keys := carrier.Keys()
	//	slices.Sort(keys)
	//	fmt.Println(keys)
	Keys() []string
}

Carrier is a string-keyed text container that propagation headers are written to and read from: HTTP headers, message metadata, or workflow metadata. A nil underlying map can be read, but callers must allocate it before Set.

var carrier telemetry.Carrier = telemetry.MapCarrier{}
carrier.Set("baggage", "region=eu")
fmt.Println(carrier.Get("baggage"))
Example

ExampleCarrier demonstrates both public carrier adapters without network access. Sorting keys avoids relying on map iteration order.

package main

import (
	"fmt"
	"net/http"
	"slices"

	"arkoren.dev/core/service/telemetry"
)

func main() {
	var carrier telemetry.Carrier = telemetry.MapCarrier{}
	carrier.Set("baggage", "region=eu")
	fmt.Println(carrier.Get("Baggage"))

	keys := carrier.Keys()
	slices.Sort(keys)
	fmt.Println(keys)

	headers := telemetry.HeaderCarrier(http.Header{})
	headers.Set("baggage", carrier.Get("baggage"))
	fmt.Println(headers.Get("Baggage"))
}
Output:
region=eu
[baggage]
region=eu

type Composite

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

Composite is a Lifecycle that routes each signal to its own driver. Build it with Compose; pass it to New or to the application. It serializes its lifecycle and must not be copied after use.

composite := telemetry.Compose(telemetry.Metrics(memorytelemetry.New()))
if err := composite.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
if err := composite.Stop(ctx); err != nil {
	return err
}

func Compose

func Compose(selections ...Selection) *Composite

Compose routes each signal to the driver chosen by its selection, for example OpenTelemetry for traces and Prometheus for metrics:

driver := telemetry.Compose(
	telemetry.Traces(otel),
	telemetry.Metrics(prom),
)
app := application.New(func(app *application.Builder) {
	app.Telemetry(telemetry.New(driver))
})

When a signal is selected twice, the last selection wins and a driver that ends up serving no signal is not started. Each distinct driver is started once, in the order of its first selection, and shut down in reverse order. Noncomparable driver values have no value identity: each selection is distinct. Pass a pointer to share one such driver's lifecycle across signals. If a Start fails, the drivers already started are shut down and the errors are joined. Rollback uses the startup context, including its cancellation and deadline; it attempts every acquired driver's Stop even when that context has expired. Drivers remain responsible for releasing resources on expired contexts and for returning from their callbacks. Lifecycle calls are serialized and must not synchronously reenter the same composite's Start or Stop. The result follows the Lifecycle rules itself: a second Start fails, Stop without Start is safe. Context cancellation does not interrupt the lifecycle mutex or forcibly cancel arbitrary backend callbacks.

func (*Composite) Start

func (composite *Composite) Start(ctx context.Context, setup Setup) error

Start starts every distinct driver in order. When one fails, the drivers started before it are shut down in reverse order and the errors are joined. Both startup and rollback receive ctx without removing cancellation or its deadline. A driver whose Start fails owns cleanup of its partial acquisition; rollback stops only drivers whose Start succeeded.

composite := telemetry.Compose(telemetry.Metrics(metrics))
if err := composite.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}

func (*Composite) Stop

func (composite *Composite) Stop(ctx context.Context) error

Stop shuts down the started drivers in reverse order, attempting all of them, and joins the errors. It is safe without Start and idempotent. ctx is forwarded unchanged, including cancellation; backend callbacks must cooperate and release their owned resources even if flushing cannot complete.

if err := composite.Stop(shutdownContext); err != nil {
	return err
}

type Counter

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

Counter is a monotonically increasing sum. The zero Counter is a no-op. Create it once and reuse it; negative increments are ignored by Add.

requests := meter.Counter("requests", telemetry.Keys("result"))
requests.Add(ctx, 1, telemetry.String("result", "hit"))

func (Counter) Add

func (counter Counter) Add(ctx context.Context, delta int64, attrs ...Attr)

Add increases the counter by delta. Negative deltas are ignored. Attributes outside the declared keys are dropped.

requests.Add(ctx, 1, telemetry.String("http.route", route))

type FinishedSpan

type FinishedSpan struct {
	// Scope is the instrumentation scope of the tracer that started it.
	// Use it to distinguish instrumentation libraries that reuse span names.
	//
	//	span := telemetry.FinishedSpan{Scope: "example.com/cache", Name: "cache get"}
	//	fmt.Println(span.Scope, span.Name)
	Scope string

	// Name is the span name.
	// It is a low-cardinality operation name, not a request URL or identifier.
	//
	//	span := telemetry.FinishedSpan{Name: "cache get"}
	//	fmt.Println(span.Name)
	Name string

	// Kind is the span kind.
	// It describes the operation's role rather than its success or failure.
	//
	//	span := telemetry.FinishedSpan{Name: "GET /users/{id}", Kind: telemetry.Server}
	//	fmt.Println(span.Kind == telemetry.Server)
	Kind SpanKind

	// Context is the identity of the span, as the driver's recorder reports
	// it.
	//
	//	span := telemetry.FinishedSpan{Context: telemetry.SpanContext{
	//		TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2},
	//	}}
	//	fmt.Println(span.Context.IsValid())
	Context SpanContext

	// Parent is the actual parent selected by the backend, local or remote,
	// or zero for a root. It is captured from SpanRecorder.Parent after the
	// child starts, without changing backend parent selection or context.
	//
	//	if span.Parent.IsValid() {
	//		fmt.Println(span.Parent.SpanID.String())
	//	}
	Parent SpanContext

	// Start and End are the wall-clock times of Start and End.
	// They are facade observations and may differ from native SDK timestamps.
	//
	//	start := time.Unix(0, 0)
	//	span := telemetry.FinishedSpan{Start: start, End: start.Add(250 * time.Millisecond)}
	//	fmt.Println(span.End.Sub(span.Start)) // 250ms
	Start, End time.Time

	// Attrs are the span attributes in the order they were first set; a
	// later attribute with the same key replaces the value in place.
	// The container is shared read-only with other observers.
	//
	//	span := telemetry.FinishedSpan{Attrs: []telemetry.Attr{telemetry.String("region", "eu")}}
	//	if attr, ok := span.Attr("region"); ok {
	//		fmt.Println(attr.AsString())
	//	}
	Attrs []Attr

	// Events are the recorded events in order. Recorded errors are events
	// named "exception" with the attributes exception.type and
	// exception.message.
	//
	//	span := telemetry.FinishedSpan{Events: []telemetry.SpanEvent{{Name: "retry"}}}
	//	for _, event := range span.Events {
	//		fmt.Println(event.Name)
	//	}
	Events []SpanEvent

	// Status is the final status.
	// Unset does not imply failure; normal successful HTTP spans leave it unset.
	//
	//	span := telemetry.FinishedSpan{Status: telemetry.Error, StatusDescription: "backend unavailable"}
	//	if span.Status == telemetry.Error {
	//		fmt.Println(span.StatusDescription)
	//	}
	Status StatusCode

	// StatusDescription is the description given with an [Error] status.
	// It is empty for other statuses and must not contain sensitive error text.
	//
	//	span := telemetry.FinishedSpan{Status: telemetry.Error, StatusDescription: "backend unavailable"}
	//	fmt.Println(span.StatusDescription)
	StatusDescription string
}

FinishedSpan is the snapshot of a span handed to Observer.SpanEnded. It is the same shape the memory driver stores. Containers are shared read-only across observers; copy them before editing an application-owned view.

Context and Parent come from the recorder after backend StartSpan. Parent is the actual selected parent, including when a native parent takes precedence over an adopted facade identity; zero identifies a backend-selected root. Attributes, events, and status reflect calls through the facade. Start, End, and event times are taken by the facade and can differ from backend timestamps.

span := telemetry.FinishedSpan{Attrs: []telemetry.Attr{telemetry.String("region", "eu")}}
if attr, ok := span.Attr("region"); ok {
	fmt.Println(attr.AsString())
}

func (FinishedSpan) Attr

func (span FinishedSpan) Attr(key string) (Attr, bool)

Attr returns the span attribute with the given key. The returned Attr is immutable; absence is reported by false, not an error.

span := telemetry.FinishedSpan{Attrs: []telemetry.Attr{telemetry.Int("attempt", 2)}}
if attr, ok := span.Attr("attempt"); ok {
	fmt.Println(attr.AsInt64())
}

type Gauge

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

Gauge records the current value of something. The zero Gauge is a no-op. Set reports an absolute sample rather than an increment or decrement.

depth := meter.Gauge("queue.depth")
depth.Set(ctx, 12)

func (Gauge) Set

func (gauge Gauge) Set(ctx context.Context, value float64, attrs ...Attr)

Set records value as the current value. Attributes outside the declared keys are dropped. NaN is ignored.

depth := meter.Gauge("queue.depth", telemetry.Keys("queue"))
depth.Set(ctx, 12, telemetry.String("queue", "default"))

type HeaderCarrier

type HeaderCarrier http.Header

HeaderCarrier adapts http.Header to a Carrier. Keys are canonicalised as HTTP header names are.

telemetry.Default().Inject(ctx, telemetry.HeaderCarrier(request.Header))

func (HeaderCarrier) Get

func (carrier HeaderCarrier) Get(key string) string

Get returns the first value for key, or "". Header lookup uses net/http's case-insensitive canonicalization.

carrier := telemetry.HeaderCarrier(http.Header{})
carrier.Set("Baggage", "region=eu")
fmt.Println(carrier.Get("baggage"))

func (HeaderCarrier) Keys

func (carrier HeaderCarrier) Keys() []string

Keys returns the header names present. The returned slice is independent and its order is unspecified.

carrier := telemetry.HeaderCarrier(http.Header{"Baggage": {"region=eu"}})
keys := carrier.Keys()
slices.Sort(keys)
fmt.Println(keys)

func (HeaderCarrier) Set

func (carrier HeaderCarrier) Set(key, value string)

Set replaces the values for key with value. The underlying header must not be nil.

carrier := telemetry.HeaderCarrier(http.Header{})
carrier.Set("baggage", "region=eu")
fmt.Println(carrier.Get("Baggage"))

type Histogram

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

Histogram records a distribution of values. The zero Histogram is a no-op. Histogram values are in the unit selected when the instrument is declared.

duration := meter.Histogram("duration", telemetry.Unit("s"))
duration.Record(ctx, 0.25)

func (Histogram) Record

func (histogram Histogram) Record(ctx context.Context, value float64, attrs ...Attr)

Record observes value, in the unit declared with Unit. Attributes outside the declared keys are dropped. NaN is ignored.

duration.Record(ctx, time.Since(start).Seconds(), telemetry.String("http.route", route))

type InstrumentKind

type InstrumentKind string

InstrumentKind identifies how an instrument aggregates measurements. Its string values are portable across drivers; the empty value is not a kind.

spec := telemetry.InstrumentSpec{Name: "requests", Kind: telemetry.CounterKind}
const (
	// CounterKind is a monotonically increasing sum.
	//
	//	spec := telemetry.InstrumentSpec{Name: "requests", Kind: telemetry.CounterKind}
	CounterKind InstrumentKind = "counter"

	// UpDownCounterKind is a sum that can also decrease.
	//
	//	spec := telemetry.InstrumentSpec{Name: "active", Kind: telemetry.UpDownCounterKind}
	UpDownCounterKind InstrumentKind = "up_down_counter"

	// HistogramKind is a distribution of observed values.
	//
	//	spec := telemetry.InstrumentSpec{Name: "duration", Kind: telemetry.HistogramKind}
	HistogramKind InstrumentKind = "histogram"

	// GaugeKind is the last observed value.
	//
	//	spec := telemetry.InstrumentSpec{Name: "temperature", Kind: telemetry.GaugeKind}
	GaugeKind InstrumentKind = "gauge"
)

type InstrumentOption

type InstrumentOption func(*InstrumentSpec)

InstrumentOption configures an instrument created by a Meter. Options run synchronously in order on one mutable InstrumentSpec; nil entries panic. After callbacks return, publication copies normalized key and bucket containers independently of the draft, so retaining it cannot change an existing instrument. Strings and other scalar metadata are copied by value. A zero meter or empty positional name creates a no-op without running options.

var option telemetry.InstrumentOption = func(definition *telemetry.InstrumentSpec) {
	definition.Unit = "ms"
}
duration := meter.Histogram("duration", option)
duration.Record(ctx, 12)

func Buckets

func Buckets(bounds ...float64) InstrumentOption

Buckets hints explicit histogram bucket upper bounds. Bounds are sorted and deduplicated, and non-finite bounds are dropped. Drivers may ignore the hint, and it has no effect on other instrument kinds.

duration := meter.Histogram("duration", telemetry.Unit("s"), telemetry.Buckets(0.1, 0.5, 1))
duration.Record(ctx, 0.25)

func Description

func Description(description string) InstrumentOption

Description sets the human-readable help text of the instrument. It is metadata for operators, not a per-measurement label.

requests := meter.Counter("requests", telemetry.Description("Completed cache operations."))
requests.Add(ctx, 1)

func Keys

func Keys(keys ...string) InstrumentOption

Keys declares the label keys of the instrument. Recordings keep only declared keys, in this order; undeclared attributes are dropped and missing keys get the value "". Declaring keys is what bounds cardinality and satisfies backends that need label names up front. Empty and duplicate keys are ignored.

requests := meter.Counter("requests", telemetry.Keys("result"))
requests.Add(ctx, 1, telemetry.String("result", "hit"))

func Unit

func Unit(unit string) InstrumentOption

Unit sets the UCUM unit of the instrument, such as "s", "By", or "{request}".

duration := meter.Histogram("duration", telemetry.Unit("s"))
duration.Record(ctx, 0.25)

type InstrumentRecorder

type InstrumentRecorder interface {
	// Record records one measurement. For counters and up-down counters it
	// is the increment, for histograms the observed value, and for gauges
	// the new value. Counter increments are never negative. values holds one
	// label value per declared key, in declared order, already normalised by
	// the facade; non-string attributes arrive in their [Attr.Text] form.
	// values is only valid during the call, so a driver that keeps it must
	// copy it. ctx lets a driver attach exemplars linked to the current
	// trace.
	//
	//	recorder, err := driver.Instrument(telemetry.InstrumentSpec{
	//		Name: "requests", Kind: telemetry.CounterKind, Keys: []string{"result"},
	//	})
	//	if err != nil {
	//		return err
	//	}
	//	recorder.Record(ctx, 1, []string{"hit"})
	Record(ctx context.Context, value float64, values []string)
}

InstrumentRecorder is the driver side of one instrument. Implementations must be safe for concurrent use. Values are per-recording input, not persistent labels owned by the recorder.

recorder, err := driver.Instrument(telemetry.InstrumentSpec{Name: "requests", Kind: telemetry.CounterKind})
if err != nil {
	return err
}
recorder.Record(ctx, 1, nil)

type InstrumentSpec

type InstrumentSpec struct {
	// Scope is the instrumentation scope of the meter.
	//
	//	spec := telemetry.InstrumentSpec{Scope: "example.com/cache", Name: "requests", Kind: telemetry.CounterKind}
	//	fmt.Println(spec.Scope)
	Scope string

	// Name is the instrument name, written as an OpenTelemetry semantic
	// convention (dots, no unit suffix). Drivers translate it if their
	// backend needs another form.
	//
	//	spec := telemetry.InstrumentSpec{Name: "http.server.requests", Kind: telemetry.CounterKind}
	//	fmt.Println(spec.Name)
	Name string

	// Description is the human-readable help text. It may be empty.
	//
	//	spec := telemetry.InstrumentSpec{
	//		Name: "requests", Kind: telemetry.CounterKind, Description: "Completed cache operations.",
	//	}
	//	fmt.Println(spec.Description)
	Description string

	// Unit is the UCUM unit, such as "s" or "{request}". It may be empty.
	//
	//	spec := telemetry.InstrumentSpec{Name: "duration", Kind: telemetry.HistogramKind, Unit: "s"}
	//	fmt.Println(spec.Unit)
	Unit string

	// Kind is the instrument kind.
	//
	//	spec := telemetry.InstrumentSpec{Name: "inflight", Kind: telemetry.UpDownCounterKind}
	//	fmt.Println(spec.Kind == telemetry.UpDownCounterKind)
	Kind InstrumentKind

	// Keys are the declared label keys, without duplicates or empty names,
	// in declaration order.
	//
	//	spec := telemetry.InstrumentSpec{Name: "requests", Kind: telemetry.CounterKind, Keys: []string{"result"}}
	//	fmt.Println(spec.Keys[0]) // First recording value corresponds to result.
	Keys []string

	// Buckets are the optional explicit histogram bucket upper bounds in
	// ascending order. A driver may ignore the hint; it is only set for
	// histograms.
	//
	//	spec := telemetry.InstrumentSpec{Name: "duration", Kind: telemetry.HistogramKind, Buckets: []float64{0.1, 1}}
	//	fmt.Println(spec.Buckets) // Raw callers supply ascending bounds.
	Buckets []float64
}

InstrumentSpec describes an instrument. Backends that need label names at creation, such as Prometheus, use Keys; others may ignore it. Kind and declared Keys form the facade's schema identity within Scope/Name. Descriptions, units and optional buckets are backend metadata.

spec := telemetry.InstrumentSpec{
	Scope: "example.com/cache", Name: "requests", Kind: telemetry.CounterKind, Keys: []string{"result"},
}
recorder, err := driver.Instrument(spec)
if err != nil {
	return err
}
recorder.Record(ctx, 1, []string{"hit"})

type Instruments

type Instruments[T any] struct {
	// contains filtered or unexported fields
}

Instruments caches the instruments a component builds for one Telemetry, so a component that follows Default at use time neither rebuilds them on every call nor leaks one set per call. Create one per component with NewInstruments and keep it for the component's lifetime. It is safe for concurrent use and must not be copied after first use.

instruments := telemetry.NewInstruments(func(service *telemetry.Telemetry) *telemetry.Tracer {
	tracer := service.Tracer("example.com/cache")
	return &tracer
})
tracer := instruments.For(telemetry.Default())
_ = tracer

func NewInstruments

func NewInstruments[T any](build func(*Telemetry) *T) *Instruments[T]

NewInstruments returns an Instruments whose build function creates the tracer, meters, and instruments of a component for the telemetry it is given. Concurrent misses are serialized: build runs once for the current telemetry, then again only when For resolves a different telemetry. Only the latest value is retained; switching back rebuilds it. build must not call For on the Instruments being built, and must synchronize any shared captured state accessed outside the callback. Callback panics propagate without publication.

instruments := telemetry.NewInstruments(func(service *telemetry.Telemetry) *telemetry.Tracer {
	tracer := service.Tracer("example.com/cache")
	return &tracer
})
tracer := instruments.For(telemetry.Default())
_, span := tracer.Start(ctx, "get")
span.End()

func (*Instruments[T]) For

func (instruments *Instruments[T]) For(t *Telemetry) *T

For returns the value built for t, building it on the first call and when t differs from the previous call. It is safe for concurrent use.

metrics := instruments.For(telemetry.Default())

type Lifecycle

type Lifecycle interface {
	// Start validates configuration and opens connections. The ctx bounds
	// startup only; the driver must not store it.
	//
	//	driver := memorytelemetry.New()
	//	if err := driver.Start(ctx, telemetry.Setup{Resource: telemetry.Resource{Service: "billing"}}); err != nil {
	//		return err
	//	}
	Start(ctx context.Context, setup Setup) error

	// Stop flushes buffered data and releases resources. After it
	// returns the driver is a no-op.
	//
	//	if err := driver.Stop(shutdownContext); err != nil {
	//		return err
	//	}
	Stop(ctx context.Context) error
}

Lifecycle is the part of a driver that every signal driver shares. It has two phases: a pure constructor that only stores configuration, then Start, which validates it and performs I/O.

Every implementation behaves as follows, and telemetrytest.Run verifies it:

  • Before Start and after Stop, every other method of the driver is a harmless no-op: spans are non-recording, recordings are discarded.
  • A second Start returns an error wrapping ErrStarted, and Start after Stop returns an error wrapping ErrStopped.
  • Stop without Start is safe and returns nil. Stop flushes what was recorded, honors ctx, and is idempotent.
  • Methods are safe for concurrent use, including concurrently with Stop.

Typical explicit lifecycle ownership with an in-process driver:

var driver telemetry.Lifecycle = memorytelemetry.New()
if err := driver.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
if err := driver.Stop(ctx); err != nil {
	return err
}

type LogDriver

type LogDriver interface {
	// Lifecycle owns startup and cleanup for all signals supplied by this driver.
	// A log-only driver still participates in normal application lifecycle.
	//
	//	var logs telemetry.LogDriver = memorytelemetry.New()
	//	var lifecycle telemetry.Lifecycle = logs
	//	service := telemetry.New(lifecycle)
	//	fmt.Println(service.Enabled())
	Lifecycle

	// Enabled reports whether Export would accept a record at level. The
	// handler returned by [Telemetry.Handler] uses it to skip work.
	//
	//	if driver.Enabled(ctx, slog.LevelInfo) {
	//		fmt.Println("exporter accepts info records")
	//	}
	Enabled(ctx context.Context, level slog.Level) bool

	// Export sends record to the backend. The record is self-contained:
	// attributes and groups added through WithAttrs and WithGroup are
	// already part of it. The record carries no trace_id or span_id
	// attributes; use [SpanFromContext] on ctx to correlate. The driver must
	// not retain the record without cloning it.
	//
	//	record := slog.NewRecord(time.Time{}, slog.LevelInfo, "operation completed", 0)
	//	if err := driver.Export(ctx, record); err != nil {
	//		return err
	//	}
	Export(ctx context.Context, record slog.Record) error
}

LogDriver is the driver capability for exporting log records. The facade folds logger groups and attributes before Export; the driver does not need the slog handler's construction state to interpret a record.

var logs telemetry.LogDriver = memorytelemetry.New()
service := telemetry.New(logs)
logger := slog.New(service.Handler(slog.DiscardHandler))
_ = logger // Start service before exporting records.

type MapCarrier

type MapCarrier map[string]string

MapCarrier adapts a map to a Carrier, for message and workflow metadata. Keys are stored as given; Get also matches keys that differ only in case, because brokers may change the case of header names.

metadata := telemetry.MapCarrier{}
telemetry.Default().Inject(ctx, metadata)

func (MapCarrier) Get

func (carrier MapCarrier) Get(key string) string

Get returns the value for key, preferring an exact match over a case-insensitive one, or "" when absent.

carrier := telemetry.MapCarrier{"Baggage": "region=eu"}
fmt.Println(carrier.Get("baggage")) // region=eu

func (MapCarrier) Keys

func (carrier MapCarrier) Keys() []string

Keys returns the keys present. Mutating the returned slice does not change the map; sort it for stable output.

carrier := telemetry.MapCarrier{"baggage": "region=eu"}
keys := carrier.Keys()
slices.Sort(keys)
fmt.Println(keys)

func (MapCarrier) Set

func (carrier MapCarrier) Set(key, value string)

Set stores value for key. The map must not be nil. It preserves key spelling; Get still supports a case-insensitive fallback.

carrier := telemetry.MapCarrier{}
carrier.Set("baggage", "region=eu")
fmt.Println(carrier.Get("Baggage"))

type Meter

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

Meter creates instruments for one instrumentation scope. It is a cheap value; create one with Telemetry.Meter. The zero Meter creates no-op instruments.

Create each instrument once, for example in a constructor, and reuse it: creating it declares its labels. Creating the same name twice in a scope shares one backend instrument; if the kind or keys differ, the later instrument is disabled and a warning goes to Setup.Logger.

meter := service.Meter("example.com/cache")
requests := meter.Counter("requests", telemetry.Keys("result"))
requests.Add(ctx, 1, telemetry.String("result", "hit"))

func (Meter) Counter

func (meter Meter) Counter(name string, opts ...InstrumentOption) Counter

Counter creates a monotonically increasing counter.

requests := meter.Counter("http.server.requests", telemetry.Unit("{request}"),
	telemetry.Keys("http.request.method", "http.route"))
requests.Add(ctx, 1, telemetry.String("http.request.method", "GET"), telemetry.String("http.route", "/users/{id}"))

func (Meter) Gauge

func (meter Meter) Gauge(name string, opts ...InstrumentOption) Gauge

Gauge creates an instrument that records the current value of something, such as queue depth.

depth := meter.Gauge("queue.depth", telemetry.Unit("{message}"))
depth.Set(ctx, 12)

func (Meter) Histogram

func (meter Meter) Histogram(name string, opts ...InstrumentOption) Histogram

Histogram creates a distribution instrument, such as a duration in seconds.

duration := meter.Histogram("duration", telemetry.Unit("s"), telemetry.Buckets(0.1, 1))
duration.Record(ctx, 0.25)

func (Meter) UpDownCounter

func (meter Meter) UpDownCounter(name string, opts ...InstrumentOption) UpDownCounter

UpDownCounter creates a counter that can decrease, such as in-flight requests.

active := meter.UpDownCounter("active", telemetry.Unit("{request}"))
active.Add(ctx, 1)
active.Add(ctx, -1)

type MetricDriver

type MetricDriver interface {
	// Lifecycle is shared with any trace or log capabilities on this driver.
	// Start and stop the owner once rather than once per signal.
	//
	//	var metrics telemetry.MetricDriver = memorytelemetry.New()
	//	var lifecycle telemetry.Lifecycle = metrics
	//	service := telemetry.New(lifecycle)
	//	fmt.Println(service.Enabled())
	Lifecycle

	// Instrument creates the recorder for one instrument. The facade calls
	// it once per scope and name for the current binding, after Start, and
	// binds again when an observer-supplied backend changes. An error disables that
	// instrument and is reported to [Setup.Logger]; the other instruments are
	// unaffected. Before Start and after Stop the returned recorder
	// discards recordings.
	//
	// The facade supplies independently owned Keys and Buckets containers;
	// the driver may retain or normalize them without changing facade
	// filtering or observer metadata. Construction must return before the
	// instrument can record; it may use other instrument identities, but must
	// not recursively record the instrument currently being constructed.
	//
	//	recorder, err := driver.Instrument(telemetry.InstrumentSpec{
	//		Scope: "example.com/cache", Name: "requests", Kind: telemetry.CounterKind,
	//	})
	//	if err != nil {
	//		return err
	//	}
	//	recorder.Record(ctx, 1, nil)
	Instrument(spec InstrumentSpec) (InstrumentRecorder, error)
}

MetricDriver is the driver capability for metrics. The facade binds declared instruments after startup and owns portable validation and label filtering; implementations supply recorders and backend-specific aggregation.

var driver telemetry.MetricDriver = memorytelemetry.New()
service := telemetry.New(driver)
meter := service.Meter("example.com/cache") // Start service before recording.
_ = meter

type MiddlewareBuilder

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

MiddlewareBuilder configures Middleware before publication. Callbacks run directly once, synchronously in order on one fresh draft; nil entries panic. Retaining the draft cannot change published settings. Methods return the same draft.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceHeader("X-Trace-ID")
}))

func (*MiddlewareBuilder) ProblemKey

func (middleware *MiddlewareBuilder) ProblemKey(key string) *MiddlewareBuilder

ProblemKey is the RFC 9457 problem extension member that carries the trace ID on error responses. Empty disables it; Middleware defaults to DefaultProblemKey.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.ProblemKey("trace_id")
}))

func (*MiddlewareBuilder) TraceHeader

func (middleware *MiddlewareBuilder) TraceHeader(header string) *MiddlewareBuilder

TraceHeader names an extra response header that carries the trace ID, such as "X-Trace-ID". Empty sets no extra header.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceHeader("X-Trace-ID")
}))

func (*MiddlewareBuilder) TraceResponse

func (middleware *MiddlewareBuilder) TraceResponse(enabled bool) *MiddlewareBuilder

TraceResponse sets the W3C traceresponse header on responses. Middleware defaults to true.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceResponse(false)
}))

func (*MiddlewareBuilder) TrustRemote

func (middleware *MiddlewareBuilder) TrustRemote(trust func(*http.Request) bool) *MiddlewareBuilder

TrustRemote decides, per request, whether the caller's trace context (traceparent, baggage, and whatever else the driver's propagator reads) is extracted and used as the parent of the server span. When it returns false the request starts a new root span, so a browser or any other untrusted client can neither join a trace it does not own nor inject baggage into the application's outgoing calls. The default, nil, is TrustNone, even for loopback and private peers. To preserve the former internal-peer default, explicitly select TrustInternal only when those peers supply trusted context. See TrustAll and MiddlewareBuilder.TrustedNetworks for other explicit policies.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TrustRemote(nil)
}))

func (*MiddlewareBuilder) TrustedNetworks

func (middleware *MiddlewareBuilder) TrustedNetworks(prefixes ...netip.Prefix) *MiddlewareBuilder

TrustedNetworks trusts requests whose RemoteAddr lies in one of prefixes and no others, replacing the default TrustNone. IPv4-mapped IPv6 addresses are matched as IPv4. With no prefixes nothing is trusted. The prefixes are copied. A trusted proxy must strip untrusted incoming context; its address alone does not make browser-supplied headers trustworthy.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

r := router.New()
r.Use(telemetry.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TrustedNetworks(netip.MustParsePrefix("10.0.0.0/8"))
}))

type MiddlewareBuilderFunc

type MiddlewareBuilderFunc = func(*MiddlewareBuilder)

MiddlewareBuilderFunc configures one construction draft before middleware publication. Callbacks run synchronously in order; retained drafts do not change the published settings.

var configure telemetry.MiddlewareBuilderFunc = func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceResponse(false)
}
routes.Use(service.Middleware(configure))

type Observer

type Observer interface {
	// SpanEnded is called once for every span at the moment it ends, after
	// the driver's recorder ended it. span and its slices are never touched
	// again by the facade and are shared by all observers: keep them as they
	// are, but do not modify them.
	// With capture already started, an observer can be called at this seam:
	//
	//	var observer telemetry.Observer = capture
	//	observer.SpanEnded(telemetry.FinishedSpan{
	//		Name: "cache get", Kind: telemetry.Internal, Status: telemetry.Unset,
	//		Context: telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}},
	//	})
	SpanEnded(span FinishedSpan)

	// Logged is called for every record that passes through the handler
	// returned by [Telemetry.Handler] and that the handler's logger emits,
	// that is, when the wrapped handler or the log driver wants its level,
	// whether or not a [LogDriver] is configured. ctx is the context the
	// record was logged with, so [SpanFromContext](ctx).Context() gives the
	// trace and span IDs; the record itself carries no [LogTraceID] or
	// [LogSpanID] attributes and is self-contained (attributes and groups
	// added through WithAttrs and WithGroup are already part of it), exactly
	// what [LogDriver.Export] receives. The observer must not retain record
	// without cloning it.
	// With a started capture, forward one self-contained record:
	//
	//	var observer telemetry.Observer = capture
	//	record := slog.NewRecord(time.Time{}, slog.LevelInfo, "operation completed", 0)
	//	observer.Logged(ctx, record)
	Logged(ctx context.Context, record slog.Record)

	// Measured is called for every recording of every enabled instrument,
	// after the metric driver recorded it. spec is the instrument as the
	// driver sees it and is shared: do not modify it. values holds one label
	// value per declared key, in order, and is only valid during the call.
	// With a started capture, every declared key needs one value:
	//
	//	var observer telemetry.Observer = capture
	//	spec := telemetry.InstrumentSpec{Name: "requests", Kind: telemetry.CounterKind, Keys: []string{"result"}}
	//	observer.Measured(spec, 1, []string{"hit"})
	Measured(spec InstrumentSpec, value float64, values []string)
}

Observer is a passive receiver of everything a Telemetry produces. It is how a component that is not the telemetry backend, such as the inspector, keeps its own copy of spans, logs, and measurements: whatever the real driver records, the observer sees too, with the same trace and span IDs, because the facade hands it the calls made through Arkoren instrumentation. Finished-span timing and backend-selected parent metadata are described by FinishedSpan; observation is not a native SDK export stream.

Attach an observer with Telemetry.Observe. An observer is a single interface on purpose: one that wants one signal ignores the other two methods. Every method is called synchronously on the goroutine that produced the signal, possibly from many goroutines at once, so implementations must be safe for concurrent use, fast, and must neither block nor panic. They must not call back into the observed Telemetry in a way that would produce another signal from the same method.

An observer that also implements TraceDriver or MetricDriver can fill in for a signal the telemetry has no driver for; see Telemetry.Observe.

capture := memorytelemetry.New()
if err := capture.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
unsubscribe := service.Observe(capture)
_, span := service.Tracer("example.com/cache").Start(ctx, "cache get")
span.End()
unsubscribe()
if err := capture.Stop(ctx); err != nil {
	return err
}

type Resource

type Resource struct {
	// Service is the logical service name. When empty a driver falls back to
	// its native configuration, such as OTEL_SERVICE_NAME.
	//
	//	setup := telemetry.Setup{Resource: telemetry.Resource{Service: "billing"}}
	//	fmt.Println(setup.Resource.Service)
	Service string

	// Version is the service version.
	// Use release metadata rather than request-specific values.
	//
	//	resource := telemetry.Resource{Service: "billing", Version: "1.2.3"}
	//	fmt.Println(resource.Version)
	Version string

	// Environment is the deployment environment, such as production.
	// It describes the emitting deployment, not the caller's environment.
	//
	//	resource := telemetry.Resource{Service: "billing", Environment: "staging"}
	//	fmt.Println(resource.Environment)
	Environment string

	// Attrs are extra resource attributes.
	// Keep them process-scoped and free of secrets or personal identifiers.
	//
	//	resource := telemetry.Resource{Attrs: []telemetry.Attr{telemetry.String("region", "eu")}}
	//	fmt.Println(resource.Attrs[0].Text())
	Attrs []Attr
}

Resource describes the process that emits telemetry. Drivers map it onto their backend's resource model. Resource values describe one process, not an individual request or user.

resource := telemetry.Resource{Service: "billing", Version: "1.2.3", Environment: "production"}
setup := telemetry.Setup{Resource: resource}
fmt.Println(setup.Resource.Service)

type Selection

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

Selection assigns a driver to one signal in Compose. Create one with Traces, Metrics, or Logs. A zero selection contributes no driver; later selections override one signal.

selection := telemetry.Metrics(memorytelemetry.New())
service := telemetry.New(telemetry.Compose(selection))
fmt.Println(service.Enabled()) // false until Start

func Logs

func Logs(driver LogDriver) Selection

Logs selects driver for log export. A nil driver selects nothing. This does not change a caller's ordinary slog handler or its local output.

logs := memorytelemetry.New()
service := telemetry.New(telemetry.Compose(telemetry.Logs(logs)))
logger := slog.New(service.Handler(slog.DiscardHandler))
_ = logger // Start service before exporting records.

func Metrics

func Metrics(driver MetricDriver) Selection

Metrics selects driver for metrics. A nil driver selects nothing. Trace and log routing are unchanged by this selection.

metrics := memorytelemetry.New()
service := telemetry.New(telemetry.Compose(telemetry.Metrics(metrics)))
fmt.Println(service.Enabled()) // false until Start

func Traces

func Traces(driver TraceDriver) Selection

Traces selects driver for traces and propagation. A nil driver selects nothing.

traces := memorytelemetry.New()
service := telemetry.New(telemetry.Compose(telemetry.Traces(traces)))
fmt.Println(service.Enabled()) // false until Start

type Setup

type Setup struct {
	// Resource describes the emitting process.
	//
	//	setup := telemetry.Setup{Resource: telemetry.Resource{Service: "billing"}}
	//	fmt.Println(setup.Resource.Service)
	Resource Resource

	// Logger is the application's unwrapped logger, for reporting exporter
	// failures. It is not wrapped by [Telemetry.Handler], so errors logged
	// through it are never exported again. Nil means "discard".
	//
	//	setup := telemetry.Setup{Logger: slog.New(slog.DiscardHandler)}
	//	setup.Logger.ErrorContext(ctx, "exporter unavailable")
	Logger *slog.Logger
}

Setup is everything a driver receives in Lifecycle.Start. It is a struct so fields can be added without breaking drivers. Supply it at startup; changing a caller's copy does not reconfigure a driver.

setup := telemetry.Setup{
	Resource: telemetry.Resource{Service: "billing"}, Logger: slog.New(slog.DiscardHandler),
}
if err := service.Start(ctx, setup); err != nil {
	return err
}

type Span

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

Span is one timed operation in a trace. A nil *Span and the span of a disabled telemetry are valid and do nothing. A Span is safe for concurrent use and must not be copied after first use. End waits for admitted recorder calls before ending the backend; later calls are no-ops.

_, span := tracer.Start(ctx, "operation")
span.SetAttributes(telemetry.String("region", "eu"))
span.End()

func SpanFromContext

func SpanFromContext(ctx context.Context) *Span

SpanFromContext returns the current span of ctx. It never returns nil: when ctx has no span it returns a non-recording span with an invalid SpanContext, on which every method is a no-op.

telemetry.SpanFromContext(ctx).SetAttributes(telemetry.String("tenant", id))

func (*Span) AddEvent

func (span *Span) AddEvent(name string, attrs ...Attr)

AddEvent records a timestamped event with attrs. Invalid attributes are dropped and the input container is copied for the recorder. It is a no-op after End or when the owning telemetry is stopped.

span.AddEvent("retry", telemetry.Int("attempt", 2))

func (*Span) Context

func (span *Span) Context() SpanContext

Context returns the span identity. It is invalid for a no-op span. For a span adopted with ContextWithSpanContext or Telemetry.Extract it is the adopted identity. Reading the identity does not record attributes or reactivate an ended span.

identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}}
ctx := telemetry.ContextWithSpanContext(context.Background(), identity)
fmt.Println(telemetry.SpanFromContext(ctx).Context().SpanID.String())

func (*Span) End

func (span *Span) End()

End finishes the span. Only the first call has an effect, and the span ignores every call after it. The first call waits for recorder operations already in progress, without holding facade locks across backend or observer callbacks. Backends must return from their callbacks; a recorder callback must not synchronously call End on its own facade span.

_, span := tracer.Start(ctx, "operation")
span.End()

func (*Span) IsRecording

func (span *Span) IsRecording() bool

IsRecording reports whether the span keeps attributes and events. Check it before computing expensive attribute values. It is false after End is requested or the owning telemetry stops; no recorder call follows backend End.

if span.IsRecording() {
	span.SetAttributes(telemetry.String("region", "eu"))
}

func (*Span) RecordError

func (span *Span) RecordError(err error)

RecordError records err on the span as an exception event. It does not change the status; call Span.SetStatus for that. A nil err is ignored.

Record only unexpected errors, and never put secrets or personal data in error text: the text is exported. Error formatting may record additional attributes, but must not synchronously end the same span from its recorder callback. End waits for admitted error recording to return.

if err != nil {
	span.RecordError(err)
	span.SetStatus(telemetry.Error, "operation failed")
}

func (*Span) SetAttributes

func (span *Span) SetAttributes(attrs ...Attr)

SetAttributes adds attributes to the span. Invalid attributes are dropped and a later attribute with the same key replaces the earlier one. The input container is copied before a recorder may retain it. End and stopped telemetry suppress new operations; concurrent admitted calls finish before backend End.

span.SetAttributes(telemetry.String("region", "eu"))

func (*Span) SetStatus

func (span *Span) SetStatus(code StatusCode, description string)

SetStatus sets the span status. Unset is ignored, OK is final, and Error replaces Unset and an earlier Error. The description is kept only for Error; keep it short and free of personal data. Status updates are ignored after End or after the owning telemetry stops.

span.SetStatus(telemetry.Error, "backend unavailable")

type SpanContext

type SpanContext struct {
	// TraceID is the trace the span belongs to.
	// Both this ID and SpanID must be nonzero for IsValid to return true.
	//
	//	identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}}
	//	fmt.Println(identity.TraceID.String())
	TraceID TraceID

	// SpanID is the span itself.
	// It identifies one operation within TraceID, not a globally unique trace.
	//
	//	identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}}
	//	fmt.Println(identity.SpanID.String())
	SpanID SpanID

	// Sampled reports whether the trace is being recorded.
	// It is a propagation flag, not a promise that a particular recorder keeps data.
	//
	//	identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, Sampled: true}
	//	fmt.Println(identity.Sampled)
	Sampled bool

	// Remote reports that the context was extracted from a carrier, so the
	// span lives in another process.
	// It conveys origin, not authorization to trust the sender.
	//
	//	identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, Remote: true}
	//	ctx := telemetry.ContextWithSpanContext(context.Background(), identity)
	//	fmt.Println(telemetry.SpanFromContext(ctx).Context().Remote)
	Remote bool

	// TraceState is the opaque W3C tracestate that was propagated with the
	// context. Drivers that do not use it pass it through unchanged.
	//
	//	identity := telemetry.SpanContext{
	//		TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, TraceState: "vendor=value",
	//	}
	//	fmt.Println(identity.TraceState)
	TraceState string
}

SpanContext identifies a span and carries the propagated trace flags. It is an immutable value that is safe to copy. Validity requires both IDs; Remote and Sampled do not establish trust or turn an adopted identity into a recorder.

identity := telemetry.SpanContext{
	TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, Remote: true,
}
fmt.Println(identity.IsValid()) // true

func (SpanContext) IsValid

func (spanContext SpanContext) IsValid() bool

IsValid reports whether both IDs are non-zero. It does not validate a remote sender, sampling decision, or the opaque vendor-specific TraceState text.

identity := telemetry.SpanContext{TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}}
fmt.Println(identity.IsValid()) // true

type SpanEvent

type SpanEvent struct {
	// Name is the event name.
	// Use an operation-level label, not payload text.
	//
	//	event := telemetry.SpanEvent{Name: "retry"}
	//	fmt.Println(event.Name)
	Name string

	// Time is when the event was recorded.
	// It is an observed timestamp rather than the time a backend exported it.
	//
	//	event := telemetry.SpanEvent{Name: "retry", Time: time.Unix(0, 0)}
	//	fmt.Println(event.Time.UTC().Format(time.RFC3339))
	Time time.Time

	// Attrs are the event attributes.
	// The Attr values are immutable; observer code must not edit the shared slice.
	//
	//	event := telemetry.SpanEvent{Name: "retry", Attrs: []telemetry.Attr{telemetry.Int("attempt", 2)}}
	//	fmt.Println(event.Attrs[0].AsInt64())
	Attrs []Attr
}

SpanEvent is a timestamped annotation on a FinishedSpan. Attribute containers in observer notifications are shared read-only, like the containing finished span; copy containers before constructing an edited view.

event := telemetry.SpanEvent{
	Name: "retry", Time: time.Unix(0, 0), Attrs: []telemetry.Attr{telemetry.Int("attempt", 2)},
}
fmt.Println(event.Name, event.Attrs[0].AsInt64())

type SpanID

type SpanID [8]byte

SpanID identifies a span within a trace as 8 bytes. The all-zero value is invalid; span IDs are meaningful together with their owning TraceID.

id := telemetry.SpanID{1}
fmt.Println(id.IsZero()) // false

func (SpanID) IsZero

func (spanID SpanID) IsZero() bool

IsZero reports whether every byte is zero. A nonzero span ID still needs a nonzero trace ID before its containing SpanContext is valid.

fmt.Println((telemetry.SpanID{}).IsZero()) // true

func (SpanID) String

func (spanID SpanID) String() string

String returns all 8 bytes as 16 lowercase hexadecimal characters, including leading zeros. It also formats an invalid zero identity without failing.

id := telemetry.SpanID{1}
fmt.Println(id.String()) // 0100000000000000

type SpanKind

type SpanKind string

SpanKind describes the role of a span in a trace. Empty kinds select Internal when starting a span; strings do not encode SDK ordinals and must be mapped explicitly by native drivers.

option := telemetry.Kind(telemetry.Server)
const (
	// Internal is an operation inside the application. It is the default.
	//
	//	option := telemetry.Kind(telemetry.Internal)
	Internal SpanKind = "internal"

	// Server handles an incoming remote request.
	//
	//	option := telemetry.Kind(telemetry.Server)
	Server SpanKind = "server"

	// Client makes an outgoing remote request.
	//
	//	option := telemetry.Kind(telemetry.Client)
	Client SpanKind = "client"

	// Producer hands a message to a broker for later processing.
	//
	//	option := telemetry.Kind(telemetry.Producer)
	Producer SpanKind = "producer"

	// Consumer processes a message received from a broker.
	//
	//	option := telemetry.Kind(telemetry.Consumer)
	Consumer SpanKind = "consumer"
)

type SpanRecorder

type SpanRecorder interface {
	// Context returns the identity of the span. It is invalid for a
	// non-recording span that was started before Start.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	fmt.Println(recorder.Context().IsValid())
	//	recorder.End()
	Context() SpanContext

	// Parent returns the actual parent selected by the backend at StartSpan,
	// not a guess from the facade's incoming context. Root and inactive spans
	// return the zero SpanContext. The result is fixed for the recorder's
	// lifetime and preserves the selected parent's flags and TraceState.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	if parent := recorder.Parent(); parent.IsValid() {
	//		fmt.Println(parent.SpanID.String())
	//	}
	//	recorder.End()
	Parent() SpanContext

	// Recording reports whether attributes and events are being kept. It is
	// false for spans that are only used for their IDs.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	if recorder.Recording() {
	//		recorder.AddEvent("hit", nil)
	//	}
	//	recorder.End()
	Recording() bool

	// SetAttributes adds attributes. A later attribute with the same key
	// replaces the earlier value.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	recorder.SetAttributes([]telemetry.Attr{telemetry.Bool("cached", true)})
	//	recorder.End()
	SetAttributes(attrs []Attr)

	// AddEvent records a timestamped event.
	// The facade supplies validated, owned attributes when it calls this method.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	recorder.AddEvent("retry", []telemetry.Attr{telemetry.Int("attempt", 2)})
	//	recorder.End()
	AddEvent(name string, attrs []Attr)

	// RecordError records err as an event named "exception" with the
	// attributes exception.type (the Go type of err) and exception.message.
	// It does not change the status.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	recorder.RecordError(errors.New("backend unavailable"))
	//	recorder.SetStatus(telemetry.Error, "operation failed")
	//	recorder.End()
	RecordError(err error)

	// SetStatus sets the status. [Unset] is ignored. [OK] is final: later
	// calls are ignored. [Error] replaces [Unset] and an earlier [Error]
	// status and its description. The description is kept only for [Error].
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	recorder.SetStatus(telemetry.Error, "backend unavailable")
	//	recorder.End()
	SetStatus(code StatusCode, description string)

	// End finishes the span and hands it to the backend.
	// The facade waits for admitted recorder operations before calling it once.
	//
	//	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
	//	recorder.End()
	End()
}

SpanRecorder is the driver side of one span. The facade calls End at most once and no other method after End. Implementations must be safe for concurrent use. Context and Parent are immutable metadata captured when the backend starts the span, including for an unsampled span that keeps only IDs.

Custom recorder migration: implement Parent() SpanContext on every recorder and decorator. Capture the effective parent before the backend creates its child; return zero for a root or inactive recorder. Forward the method through wrappers. Do not infer it later from caller context or from the child's ID. This is a required method, not an optional capability; JSON field names do not change.

_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get"})
parent := recorder.Parent()
_ = parent.IsValid()
recorder.End()
Example

ExampleSpanRecorder exercises the complete raw protocol, including the required actual-parent accessor, with an in-process backend and fixed parent.

package main

import (
	"context"
	"errors"
	"fmt"

	"arkoren.dev/core/service/telemetry"
	"arkoren.dev/core/service/telemetry/memorytelemetry"
)

func main() {
	ctx := context.Background()
	driver := memorytelemetry.New()

	if err := driver.Start(ctx, telemetry.Setup{}); err != nil {
		fmt.Println(err)

		return
	}

	parent := telemetry.SpanContext{
		TraceID: telemetry.TraceID{1}, SpanID: telemetry.SpanID{2}, Remote: true,
	}
	ctx = telemetry.ContextWithSpanContext(ctx, parent)
	_, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get", Kind: telemetry.Client})
	fmt.Println(recorder.Context().IsValid(), recorder.Parent() == parent)

	if recorder.Recording() {
		recorder.SetAttributes([]telemetry.Attr{telemetry.String("region", "eu")})
		recorder.AddEvent("retry", []telemetry.Attr{telemetry.Int("attempt", 2)})
		recorder.RecordError(errors.New("backend unavailable"))
		recorder.SetStatus(telemetry.Error, "operation failed")
	}

	recorder.End()

	if err := driver.Stop(ctx); err != nil {
		fmt.Println(err)

		return
	}

	span := driver.Spans()[0]
	fmt.Println(span.Parent == parent, span.Status, len(span.Events))
}
Output:
true true
true error 2

type SpanStart

type SpanStart struct {
	// Scope is the instrumentation scope of the tracer, such as
	// "arkoren.dev/core/service/cache".
	//
	//	start := telemetry.SpanStart{Scope: "example.com/cache", Name: "cache get"}
	//	fmt.Println(start.Scope)
	Scope string

	// Name is the span name. It must have low cardinality.
	// Put request-specific values in attributes rather than the name.
	//
	//	start := telemetry.SpanStart{Name: "cache get"}
	//	fmt.Println(start.Name)
	Name string

	// Kind is the span kind. An empty value selects [Internal]; the facade
	// supplies that named default before calling the driver.
	//
	//	start := telemetry.SpanStart{Name: "cache get", Kind: telemetry.Client}
	Kind SpanKind

	// Attrs are the initial attributes. They are valid and owned by the
	// driver.
	//
	//	start := telemetry.SpanStart{Name: "cache get", Attrs: []telemetry.Attr{telemetry.Bool("cached", true)}}
	//	fmt.Println(start.Attrs[0].AsBool())
	Attrs []Attr
}

SpanStart describes a span to start. Drivers receive normalized kind and independently owned attribute containers from the facade; raw driver callers supply their own valid metadata.

start := telemetry.SpanStart{Scope: "example.com/cache", Name: "cache get", Kind: telemetry.Client}
_, recorder := driver.StartSpan(ctx, start)
recorder.End()

type StartOption

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

StartOption configures a span started with Tracer.Start. It owns its attribute containers and is safe to reuse concurrently. Passing an existing option to a disabled tracer does not allocate; constructing Attrs with more than one attribute copies its slice.

option := telemetry.Attrs(telemetry.String("region", "eu"))
_, span := tracer.Start(ctx, "operation", option)
span.End()

func Attrs

func Attrs(attrs ...Attr) StartOption

Attrs sets the initial span attributes. Prefer it to calling Span.SetAttributes right after Start: samplers see initial attributes. The option copies the slice, so later caller edits do not change it. Attr values themselves are immutable and can be shared. Empty input adds nothing; invalid attributes are filtered when the span starts.

tracer.Start(ctx, "charge", telemetry.Attrs(telemetry.String("currency", "EUR")))

func Kind

func Kind(kind SpanKind) StartOption

Kind sets the span kind. The default is Internal.

tracer.Start(ctx, "GET /users", telemetry.Kind(telemetry.Server))

type StatusCode

type StatusCode string

StatusCode is the outcome of a span. Recorded spans start with Unset. Empty or unknown codes passed to SetStatus have no effect, like Unset, and cannot clear an existing verdict.

span.SetStatus(telemetry.Error, "backend unavailable")
const (
	// Unset is the default status: the span has no verdict.
	//
	//	pending := recorded.Status == telemetry.Unset
	Unset StatusCode = "unset"

	// OK marks the operation as successful. It is final.
	//
	//	span.SetStatus(telemetry.OK, "")
	OK StatusCode = "ok"

	// Error marks the operation as failed.
	//
	//	span.SetStatus(telemetry.Error, "backend unavailable")
	Error StatusCode = "error"
)

type Telemetry

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

Telemetry is the facade over a telemetry driver. It owns the behavior that must be identical for every driver: the single lifecycle, attribute validation, declared-key filtering for metrics, and a cheap non-recording fast path. Drivers stay small.

Signals the driver does not implement fall back to a no-op, unless an observer fills in (see Telemetry.Observe). Until Start succeeds, and after Stop, everything is a no-op, so instrumentation can be written unconditionally and components can be built before the application starts.

A Telemetry is safe for concurrent use and must not be copied after first use. Obtain the application's already-started service for instrumentation:

service := app.Telemetry()
_, span := service.Tracer("example.com/billing").Start(ctx, "charge")
span.End()

func Default

func Default() *Telemetry

Default returns the process-wide Telemetry, in the manner of log/slog.Default. Until SetDefault is called it is a no-op: spans are non-recording, no IDs are generated, and the hot path does not allocate.

Components resolve the default when they use it, not when they are built, so calling SetDefault after building them still works.

ctx, span := telemetry.Default().
	Tracer("example.com/billing").
	Start(ctx, "charge")
defer span.End()

func New

func New(driver Lifecycle) *Telemetry

New returns a Telemetry over driver. The driver's capabilities are found by type assertion: implement TraceDriver, MetricDriver, and LogDriver as needed, or pass the result of Compose to mix drivers per signal. A nil driver gives a no-op Telemetry.

New performs no I/O; call Telemetry.Start before use. The application does it for you.

service := telemetry.New(memorytelemetry.New())
if err := service.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
_, span := service.Tracer("example.com/billing").Start(ctx, "charge")
span.End()
if err := service.Stop(ctx); err != nil {
	return err
}

func (*Telemetry) Driver

func (telemetry *Telemetry) Driver() Lifecycle

Driver returns the driver given to New, or nil for a no-op Telemetry. Direct driver calls bypass facade validation and observation. Prefer facade operations unless native integration specifically needs the lifecycle value.

store := memorytelemetry.New()
service := telemetry.New(store)
if driver, ok := service.Driver().(*memorytelemetry.Driver); ok {
	fmt.Println(len(driver.Spans()))
}

func (*Telemetry) DriverAs

func (telemetry *Telemetry) DriverAs[T any]() (T, bool)

DriverAs returns the first driver of type T behind telemetry. A driver that wraps another exposes it with an Unwrap() Lifecycle method, and a [Compose]d driver is searched part by part.

if memory, ok := t.DriverAs[*memorytelemetry.Driver](); ok {
	_ = memory.Spans()
}

func (*Telemetry) Enabled

func (telemetry *Telemetry) Enabled() bool

Enabled reports whether telemetry is started and so records signals. It is false for the no-op default, before Start, and after Stop. Instrumented components check it first to skip all work, including building attributes, when nothing would be recorded. A nil receiver is not enabled.

if !telemetry.Default().Enabled() {
	return run()
}

func (*Telemetry) Extract

func (telemetry *Telemetry) Extract(ctx context.Context, carrier Carrier) context.Context

Extract reads propagation headers from carrier and returns a context whose current span is the remote parent, so the next span started from it continues the remote trace. Absent or malformed headers return ctx unchanged. This low-level operation performs no peer authentication; HTTP middleware applies its trust predicate before invoking it for an inbound request.

ctx := service.Extract(ctx, carrier)
_, span := service.Tracer("example.com/worker").Start(ctx, "process")
span.End()

func (*Telemetry) Handler

func (telemetry *Telemetry) Handler(next slog.Handler) slog.Handler

Handler wraps next so that records logged with a context holding a valid span get LogTraceID and LogSpanID attributes, and, when the driver implements LogDriver, are also exported to it. Every record the logger emits, meaning next or the log driver wants its level, is also handed to the attached [Observer]s (see Telemetry.Observe) with the logging context, whether or not a log driver is configured; they get the same self-contained record as the driver, without the correlation attributes. Observers only see records while the telemetry is enabled.

Correlation needs the context-aware slog methods (InfoContext and so on); plain Info passes context.Background(). The attributes are added to the record, so under an open group (WithGroup) they appear inside it.

The returned handler follows the slog.Handler contract: Enabled is true when next or the exporter wants the level, and WithAttrs and WithGroup derive handlers that keep the correlation and export behavior. A nil next discards records locally. The handler resolves nothing at construction, so it may be built before Start; export begins when the Telemetry starts.

logger := slog.New(t.Handler(slog.NewJSONHandler(os.Stderr, nil)))
logger.InfoContext(ctx, "invoice sent") // ... "trace_id":"4bf9...","span_id":"00f0..."

func (*Telemetry) Inject

func (telemetry *Telemetry) Inject(ctx context.Context, carrier Carrier)

Inject writes the propagation headers of the current span of ctx into carrier, in the driver's format (W3C Trace Context for the memory driver). It does nothing without a started trace driver (or filling-in observer, see Telemetry.Observe) or a valid span. Initialize mutable carriers before use; propagation alone does not authenticate a destination or scrub baggage. Apply the application's boundary policy first.

ctx, span := service.Tracer("example.com/cache").Start(ctx, "cache get")
carrier := telemetry.MapCarrier{}
service.Inject(ctx, carrier)
span.End()
fmt.Println(carrier.Get("traceparent"))

func (*Telemetry) Meter

func (telemetry *Telemetry) Meter(scope string) Meter

Meter returns a Meter for the instrumentation scope. Instrument declarations are cheap and bind to the active backend on use.

meter := service.Meter("example.com/billing")
charges := meter.Counter("charges", telemetry.Keys("currency"))
charges.Add(ctx, 1, telemetry.String("currency", "EUR"))

func (*Telemetry) Middleware

func (telemetry *Telemetry) Middleware(configure ...MiddlewareBuilderFunc) router.Middleware

Middleware returns middleware that traces and measures requests with this instance.

It extracts the remote parent through the driver's propagator, but only from callers that MiddlewareBuilder.TrustRemote trusts. The default, including nil, is TrustNone: all requests start a new root span, so a caller cannot choose the trace ID, force sampling, or smuggle baggage into the application's outgoing calls through incoming headers. This replaces the former TrustInternal default. Opt in to TrustInternal, MiddlewareBuilder.TrustedNetworks, or TrustAll only for trusted workloads. A private proxy can forward browser-supplied context unchanged; trusting its address does not authenticate that context. Keep TrustNone at a public edge unless the proxy strips untrusted context. X-Forwarded-For is never consulted. Outgoing propagation policy is independent and unchanged.

The middleware then starts a server span named "<METHOD> <route>" (the method alone when the request matched no route). Unknown methods use "HTTP" in the name and "_OTHER" in http.request.method, as required by the HTTP span naming conventions. It records http.server.request.duration and http.server.active_requests. The span and metric finish after Arkoren has rendered returned errors, and also when a panic is recovered, so the status is the one sent to the client. Only 5xx responses mark the span as an error, with the description "HTTP <code>" and error.type set to the status code.

Only low-cardinality method, scheme, protocol, route, and status values are recorded. URLs, query strings, headers, bodies, and error text never are.

When the span context is valid the middleware sets the W3C traceresponse header (see MiddlewareBuilder.TraceResponse), the optional MiddlewareBuilder.TraceHeader header, and the MiddlewareBuilder.ProblemKey property on problem responses. With a no-op or not-yet-started telemetry the middleware passes requests straight through and sets nothing.

Register it before other middleware, including router.Recover, so it observes the complete request. Enabled instrumentation requires the router's lifecycle hooks: invoking the wrapped handler directly without that context panics before starting a span or recording an active request.

Example:

t := app.Telemetry()
r.Use(t.Middleware(func(middleware *telemetry.MiddlewareBuilder) {
	middleware.TraceHeader("X-Trace-ID")
}))

func (*Telemetry) Observe

func (telemetry *Telemetry) Observe(observer Observer) (unsubscribe func())

Observe attaches observer for as long as the returned function has not been called. It is a runtime subscription, like event listeners, not a setter: any number of observers can be attached and detached while traffic flows, and the observed Telemetry and its configured drivers are unchanged. It is safe for concurrent use. The returned unsubscribe function is idempotent and safe to call concurrently; a nil observer attaches nothing.

Notifications are admitted only while telemetry is enabled (see Telemetry.Enabled). An admitted callback may complete after Stop; Stop neither waits for it nor cancels it. Spans that started before Observe are not delivered, since their capture begins at start; spans that started while observed are delivered even if the observer detached meanwhile, provided telemetry is still enabled when completion delivery is admitted. Stop suppresses deliveries not yet admitted. Noncomparable driver values in separate registrations have no shared value identity; use pointers to share one backend across roles. With no observer attached the cost on every hot path is one atomic load.

Filling in missing drivers

When telemetry has no TraceDriver (or no MetricDriver) and an attached observer also implements it, that observer acts as the driver for the signal while it stays attached: spans get IDs and a recorder, Telemetry.Inject and Telemetry.Extract propagate, trace IDs appear in logs and in Middleware responses, all with no other telemetry configured. If several observers qualify, the first one attached wins; others still observe. The observer in the driver role is not also called through Observer, so the memory driver records every span and measurement exactly once. When the last qualifying observer detaches the signal reverts to no-op; spans already started finish on the recorder they started with. A configured driver is never replaced.

Observe neither starts nor stops observers: a driver-capable observer (such as the memory driver) must be started by its owner to record. The facade itself must be started, which also means a no-op New(nil) must be started to be observed. The initial shared Default begins unstarted; Observe alone does not enable it. Prefer an explicitly started New(nil) facade for observer-only telemetry, then publish it with SetDefault if needed.

With service already started, the observer's owner starts and stops its own capture independently:

memory := memorytelemetry.New()
if err := memory.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
unsubscribe := service.Observe(memory)
_, span := service.Tracer("example.com/cache").Start(ctx, "get")
span.End()
unsubscribe()
if err := memory.Stop(ctx); err != nil {
	return err
}

func (*Telemetry) Start

func (telemetry *Telemetry) Start(ctx context.Context, setup Setup) error

Start starts the driver with setup. It returns an error wrapping ErrStarted when already started and ErrStopped after Stop or a failed Start. ctx is passed to backend startup only and is not retained. Starting a Telemetry with no driver succeeds and makes Enabled true, allowing attached observers to receive signals; absent signals still do nothing. Lifecycle calls are serialized. A backend must return from its callbacks and must not synchronously reenter Start or Stop on the same facade; ctx does not interrupt waiting for an earlier lifecycle call to finish.

service := telemetry.New(nil)
if err := service.Start(ctx, telemetry.Setup{}); err != nil {
	return err
}
fmt.Println(service.Enabled()) // true, even without a backend
Example

ExampleTelemetry_Start demonstrates the observable enabled state of a facade with no backend. Such a facade can later accept passive observers.

package main

import (
	"context"
	"fmt"

	"arkoren.dev/core/service/telemetry"
)

func main() {
	ctx := context.Background()
	service := telemetry.New(nil)

	if err := service.Start(ctx, telemetry.Setup{}); err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println(service.Enabled())

	if err := service.Stop(ctx); err != nil {
		fmt.Println(err)

		return
	}

	fmt.Println(service.Enabled())
}
Output:
true
false

func (*Telemetry) Stop

func (telemetry *Telemetry) Stop(ctx context.Context) error

Stop stops recording, then shuts the driver down so it can flush. It is safe without Start, safe to call twice, and returns nil in both cases. After it returns, Telemetry is a no-op for good. ctx is forwarded unchanged to the backend, including cancellation. Stop does not forcibly interrupt backend or observer callbacks, wait for observer notifications already admitted, or interrupt an earlier serialized lifecycle call. Drivers own resource release and must return from Stop; applications should give exporter flushing its own shutdown budget rather than reuse an expired startup budget.

if err := service.Stop(shutdownContext); err != nil {
	return err
}

func (*Telemetry) Tracer

func (telemetry *Telemetry) Tracer(scope string) Tracer

Tracer returns a Tracer for the instrumentation scope, such as "arkoren.dev/core/service/cache" or the importing package's path. It is cheap; resolve it where it is used.

tracer := service.Tracer("example.com/billing")
_, span := tracer.Start(ctx, "charge")
span.End()

func (*Telemetry) Transport

func (telemetry *Telemetry) Transport(base http.RoundTripper, configure ...TransportBuilderFunc) http.RoundTripper

Transport returns a round tripper that traces and measures outgoing requests with this instance and delegates them to base. A nil base uses http.DefaultTransport.

Each request gets a client span named after the method, parented to the span in the request context, and the driver's propagation headers (such as traceparent) are injected into a clone of the request; the caller's request is never modified. Headers are injected only when TransportBuilder.Propagation allows the destination; the default, PropagateInternal, skips third parties so trace IDs and baggage do not leak, while the span and metric are still recorded for every host. Add partners with TransportBuilder.PropagationHosts or inject everywhere with PropagateAll. The span ends when the response body is read to the end or closed, so its duration covers reading the body; callers must close the body, as net/http already requires. A transport error or a 5xx response marks the span as an error; error.type is the status code or the Go type name of the error, never its text. Unknown methods use "HTTP" as the span name and "_OTHER" as the bounded http.request.method attribute; no request path becomes a span name.

A request resent by arkoren.dev/core/http/client also carries http.request.resend_count on its span.

Only the method, scheme, host, port, and status are recorded. Paths, query strings, headers, and bodies never are. With a no-op or not-yet-started telemetry requests pass straight to base.

Example:

client := &http.Client{Transport: app.Telemetry().Transport(nil)}

type TraceDriver

type TraceDriver interface {
	// Lifecycle is the one shared lifecycle for this driver's signals.
	// Passing a trace driver to New does not create a second lifecycle owner.
	//
	//	var traces telemetry.TraceDriver = memorytelemetry.New()
	//	var lifecycle telemetry.Lifecycle = traces
	//	service := telemetry.New(lifecycle)
	//	fmt.Println(service.Enabled())
	Lifecycle

	// StartSpan starts a span using the backend's parent-selection rules,
	// which may prefer native context over [SpanFromContext](ctx).Context().
	// Its recorder reports the actual decision through Parent. A driver
	// that wraps a native tracer returns a context
	// carrying the native span so native instrumentation parents correctly;
	// otherwise it returns ctx unchanged. The facade stores the returned
	// recorder's identity in the context itself.
	//
	// Before Start and after Stop it returns ctx and a non-recording
	// recorder with an invalid [SpanContext].
	//
	//	ctx, recorder := driver.StartSpan(ctx, telemetry.SpanStart{Name: "cache get", Kind: telemetry.Client})
	//	fmt.Println(recorder.Context().IsValid(), recorder.Parent().IsValid())
	//	recorder.End()
	//	_ = ctx
	StartSpan(ctx context.Context, start SpanStart) (context.Context, SpanRecorder)

	// Inject writes the propagation headers of the current span of ctx into
	// carrier. It writes nothing when ctx has no valid span.
	//
	//	carrier := telemetry.MapCarrier{}
	//	driver.Inject(ctx, carrier)
	//	_ = carrier.Get("traceparent")
	Inject(ctx context.Context, carrier Carrier)

	// Extract reads propagation headers from carrier. It returns the context
	// to continue with (a driver with native context state puts it there;
	// otherwise ctx unchanged) and the extracted remote [SpanContext], which
	// has Remote set. Absent or malformed headers yield ctx and the zero
	// [SpanContext]; Extract never fails.
	//
	//	ctx, parent := driver.Extract(ctx, carrier)
	//	if parent.IsValid() {
	//		fmt.Println(parent.TraceID.String())
	//	}
	//	_ = ctx
	Extract(ctx context.Context, carrier Carrier) (context.Context, SpanContext)
}

TraceDriver is the driver capability for traces and propagation.

The facade (Tracer, Span) validates attributes and keeps the fast path for disabled telemetry, so a driver only sees valid, owned data. Parent selection belongs to the backend; the returned recorder reports the actual decision through SpanRecorder.Parent.

var traces telemetry.TraceDriver = memorytelemetry.New()
service := telemetry.New(traces)
fmt.Println(service.Enabled()) // false until Start

type TraceID

type TraceID [16]byte

TraceID identifies a trace as 16 bytes. The all-zero value is invalid; this type represents an identity and does not generate randomness itself.

id := telemetry.TraceID{1}
fmt.Println(id.IsZero()) // false

func (TraceID) IsZero

func (traceID TraceID) IsZero() bool

IsZero reports whether every byte is zero. It validates absence only, not whether an identity is trusted or was issued by a particular backend.

fmt.Println((telemetry.TraceID{}).IsZero()) // true

func (TraceID) String

func (traceID TraceID) String() string

String returns all 16 bytes as 32 lowercase hexadecimal characters, including leading zeros. It also formats an invalid zero identity without failing.

id := telemetry.TraceID{1}
fmt.Println(id.String()) // 01000000000000000000000000000000

type Tracer

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

Tracer starts spans for one instrumentation scope. It is a cheap value; create one per use with Telemetry.Tracer. The zero Tracer is a no-op. Starting a span does not start its owning telemetry lifecycle.

tracer := service.Tracer("example.com/cache")
_, span := tracer.Start(ctx, "cache get")
span.End()

func (Tracer) Start

func (tracer Tracer) Start(ctx context.Context, name string, opts ...StartOption) (context.Context, *Span)

Start starts a span named name as a child of the current span of ctx and returns a context carrying it. The caller must call Span.End.

Without a started trace driver (or a filling-in observer, see Telemetry.Observe) it allocates nothing and returns ctx and a shared non-recording span, so instrumentation is safe to leave in hot paths. Invalid attributes are dropped. The span name should have low cardinality: "GET /users/{id}", not "GET /users/42".

ctx, span := tracer.Start(ctx, "invoice.send", telemetry.Kind(telemetry.Producer))
defer span.End()

type TransportBuilder

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

TransportBuilder configures Transport before publication. Callbacks run directly once, synchronously in order on one fresh draft; nil entries panic. Retaining the draft cannot change published settings. Methods return the same draft.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.PropagationHosts("*.example.com")
})}
_ = httpClient

func (*TransportBuilder) Propagation

func (transport *TransportBuilder) Propagation(propagate func(*http.Request) bool) *TransportBuilder

Propagation decides, per outgoing request, whether the driver's propagation headers (traceparent, baggage, ...) are injected. Spans and metrics are recorded for every request regardless. Trace context and baggage can carry data meant for your own services, so the default, nil, is PropagateInternal: inject only to internal destinations and never leak them to third parties. See PropagateAll, PropagateNone, TransportBuilder.PropagationHosts.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.Propagation(nil)
})}
_ = httpClient

func (*TransportBuilder) PropagationHosts

func (transport *TransportBuilder) PropagationHosts(hosts ...string) *TransportBuilder

PropagationHosts injects trace context into internal destinations (see PropagateInternal) and into the listed hosts. A host is an exact name such as "api.partner.com" or a wildcard such as "*.example.com", which matches any subdomain but not "example.com" itself. Matching is case-insensitive and ignores the port. It replaces any earlier predicate and copies hosts.

The setting replaces the previous value on this construction draft. Publication keeps independent settings; functions and services stay shared.

Example:

httpClient := &http.Client{Transport: telemetry.Transport(nil, func(transport *telemetry.TransportBuilder) {
	transport.PropagationHosts("*.example.com")
})}
_ = httpClient

type TransportBuilderFunc

type TransportBuilderFunc = func(*TransportBuilder)

TransportBuilderFunc configures an outgoing transport at construction, not per request. Use the alias when reusing one propagation policy across clients.

var configure telemetry.TransportBuilderFunc = func(transport *telemetry.TransportBuilder) {
	transport.Propagation(telemetry.PropagateNone)
}
client := &http.Client{Transport: service.Transport(nil, configure)}
_ = client

type UpDownCounter

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

UpDownCounter is a sum that can increase and decrease. The zero value is a no-op.

active := meter.UpDownCounter("active")
active.Add(ctx, 1)
active.Add(ctx, -1)

func (UpDownCounter) Add

func (counter UpDownCounter) Add(ctx context.Context, delta int64, attrs ...Attr)

Add changes the sum by delta, which may be negative. Attributes outside the declared keys are dropped.

active := meter.UpDownCounter("active", telemetry.Keys("worker"))
active.Add(ctx, -1, telemetry.String("worker", "primary"))

Directories

Path Synopsis
internal
httptrack
Package httptrack implements HTTP observation policy without telemetry state.
Package httptrack implements HTTP observation policy without telemetry state.
lazy
Package lazy caches the latest value built for a comparable owner.
Package lazy caches the latest value built for a comparable owner.
Package memorytelemetry provides an in-process telemetry.TraceDriver, telemetry.MetricDriver, and telemetry.LogDriver for tests, local development, and applications that want trace IDs in logs and responses without a backend.
Package memorytelemetry provides an in-process telemetry.TraceDriver, telemetry.MetricDriver, and telemetry.LogDriver for tests, local development, and applications that want trace IDs in logs and responses without a backend.
internal/aggregate
Package aggregate retains bounded, ordered metric label tuples.
Package aggregate retains bounded, ordered metric label tuples.
internal/capture
Package capture copies and normalizes telemetry snapshots before storage.
Package capture copies and normalizes telemetry snapshots before storage.
internal/tracegroup
Package tracegroup organizes retained span snapshots without storage or DTOs.
Package tracegroup organizes retained span snapshots without storage or DTOs.
Package telemetrytest tests code that emits telemetry and drivers that implement it.
Package telemetrytest tests code that emits telemetry and drivers that implement it.
internal/conformance
Package conformance exercises telemetry driver protocols and readback.
Package conformance exercises telemetry driver protocols and readback.

Jump to

Keyboard shortcuts

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