Documentation
¶
Overview ¶
Package trace provides a thin, ergonomic wrapper around OpenTelemetry distributed tracing for GitLab Go services.
The package exposes four groups of API:
- New / NewWithConfig for managed setup — exporter, provider, sampler and shutdown are handled for you.
- TracingConfig, ParseTracingConfig, and ParseTracingConfigFromEnv for parsing GitLab tracing connection strings from any source (env var, Rails JSON response, CLI flag, etc.) and building OTLP options yourself.
- Tracer for creating spans. Tracer.Start returns the raw go.opentelemetry.io/otel/trace.Span interface — no labkit wrapper.
- RecordError for recording errors with both an exception event and status=Error in one call.
Quick start — managed setup ¶
For most services, New wires everything up from the GitLabTracingEnvVar environment variable and sensible defaults:
tracer, shutdown, err := trace.New(ctx)
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx)
ctx, span := tracer.Start(ctx, "my-operation")
defer span.End()
span.SetAttributes(attribute.String("user.id", userID))
Error recording ¶
result, err := doWork(ctx)
if err != nil {
trace.RecordError(span, err) // records exception event AND sets status=Error
return err
}
Propagating context downstream ¶
The context returned by Tracer.Start carries the active span. Pass it to downstream calls so that child spans are nested correctly:
ctx, span := tracer.Start(ctx, "parent") defer span.End() ctx, child := tracer.Start(ctx, "child") defer child.End()
Advanced setup — build your own provider ¶
When you receive a tracing URL from a source other than the environment (e.g. a JSON configuration document from Rails), parse it with ParseTracingConfig and use the fields directly with the OTEL SDK:
cfg := trace.ParseTracingConfig(urlFromRails)
if cfg == nil {
// URL was empty or unparseable — proceed without tracing.
}
var exporterOpts []otlptracehttp.Option
if cfg.Endpoint != "" {
exporterOpts = append(exporterOpts, otlptracehttp.WithEndpointURL(cfg.Endpoint+cfg.URLPath))
}
if cfg.Insecure {
exporterOpts = append(exporterOpts, otlptracehttp.WithInsecure())
}
exporter, err := otlptracehttp.New(ctx, exporterOpts...)
if err != nil { ... }
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(cfg.Sampler()),
)
defer provider.Shutdown(ctx)
// Wrap the provider in a LabKit Tracer for convenience, or use it directly.
tracer := trace.NewWithProvider(provider)
Using the full OpenTelemetry API ¶
Tracer.Start returns go.opentelemetry.io/otel/trace.Span directly, so the full OTEL span API is available without any labkit wrapper:
span.AddEvent("cache-miss", oteltrace.WithAttributes(attribute.String("key", k)))
span.AddLink(oteltrace.Link{SpanContext: remoteCtx})
sc := span.SpanContext() // retrieve trace-id / span-id
To pass the tracer to a third-party OTEL-instrumented library that requires go.opentelemetry.io/otel/trace.Tracer (a sealed interface), use Tracer.OTELTracer:
oteltracer := tracer.OTELTracer() thirdPartyLib.Init(oteltracer)
Tracer.Provider returns the underlying sdktrace.TracerProvider for passing to instrumentation libraries that require it:
provider := tracer.Provider() thirdPartyLib.SetProvider(provider)
Environment-based configuration (GITLAB_TRACING) ¶
The GitLabTracingEnvVar environment variable configures the tracer without changing code. It uses a URL-style connection string:
GITLAB_TRACING="otlp://collector.example.com:4318?service_name=my-api&sampler=probabilistic&sampler_param=0.01"
Scheme:
- otlp — HTTP (insecure)
- otlps — HTTPS (secure)
Query parameters:
- service_name — overrides [Config.ServiceName]
- sampler — "probabilistic" or "const"
- sampler_param — rate (0.0–1.0) for probabilistic; 0 or 1 for const
When a non-nil Config is also passed to NewWithConfig, explicit Config fields take priority: Endpoint and ServiceName fill in only if they are empty; SampleRate in the Config is always used as-is.
Testing ¶
Use NewWithProvider together with the tracetest package to capture spans without a live collector:
import "gitlab.com/gitlab-org/labkit/v2/testing/tracetest" tracer, rec := tracetest.NewRecorder() ctx, span := tracer.Start(context.Background(), "op") span.End() spans := rec.Ended() assert.Equal(t, "op", spans[0].Name)
Example ¶
Example shows managed setup using New, which reads GITLAB_TRACING and applies sensible defaults. This is the right starting point for most services.
package main
import (
"context"
"log"
"gitlab.com/gitlab-org/labkit/v2/trace"
"go.opentelemetry.io/otel/attribute"
)
// Package-level attribute.Key constants give each attribute a stable, typed
// name. Call the key's method (String, Int64, Bool, …) to produce a KeyValue —
// mismatched key strings across call sites become impossible.
const (
attrUserID attribute.Key = "user.id"
attrMRIID attribute.Key = "mr.iid"
attrDryRun attribute.Key = "dry_run"
)
func main() {
ctx := context.Background()
tracer, shutdown, err := trace.New(ctx)
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx) //nolint:errcheck
_, span := tracer.Start(ctx, "my-operation")
defer span.End()
span.SetAttributes(
attrUserID.String("u-42"),
attrMRIID.Int64(101),
attrDryRun.Bool(false),
)
}
Output:
Index ¶
Examples ¶
Constants ¶
const GitLabTracingEnvVar = "GITLAB_TRACING"
GitLabTracingEnvVar is the name of the environment variable that holds the GitLab tracing connection string. It is exported so that callers can read or watch the variable themselves.
const SampleRateDropAll float64 = -1
SampleRateDropAll may be assigned to [Config.SampleRate] to explicitly configure a TracerProvider that never samples spans. Setting SampleRate to 0 has the same effect.
Variables ¶
This section is empty.
Functions ¶
func RecordError ¶
RecordError records err as an exception span event and sets the span status to codes.Error with err.Error() as the description. It is a no-op when err is nil.
Note: the raw OTEL oteltrace.Span.RecordError records the exception event but does not set the span status. This function does both, which is almost always what you want.
Example ¶
ExampleRecordError shows how to record an error on a span. Unlike the raw OTEL Span.RecordError, trace.RecordError also sets the span status to codes.Error in the same call.
package main
import (
"context"
"errors"
"log"
"gitlab.com/gitlab-org/labkit/v2/trace"
)
func main() {
ctx := context.Background()
tracer, shutdown, err := trace.New(ctx)
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx) //nolint:errcheck
_, span := tracer.Start(ctx, "do-work")
defer span.End()
if err := errors.New("something failed"); err != nil {
trace.RecordError(span, err)
}
}
Output:
Types ¶
type Config ¶
type Config struct {
// ServiceName is the service.name resource attribute included on all spans.
// Default: path.Base(os.Args[0])
ServiceName string
// ServiceVersion is the service.version resource attribute.
// Omitted from the resource when empty.
ServiceVersion string
// Endpoint is the full URL of the OTLP HTTP collector, including any path.
// Example: "https://collector.example.com:4318/otel/v1/traces"
// Default: "" — the SDK honours OTEL_EXPORTER_OTLP_ENDPOINT or falls back
// to http://localhost:4318.
Endpoint string
// Insecure disables TLS when connecting to the OTLP collector.
// Useful for local development. Only applied when Endpoint is also set.
Insecure bool
// Headers are additional HTTP headers sent with every export request,
// for example authentication tokens required by managed collectors.
Headers map[string]string
// SampleRate controls the fraction of traces that are sampled, in the
// range [0.0, 1.0]. 0 disables sampling entirely. Values >= 1.0 sample
// every trace. [SampleRateDropAll] (-1) is an alias for 0 kept for
// clarity. Default when Config is nil: 0.01 (1%).
SampleRate float64
}
Config holds the configuration for creating a new Tracer. A nil Config passed to NewWithConfig produces sensible defaults: 1% sampling, OTLP/HTTP export to localhost:4318, and the service name derived from the process binary.
type Tracer ¶
type Tracer struct {
// contains filtered or unexported fields
}
Tracer creates spans for tracing operations. Obtain one from New or NewWithConfig, then call Tracer.Start to begin each operation you want to trace.
func New ¶
New returns a Tracer and a shutdown function using default Config. The shutdown function must be called on application exit to flush any buffered spans before the process terminates.
tracer, shutdown, err := trace.New(ctx)
if err != nil { ... }
defer shutdown(ctx)
func NewWithConfig ¶
func NewWithConfig(ctx context.Context, cfg *Config, opts ...sdktrace.TracerProviderOption) (*Tracer, func(context.Context) error, error)
NewWithConfig returns a Tracer and a shutdown function using the provided Config. A nil cfg causes all values to be derived from the [GITLAB_TRACING] environment variable (when set) or built-in defaults.
When cfg is non-nil, [GITLAB_TRACING] fills in any fields that are left at their zero value (empty string for Endpoint / ServiceName). SampleRate in the explicit Config always takes precedence; set it to the SampleRateDropAll sentinel if you want to disable sampling regardless of the environment.
Additional sdktrace.TracerProviderOption values may be passed as opts and are appended to the provider after the defaults derived from cfg. Use this to inject extra span processors, additional exporters, or any other provider option not covered by Config without requiring labkit changes:
tracer, shutdown, err := trace.NewWithConfig(ctx, cfg,
sdktrace.WithSpanProcessor(myProcessor),
)
Example (WithProviderOptions) ¶
ExampleNewWithConfig_withProviderOptions shows how to inject additional sdktrace.TracerProviderOption values via the variadic opts parameter. Use this for a second exporter, custom batch settings, or any other provider option not covered by trace.Config — without requiring labkit changes.
package main
import (
"context"
"log"
"gitlab.com/gitlab-org/labkit/v2/trace"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func main() {
ctx := context.Background()
// myProcessor could be any sdktrace.SpanProcessor implementation.
var myProcessor sdktrace.SpanProcessor
tracer, shutdown, err := trace.NewWithConfig(ctx,
&trace.Config{SampleRate: trace.SampleRateDropAll},
sdktrace.WithSpanProcessor(myProcessor),
)
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx) //nolint:errcheck
_ = tracer
}
Output:
func NewWithProvider ¶
func NewWithProvider(provider *sdktrace.TracerProvider) *Tracer
NewWithProvider wraps an existing sdktrace.TracerProvider in a Tracer. It is intended for testing (via the tracetest package) and advanced use cases where the caller manages the provider lifecycle directly.
func (*Tracer) OTELTracer ¶
OTELTracer returns the underlying oteltrace.Tracer. Use this when passing a tracer to a third-party OTEL-instrumented library that requires the raw interface. Note: oteltrace.Tracer is a sealed interface; *Tracer cannot implement it directly.
func (*Tracer) Provider ¶
func (t *Tracer) Provider() *sdktrace.TracerProvider
Provider returns the underlying sdktrace.TracerProvider. Use this when code outside of LabKit needs the provider directly, for example to register it as a global or to pass it to another instrumentation library.
Example ¶
ExampleTracer_Provider shows how to retrieve the underlying TracerProvider from a LabKit Tracer, for example to pass it to an instrumentation library that requires it directly.
package main
import (
"context"
"log"
"gitlab.com/gitlab-org/labkit/v2/trace"
)
func main() {
ctx := context.Background()
tracer, shutdown, err := trace.NewWithConfig(ctx, &trace.Config{
SampleRate: trace.SampleRateDropAll,
})
if err != nil {
log.Fatal(err)
}
defer shutdown(ctx) //nolint:errcheck
provider := tracer.Provider()
_ = provider // pass to an instrumentation library that requires it
}
Output:
func (*Tracer) Start ¶
func (t *Tracer) Start(ctx context.Context, name string, opts ...oteltrace.SpanStartOption) (context.Context, oteltrace.Span)
Start begins a new span named name, forwarding any span start options to the underlying OTEL tracer. The returned context carries the active span and must be passed to downstream calls. Always pair Start with a deferred End:
ctx, span := tracer.Start(ctx, "do-work") defer span.End()
type TracingConfig ¶
type TracingConfig struct {
// Endpoint is the base URL of the OTLP HTTP collector (scheme + host),
// e.g. "http://host:4318" or "https://host:4317". The path component, if
// any, is stored separately in URLPath.
Endpoint string
// URLPath is the path component of the connection string, e.g.
// "/otel/v1/traces". Empty when the connection string contained no path.
// Append it to Endpoint when constructing the collector URL.
URLPath string
// Insecure is true when the connection string used the "otlp" scheme
// (plain HTTP). When false the connection uses TLS.
Insecure bool
// ServiceName is the value of the service_name query parameter, if present.
ServiceName string
// SampleRate is the resolved sample rate. Only meaningful when SamplerSet
// is true; zero otherwise.
SampleRate float64
// SamplerSet is true when a sampler and sampler_param were explicitly
// provided in the connection string.
SamplerSet bool
// Headers holds additional HTTP headers to send with every export
// request. Populated from header_<Name>=<value> query parameters in the
// connection string, e.g. header_Authorization=Bearer+token.
Headers map[string]string
}
TracingConfig holds configuration parsed from a GitLab tracing connection string. Callers that receive a tracing URL from a source other than the environment (e.g. a JSON configuration document from Rails) can call ParseTracingConfig directly and use the fields to build their own exporter and provider with the OTEL SDK.
func ParseTracingConfig ¶
func ParseTracingConfig(connectionString string) *TracingConfig
ParseTracingConfig parses a GitLab tracing connection string and returns the resulting TracingConfig. Returns nil when connectionString is empty or cannot be parsed.
Supported format:
otlp://host:port?service_name=X&sampler=probabilistic&sampler_param=0.01 otlps://host:port?... (TLS / secure)
Sampler types:
- probabilistic — sampler_param is a float in [0.0, 1.0]; use 0 to drop all, 1 to keep all
HTTP headers for the OTLP exporter can be set with header_<Name>=<value> query parameters:
otlp://host:4318?header_Authorization=Bearer+token&header_X-Custom=value
Example ¶
ExampleParseTracingConfig shows how a service that receives a tracing URL from an external source (e.g. a Rails JSON response) can parse it and build its own exporter and provider using the OTEL SDK directly.
package main
import (
"context"
"log"
"gitlab.com/gitlab-org/labkit/v2/trace"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
func main() {
ctx := context.Background()
// urlFromRails is whatever the caller received over the wire.
urlFromRails := "otlp://collector.internal:4318?service_name=runner&sampler=probabilistic&sampler_param=0.05"
cfg := trace.ParseTracingConfig(urlFromRails)
if cfg == nil {
// URL was empty or unparseable — run without tracing.
return
}
// Build exporter options directly from the parsed fields.
var exporterOpts []otlptracehttp.Option
if cfg.Endpoint != "" {
exporterOpts = append(exporterOpts, otlptracehttp.WithEndpointURL(cfg.Endpoint+cfg.URLPath))
}
if cfg.Insecure {
exporterOpts = append(exporterOpts, otlptracehttp.WithInsecure())
}
exporter, err := otlptracehttp.New(ctx, exporterOpts...)
if err != nil {
log.Fatal(err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithSampler(cfg.Sampler()),
)
defer provider.Shutdown(ctx) //nolint:errcheck
// Use the provider directly, or wrap it for the labkit convenience API.
tracer := trace.NewWithProvider(provider)
ctx, span := tracer.Start(ctx, "job-execution")
defer span.End()
_ = ctx
}
Output:
func ParseTracingConfigFromEnv ¶
func ParseTracingConfigFromEnv() *TracingConfig
ParseTracingConfigFromEnv reads GitLabTracingEnvVar and parses it as a GitLab tracing connection string. Returns nil when the variable is unset, empty, or cannot be parsed.
Example ¶
ExampleParseTracingConfigFromEnv shows how to read and parse the GITLAB_TRACING environment variable without going through New.
package main
import (
"gitlab.com/gitlab-org/labkit/v2/trace"
)
func main() {
cfg := trace.ParseTracingConfigFromEnv()
if cfg == nil {
// GITLAB_TRACING is unset or unparseable.
return
}
// cfg.Endpoint, cfg.URLPath, cfg.Insecure, cfg.ServiceName, cfg.SampleRate
// are available for constructing your own exporter and provider.
_ = cfg.Sampler()
}
Output:
func (*TracingConfig) Sampler ¶
func (c *TracingConfig) Sampler() sdktrace.Sampler
Sampler returns an sdktrace.Sampler for this config.
When [TracingConfig.SamplerSet] is true it returns sdktrace.TraceIDRatioBased using [TracingConfig.SampleRate]. When SamplerSet is false (no sampler was specified in the connection string) it returns sdktrace.ParentBased(sdktrace.AlwaysSample), which is the OpenTelemetry SDK default: sample all root spans and follow the parent decision for non-root spans.
Sampler is nil-safe: a nil receiver is treated as an absent config and returns sdktrace.ParentBased(sdktrace.AlwaysSample).