Using an existing OpenTelemetry provider

August 15, 2026 ยท View on GitHub

Use borrowed-provider mode when the application already owns an *sdktrace.TracerProvider and needs the same spans sent to a generic backend and Langfuse.

cfg := langfuse.ConfigFromEnv()
cfg.TracerProvider = provider

lf, err := langfuse.New(ctx, cfg)
if err != nil {
	return err
}

The client registers one additional processor. It does not replace the global provider, sampler, resource, or existing processors. The application's sampler remains authoritative: non-recording or record-only spans are not exported to Langfuse.

Call WithTraceAttributes before starting provider spans that need trace name, user, session, tags, metadata, or version. The Langfuse processor writes these attributes, plus configured environment and release, at span start. Because processors share an SDK span, existing exporters see those annotations too.

The smart filter forwards SDK observations, spans with gen_ai.* attributes, and spans from known LLM instrumentation scopes. It does not prevent another processor from receiving unrelated spans. Attributes added at span end can make a span exportable, but cannot retroactively change its start-time application- root decision.

Config.ShouldExportSpan replaces the smart filter when set. Set it before calling New. The callback can run at span start and end, must be concurrency-safe, side-effect-free, and non-blocking. A start-time panic omits application-root classification; an end-time panic rejects export. Compose with the exported default when extending the standard policy:

cfg.ShouldExportSpan = func(span sdktrace.ReadOnlySpan) bool {
	return langfuse.IsDefaultExportSpan(span) || span.InstrumentationScope().Name == "my.framework"
}

To export only observations created by this go-langfuse client while retaining a borrowed provider, set cfg.ShouldExportSpan = langfuse.IsLangfuseSpan. This affects only the Langfuse processor. Other processors still receive SDK observations, and the Langfuse processor still adds propagated attributes at span start. A span accepted by the filter at start can also retain the internal application-root marker in other exporters even if the filter rejects it at end. Use an isolated provider when destination isolation is required.

Content controls on Config are not provider-wide scrubbers. DisableContentCapture drops only SDK-supplied observation input/output. Mask receives a MaskField with SDK-supplied observation input/output and observation, trace, or score metadata. It does not receive names, IDs, tags, model parameters, status/error text, score comments or values, or provider/framework attributes and events. In particular, RecordError exports err.Error() without masking. Configure third-party instrumentation independently and use payload-free error/status values.

SDK observation scopes carry the project public key so each processor can reject another project's SDK spans; other exporters on the shared provider see that identifier as well. The secret key is never attached to telemetry.

The caller owns span limits. If they are unusually low, OpenTelemetry may drop SDK fields; the client emits a payload-free diagnostic when it detects that on an SDK observation.

Only one active client is supported on a borrowed provider. A duplicate construction returns ErrTracerProviderInUse. The returned client is nil. This prevents unscoped AI spans from going to two projects.

Borrowed mode batches accepted spans with the standard OpenTelemetry geometry: a 2048-span queue and up to 512 spans per export. One Langfuse HTTP request is capped at 4 MiB; oversized batches are split across requests so an oversized third-party span cannot discard otherwise-valid spans, and only a span that alone exceeds the cap is dropped. The SDK does not sanitize or copy arbitrary third-party attributes and events; their size and custom serialization remain caller-owned. The queue drops newly ended spans when full by default; Config.MaxQueueSize resizes it and Config.BlockOnQueueFull opts into blocking backpressure, so configure instrumentor/provider limits and watch for sustained export saturation.

Path-prefixed reverse-proxy base URLs (for example https://gw.example.com/langfuse/api/public/otel) are not supported in v0.1.

At graceful shutdown, stop the Langfuse client before the application-owned provider:

langfuseCtx, cancelLangfuse := context.WithTimeout(context.Background(), 5*time.Second)
langfuseErr := lf.Shutdown(langfuseCtx)
cancelLangfuse()

providerCtx, cancelProvider := context.WithTimeout(context.Background(), 5*time.Second)
providerErr := provider.Shutdown(providerCtx)
cancelProvider()

return errors.Join(langfuseErr, providerErr)

Create each timeout context immediately before its lifecycle call. Reusing the Langfuse client's context for provider shutdown can leave the provider no time if the first shutdown consumes the deadline.

Client.Shutdown flushes and stops its batch processor with the supplied context, then unregisters it. It never shuts down another exporter. A concurrent or re-entrant call returns ErrShutdownInProgress. A call after shutdown completes returns the stored result from the first call. End all active spans before shutdown; Flush can export only spans that have ended.

See the compiled existing-provider example.