Skip to content
Get distributed tracing on Runway

Get distributed tracing on Runway

A scaffolded Theseus service already contains everything distributed tracing needs. LabKit v2 builds the tracer, the platform supplies the collector, and the pipeline to Google Cloud Trace is Runway’s problem, not yours. This guide shows the full path — and just how short it is.

1. What your service already does

cmd/server/main.go in every scaffolded service starts with:

a, err := app.New(ctx)

That one call initialises an OpenTelemetry tracer from the GITLAB_TRACING environment variable (OTLP/HTTP export, 1% sampling by default). From there, LabKit’s components pick the tracer up at composition time:

  • httpserver wraps every inbound request in a server span and propagates W3C traceparent headers — cross-service traces stitch together with no handler-level code.
  • httpclient and postgres take Tracer: a.Tracer() in their config, so outbound calls and queries appear as child spans.

One contract to know: LabKit never installs a global tracer. The tracer is passed explicitly to each component — which the scaffold’s composition root already does.

2. Deploy — there is nothing to configure

Your Fairway manifest needs no tracing stanza; the generated chart is tracing-agnostic. When Runway deploys the service (GKE and EKS), it injects the connection string automatically:

GITLAB_TRACING=otlp://agent-otelcol-collector.monitoring.svc.cluster.local:4318?service_name=<runway_service_id>

The in-cluster OTel collector receives the spans, enriches them with Kubernetes attributes, and exports them to Google Cloud Trace. The Runway distributed tracing guide documents the pipeline in full.

So the complete recipe for tracing an inbound-request path is: deploy.

3. Add domain-level spans

The only tracing code you will actually write is for spans you care about — marking out domain operations inside the request:

func (s *Service) Hello(ctx context.Context, name string) (string, error) {
    ctx, span := s.Tracer().Start(ctx, "domain.Hello")
    defer span.End()

    result, err := s.store.Greet(ctx, name)
    if err != nil {
        trace.RecordError(span, err) // exception event + status=Error in one call
        return "", err
    }

    return result, nil
}

Pass the returned ctx downstream and child spans nest correctly.

4. View your traces

  1. Open Cloud Trace in the tenant project — gitlab-runway-staging or gitlab-runway-production.
  2. Filter by service.name matching your runway_service_id.

Each trace shows the inbound server span, your domain spans, and the PostgreSQL and outbound HTTP child spans LabKit created for you.

5. Tune sampling

The default samples 1% of traces. Raise it for a low-traffic service via the app configuration:

a, err := app.NewWithConfig(ctx, &app.Config{
    Trace: &trace.Config{SampleRate: 0.05},
})

An explicit SampleRate always wins over the environment. To disable sampling entirely, use trace.SampleRateDropAll.

6. Verify locally first (optional)

Without GITLAB_TRACING set, the tracer exports to http://localhost:4318 — so a throwaway local collector shows your spans before anything is deployed:

# otel-collector.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: 0.0.0.0:4318
exporters:
  debug:
    verbosity: detailed
service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [debug]
docker run --rm -p 4318:4318 \
  -v $(pwd)/otel-collector.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector:latest --config=/etc/otelcol/config.yaml

Run your service with sampling forced to 100% so every request shows up:

GITLAB_TRACING="otlp://localhost:4318?sampler=probabilistic&sampler_param=1" go run ./cmd/server

Hit an endpoint and watch the spans print in the collector’s output. In unit tests, skip the collector entirely: v2/testing/tracetest.NewRecorder() returns a tracer backed by an in-memory span recorder you can assert on.

Related

Last updated on