This module exposes the approved metrics API backed exclusively by OpenTelemetry.
The package name remains metrics so call sites stay concise:
import metrics "github.com/InjectiveLabs/otel"Initialize the OTLP exporters from the environment:
shutdown, err := metrics.InitFromEnv(ctx, "APP")
if err != nil {
return err
}
defer shutdown(context.Background())To avoid blocking application startup while the collector is unavailable, initialize in the background and choose the retry interval:
shutdown, err := metrics.InitFromEnvBackground(ctx, "APP", 30*time.Second)
if err != nil {
return err
}
defer shutdown(context.Background())Pass an empty prefix to read OTEL_* variables, or a prefix such as APP
to read APP_OTEL_*. Passing the full APP_OTEL prefix is also valid.
This call returns before connecting. Initialization failures are sent to the
global OpenTelemetry error handler and retried until ctx is canceled or
shutdown is called. Canceling ctx also shuts down an initialized provider.
This reads:
APP_OTEL_ADDR: OTLP/gRPC endpoint.APP_OTEL_PREFIX: metric prefix and OpenTelemetryservice.name.APP_OTEL_INSECURE: disable transport security.APP_OTEL_TRACING_DISABLED: disable trace export; defaults tofalse.APP_OTEL_REPORT_CANCELED_AS_ERROR: classifycontext.Canceledas an error; defaults tofalse.APP_OTEL_DISABLED: disable metrics, tracing, exporters, and client connections; defaults totrue.
Legacy counter mode, mocking, Datadog, Mixpanel, and Bugsnag variables are not used.
The metric and tracer providers are owned by this package and are not installed
as OpenTelemetry global providers. This allows the package to run alongside
another telemetry client without either initializer replacing the other's
providers. Init only configures the global W3C trace-context and baggage
propagator.
For explicit configuration, use metrics.Init(ctx, metrics.Config{...}) or
metrics.InitBackground(ctx, metrics.Config{...}, retryInterval).
Use Counter for unsigned count values and Incr to increment by one.
metrics.Counter("orders.processed", uint64(count), "market", marketID)
metrics.Incr("orders.accepted", "market", marketID)metrics.Gauge("queue.depth", float64(depth), "queue", queueName)Histogram records durations in milliseconds.
metrics.Histogram("request.latency", time.Since(start), "route", routeName)Use Event to bind tags whose final values are only known when the metric is
emitted:
func ProcessOrder(marketID string) (result string, err error) {
defer metrics.Event("order.processed", "market", marketID).
BindErr(&err).
Bind("result", &result).
Incr()
result = "accepted"
return result, nil
}Record measures an operation and emits its duration as a histogram when
Done runs. Done is idempotent.
func ProcessOrder(ctx context.Context, marketID string) (err error) {
defer metrics.Record("order.process").
BindErr(&err).
Done("market", marketID)
return process(ctx, marketID)
}Pointer values passed to Bind are resolved when the metric is emitted.
Record measures duration and emits a histogram. Adding WithSpan also creates
a span with the same name. The context returned by Context contains that span
and must be passed to child operations so OpenTelemetry can place their spans in
the same trace.
func HandleOrder(ctx context.Context, orderID string) (err error) {
root := metrics.Record("order.handle", "order_id", orderID).
WithSpan(ctx).
BindErr(&err)
defer root.Done()
// Every operation receiving this context becomes a child of order.handle.
ctx = root.Context()
order, err := loadOrder(ctx, orderID)
if err != nil {
return err
}
if err = validateOrder(ctx, order); err != nil {
return err
}
return persistOrder(ctx, order)
}
func loadOrder(ctx context.Context, orderID string) (order *Order, err error) {
rec := metrics.Record("order.load", "order_id", orderID).
WithSpan(ctx).
BindErr(&err)
defer rec.Done()
// Pass the child context further down if the repository is instrumented.
return repository.Load(rec.Context(), orderID)
}
func validateOrder(ctx context.Context, order *Order) (err error) {
rec := metrics.Record("order.validate").
WithSpan(ctx).
BindErr(&err).
Bind("market", &order.Market)
defer rec.Done()
return rules.Validate(rec.Context(), order)
}
func persistOrder(ctx context.Context, order *Order) (err error) {
rec := metrics.Record("order.persist").
WithSpan(ctx).
BindErr(&err)
defer rec.Done("market", order.Market)
return repository.Save(rec.Context(), order)
}If the initial ctx has no span, the resulting trace has one root span and
three child spans:
order.handle
├── order.load
├── order.validate
└── order.persist
If the initial context came from instrumented HTTP/gRPC middleware,
order.handle is instead a child of the remote span, while all four operations
keep the propagated trace ID.
Done ends the span, records the duration histogram, and attaches the final
tags as span attributes. BindErr adds an error=true or error=false
attribute. By default, context.Canceled produces error=false, while
context.DeadlineExceeded remains an error. Set
REPORT_CANCELED_AS_ERROR=true if cancellations must count as errors. Because
deferred calls run last-in, first-out, each child span ends before the root
span.
For propagation across services, pass rec.Context() to an instrumented
HTTP/gRPC client. On the receiving service, pass the context created by its
OpenTelemetry middleware to WithSpan. The propagator configured by Init
then keeps both services in the same trace.
When tracing is disabled, WithSpan does not create a span and returns the
original context through Context; duration metrics continue to work.
For the compact deferred form, Trace combines Record and BindCtx: it
creates a span and replaces the supplied context with its span-bearing context
before Done is deferred. Done ends the span and restores the previous
context:
defer metrics.Trace(&runnerCtx, "orchestrator-run", "backend", backend).
BindErr(&err).
Done()
return runNextOperation(runnerCtx)Restoring the context makes sequential scopes siblings:
first := metrics.Trace(&ctx, "first")
runFirst(ctx)
first.Done() // ctx returns to the parent span
second := metrics.Trace(&ctx, "second")
runSecond(ctx)
second.Done() // ctx returns to the same parent spanFor child operations, prefer the structured Span helper. It passes the child
context to the callback and always ends the span when the callback returns:
metrics.Span(ctx, "child", func(ctx context.Context) {
runChild(ctx)
}, "foo", "bar")Use SpanErr when the callback returns an error. It returns the same error and
binds it to the span and duration metric:
err := metrics.SpanErr(ctx, "child", func(ctx context.Context) error {
return runChild(ctx)
}, "foo", "bar")Both helpers receive the parent context by value, so separate invocations create sibling spans without mutating the caller's context.