trace

package
v1.39.0 Latest Latest
Warning

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

Go to latest
Published: Mar 2, 2026 License: MIT Imports: 15 Imported by: 0

README

Trace Package

The trace package provides a thin, ergonomic wrapper around OpenTelemetry (OTEL) distributed tracing for GitLab Go services.

Engineers in GitLab must use this package to instrument their services so that trace data is emitted consistently across all systems.

Quick Start

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.SetAttribute("user.id", userID)

New exports spans via OTLP/HTTP to localhost:4318 by default. The collector endpoint and other settings can be configured without code changes via the GITLAB_TRACING environment variable (see below) or via Config.

Environment Variables

The recommended way to configure tracing in cloud-native deployments is via the GITLAB_TRACING environment variable. It accepts a connection string that controls the collector endpoint, TLS, service name, and sampling in a single value.

Connection string format
<scheme>://<host>:<port>[?param=value&...]
Scheme Transport TLS
otlp HTTP No (insecure)
otlps HTTP Yes
otlp+tls HTTP Yes
Query parameter Example Description
service_name my-api Sets the service.name resource attribute
sampler probabilistic or const Sampler type
sampler_param 0.1 (probabilistic) or 0/1 (const) Sampler parameter
Examples
# Insecure local collector, 10% sampling
GITLAB_TRACING="otlp://localhost:4318?sampler=probabilistic&sampler_param=0.1"

# TLS-enabled remote collector, always sample, custom service name
GITLAB_TRACING="otlps://collector.example.com:4317?service_name=gitaly&sampler=const&sampler_param=1"

In Kubernetes:

env:
  - name: GITLAB_TRACING
    value: "otlps://otel-collector.observability:4317?service_name=my-service&sampler=probabilistic&sampler_param=0.05"
Precedence

Values set in Config take precedence over GITLAB_TRACING. The env var fills in only fields that are left at their zero value in the Config struct, so you can mix both: use GITLAB_TRACING for the endpoint and set SampleRate programmatically, for example.

Programmatic Configuration

tracer, shutdown, err := trace.NewWithConfig(ctx, &trace.Config{
    // ServiceName is included as the service.name resource attribute on every span.
    // Defaults to the binary name when empty.
    ServiceName: "gitaly",

    // ServiceVersion is included as the service.version resource attribute.
    ServiceVersion: "16.0.0",

    // Endpoint is the base URL of the OTLP HTTP collector (no path suffix).
    Endpoint: "https://collector.example.com:4318",

    // Insecure disables TLS. Useful for local development.
    Insecure: true,

    // Headers are sent with every export request, e.g. for authentication.
    Headers: map[string]string{"x-honeycomb-team": token},

    // SampleRate controls the fraction of traces that are sampled [0.0, 1.0].
    // Zero value defaults to 1.0 (always sample).
    SampleRate: 0.1,

    // DisableGlobalProvider prevents registering the provider as the global
    // OTEL provider. Leave false (default) for services that use third-party
    // OTEL-instrumented libraries.
    DisableGlobalProvider: false,
})

To explicitly drop all spans (useful in tests that use the real provider):

tracer, shutdown, err := trace.NewWithConfig(ctx, &trace.Config{
    SampleRate: trace.SampleRateDropAll,
})

Creating Spans

Always pair Start with a deferred End. The returned context carries the active span — pass it to all downstream calls so child spans are nested correctly.

ctx, span := tracer.Start(ctx, "process-merge-request")
defer span.End()

// Annotate the span with typed attributes.
span.SetAttribute("mr.iid", mergeRequestIID)
span.SetAttribute("project.id", projectID)
span.SetAttribute("dry_run", false)

SetAttribute accepts Go primitives and their slice variants directly:

Go type Example
bool span.SetAttribute("flag", true)
int, int32, int64 span.SetAttribute("count", 42)
float32, float64 span.SetAttribute("ratio", 0.95)
string span.SetAttribute("env", "production")
fmt.Stringer span.SetAttribute("status", myStatus)
[]bool, []int64, []float64, []string span.SetAttribute("ids", []int64{1, 2, 3})

Any other type falls back to fmt.Sprintf("%v", value).

Recording Errors

result, err := doWork(ctx)
if err != nil {
    span.RecordError(err) // records the exception event AND sets status=Error
    return err
}

RecordError records the exception event and sets the span status to codes.Error. The raw OTEL Span.RecordError only records the event; this wrapper handles both in one call.

Retrieving the Active Span from Context

span := trace.SpanFromContext(ctx)
span.SetAttribute("queue.depth", depth)

SpanFromContext always returns a non-nil span. If no span is active in the context a no-op span is returned — all methods are safe to call on it.

Context Propagation

Child spans are automatically nested under the active span when the enriched context is passed downstream:

ctx, parent := tracer.Start(ctx, "handle-request")
defer parent.End()

// child is nested under parent in the trace.
ctx, child := tracer.Start(ctx, "query-database")
defer child.End()

Using the Full OpenTelemetry API

Span embeds oteltrace.Span, so any OTEL method not wrapped by labkit is accessible directly:

// Add a structured event.
span.AddEvent("cache-miss", oteltrace.WithAttributes(
    attribute.String("cache.key", key),
))

// Add a span link to a remote trace.
span.AddLink(oteltrace.Link{SpanContext: remoteSpanCtx})

// Retrieve the trace-id and span-id.
sc := span.SpanContext()
traceID := sc.TraceID().String()

Where labkit's helper method shadows an OTEL method with a different signature, reach the original via the embedded field:

// End with an explicit timestamp.
span.Span.End(oteltrace.WithTimestamp(endTime))

// RecordError with event options.
span.Span.RecordError(err, oteltrace.WithAttributes(
    attribute.String("error.source", "upstream"),
))

To pass the underlying tracer to a third-party OTEL-instrumented library:

oteltracer := tracer.OTELTracer() // returns oteltrace.Tracer
thirdPartyLib.Init(oteltracer)

Testing

Use tracetest.NewRecorder to capture completed spans in memory without a live collector. The recorder always samples and does not register a global provider, keeping tests isolated.

import "gitlab.com/gitlab-org/labkit/v2/testing/tracetest"

func TestMyHandler(t *testing.T) {
    tracer, rec := tracetest.NewRecorder()

    ctx, span := tracer.Start(context.Background(), "handle-request")
    span.SetAttribute("user.id", 42)
    span.RecordError(errors.New("something went wrong"))
    span.End()

    spans := rec.Ended()
    require.Len(t, spans, 1)

    s := spans[0]
    assert.Equal(t, "handle-request", s.Name)
    assert.Equal(t, int64(42), s.Attributes["user.id"])
    assert.True(t, s.HasError)
    assert.Equal(t, codes.Error, s.StatusCode)
    assert.Equal(t, "something went wrong", s.StatusMsg)
}

rec.Reset() clears recorded spans between sub-tests when a single recorder is shared across a table-driven test.

Documentation

Overview

Package trace provides a thin, ergonomic wrapper around OpenTelemetry distributed tracing for GitLab Go services.

Engineers must use this package to create and manage spans so that trace data is emitted consistently across all GitLab services.

The package exposes three core constructs:

- Config and New / NewWithConfig for TracerProvider initialisation - Tracer and Span for creating and annotating spans - SpanFromContext for retrieving the active span from a context

Quick start

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.SetAttribute("user.id", userID)

Error recording

result, err := doWork(ctx)
if err != nil {
	span.RecordError(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()

Using the full OpenTelemetry API

Span embeds go.opentelemetry.io/otel/trace.Span, so any OTEL method not wrapped by labkit is accessible directly:

span.AddEvent("cache-miss", oteltrace.WithAttributes(attribute.String("key", k)))
span.AddLink(oteltrace.Link{SpanContext: remoteCtx})
sc := span.SpanContext() // retrieve trace-id / span-id

Where labkit's helper methods shadow an OTEL method with a different signature, reach the original via the embedded field:

span.Span.End(oteltrace.WithTimestamp(t))           // End with explicit timestamp
span.Span.RecordError(err, oteltrace.WithAttributes(...)) // RecordError with options

Similarly, Tracer.OTELTracer returns the underlying go.opentelemetry.io/otel/trace.Tracer for passing to third-party instrumented libraries:

oteltracer := tracer.OTELTracer()

Environment-based configuration (GITLAB_TRACING)

The GITLAB_TRACING environment variable can be used to configure 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)

Index

Constants

View Source
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

This section is empty.

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 base URL of the OTLP HTTP collector (no path suffix).
	// Example: "https://collector.example.com:4318"
	// 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

	// DisableGlobalProvider prevents New from registering the TracerProvider
	// as the global OTEL provider. The zero value (false) sets the global,
	// which is the safe default for services that use third-party libraries
	// instrumented with OTEL.
	DisableGlobalProvider bool
}

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 Span

type Span struct {
	oteltrace.Span
}

Span represents an active trace span. Always call Span.End when the operation is complete, typically via defer.

Span embeds oteltrace.Span so the full OpenTelemetry span API is available directly — for example oteltrace.Span.AddEvent, oteltrace.Span.AddLink, and oteltrace.Span.SpanContext. The labkit helper methods (SetAttribute, RecordError, End, IsRecording) shadow their OTEL counterparts where they add value; reach the raw OTEL methods via the embedded field when needed:

span.Span.End(oteltrace.WithTimestamp(t))
span.Span.RecordError(err, oteltrace.WithAttributes(...))

func SpanFromContext

func SpanFromContext(ctx context.Context) *Span

SpanFromContext retrieves the active Span from ctx. If no span is present, a no-op Span is returned — all methods are safe to call on it.

func (*Span) End

func (s *Span) End()

End marks the span as complete. It is safe to call on a nil Span.

func (*Span) IsRecording

func (s *Span) IsRecording() bool

IsRecording reports whether the span is actively recording events. Returns false for nil or no-op spans.

func (*Span) RecordError

func (s *Span) RecordError(err error)

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 or when called on a nil Span.

Note: the raw OTEL oteltrace.Span.RecordError records the exception event but does not set the span status. This method does both, which is almost always what you want.

func (*Span) SetAttribute

func (s *Span) SetAttribute(key string, value any)

SetAttribute records a key-value attribute on the span. Supported value types are: bool, int, int32, int64, float32, float64, string, fmt.Stringer, and slice variants of each. Values of any other type are converted to their fmt.Sprintf("%v") string representation. Safe to call on a nil Span.

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

func New(ctx context.Context) (*Tracer, func(context.Context) error, error)

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

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

func (t *Tracer) OTELTracer() oteltrace.Tracer

Start begins a new span named name. It returns an enriched context — which carries the span and should be passed to all downstream calls — and the new Span. Always pair Start with a deferred Span.End:

ctx, span := tracer.Start(ctx, "do-work")
defer span.End()

OTELTracer returns the underlying oteltrace.Tracer. Use this when passing a tracer to third-party OTEL-instrumented code that expects the raw interface.

func (*Tracer) Start

func (t *Tracer) Start(ctx context.Context, name string) (context.Context, *Span)

Jump to

Keyboard shortcuts

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