Go tracing installation

  1. Install OpenTelemetry packages

    Required

    For the complete SDK reference, see the OpenTelemetry Go docs.

    Terminal
    go get go.opentelemetry.io/otel
    go get go.opentelemetry.io/otel/sdk
    go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp

    otlptracehttp is the OTLP HTTP/protobuf trace exporter. The otlptracegrpc module sends gRPC, so pick otlptracehttp to match this guide.

  2. Get your project token

    Required

    You'll need your PostHog project token to authenticate trace requests. This is the same token you use for capturing events with the PostHog SDK.

    Important: Use your project token which starts with phc_. Do not use a personal API key (which starts with phx_).

    You can find your project token in Project settings.

  3. Configure the SDK

    Required

    Set up the OpenTelemetry SDK to export spans to PostHog over OTLP HTTP. Use WithEndpointURL so the full /i/v1/traces path is sent as-is.

    Go
    package main
    import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
    "go.opentelemetry.io/otel/sdk/resource"
    sdktrace "go.opentelemetry.io/otel/sdk/trace"
    semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
    )
    func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
    exporter, err := otlptracehttp.New(ctx,
    otlptracehttp.WithEndpointURL("https://us.i.posthog.com/i/v1/traces"),
    otlptracehttp.WithHeaders(map[string]string{
    "Authorization": "Bearer <ph_project_token>",
    }),
    )
    if err != nil {
    return nil, err
    }
    res, err := resource.New(ctx,
    resource.WithAttributes(semconv.ServiceName("my-service")),
    )
    if err != nil {
    return nil, err
    }
    tp := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),
    sdktrace.WithResource(res),
    )
    otel.SetTracerProvider(tp)
    return tp, nil
    }

    Call defer tp.Shutdown(ctx) in main so buffered spans flush on exit.

    Alternatively, configure the exporter with environment variables and call otlptracehttp.New(ctx) with no options:

    Terminal
    OTEL_EXPORTER_OTLP_TRACES_ENDPOINT="https://us.i.posthog.com/i/v1/traces"
    OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer <ph_project_token>"
    OTEL_SERVICE_NAME="my-service"

    Note: WithEndpoint sets only the host and defaults the path to /v1/traces. To send to the full /i/v1/traces path, use WithEndpointURL (or the per-signal OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), as shown.

  4. Create spans

    Required

    Wrap the operations you want to measure in spans, and attach attributes for context.

    Go
    import (
    "context"
    "go.opentelemetry.io/otel"
    "go.opentelemetry.io/otel/attribute"
    )
    var tracer = otel.Tracer("my-service")
    func chargeCustomer(ctx context.Context, customerID string) error {
    ctx, span := tracer.Start(ctx, "charge-customer")
    defer span.End()
    span.SetAttributes(attribute.String("customer.id", customerID))
    // ... do work ...
    return nil
    }
  5. Recommended

    Install the optional PostHog OpenTelemetry module:

    Terminal
    go get github.com/posthog/posthog-go/otel

    Register the context processor on your existing provider before requests start, then wrap your HTTP handler:

    Go
    import posthogotel "github.com/posthog/posthog-go/otel"
    tp.RegisterSpanProcessor(posthogotel.NewContextSpanProcessor())
    handler := posthogotel.NewRequestContextMiddleware(existingHandler)

    The wrapper accepts the core request middleware options. Configure tracing_headers for your backend hostname in the browser SDK. Put OpenTelemetry HTTP middleware outside the PostHog wrapper to enrich its existing request span. Create child spans with r.Context() so the processor can read the request IDs.

    The bridge adds missing sessionId and posthogDistinctId attributes. Existing sessionId and posthogDistinctId attributes take precedence. Set authenticated application identity with posthogDistinctId. Missing IDs stay absent. The optional module keeps your existing provider, exporter, and sampler. It does not change the core SDK dependencies.

    Request context expires when the handler returns or panics. Spans that already started keep their attributes. New spans from an ended request context omit its IDs. For a separate background job, start from a fresh non-request context and set the job's IDs explicitly. Manual WithRequestContext use has no automatic HTTP lifetime management.

  6. Test your setup

    Recommended

    Once everything is configured, confirm spans are reaching PostHog:

    1. Run your application and trigger the instrumented code
    2. Open the PostHog Tracing interface
    3. Confirm your spans and traces appear
    View your traces in PostHog
  7. Next steps

    Checkpoint
    What you can do with your traces

    ActionDescription
    Why you need distributed tracingWhat a trace shows you that nothing else does
    Explore tracesRead a trace as a waterfall to see where time goes
    Filter spansNarrow down by service, status, duration, and attributes
    Propagate contextPass trace context across services so spans join the same trace

    View your traces in PostHog

Still have questions?

Was this page useful?