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.