Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

79 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

OpenTelemetry metrics

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"

Initialization

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 OpenTelemetry service.name.
  • APP_OTEL_INSECURE: disable transport security.
  • APP_OTEL_TRACING_DISABLED: disable trace export; defaults to false.
  • APP_OTEL_REPORT_CANCELED_AS_ERROR: classify context.Canceled as an error; defaults to false.
  • APP_OTEL_DISABLED: disable metrics, tracing, exporters, and client connections; defaults to true.

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).

Counters

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)

Gauges

metrics.Gauge("queue.depth", float64(depth), "queue", queueName)

Histograms

Histogram records durations in milliseconds.

metrics.Histogram("request.latency", time.Since(start), "route", routeName)

Events

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
}

Duration recording

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.

Spans

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 span

For 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.

About

A library supports open telemetry metrics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages