Arinti.Langfuse.Tracing.ContextPropagation
1.0.0-alpha.1
dotnet add package Arinti.Langfuse.Tracing.ContextPropagation --version 1.0.0-alpha.1
NuGet\Install-Package Arinti.Langfuse.Tracing.ContextPropagation -Version 1.0.0-alpha.1
<PackageReference Include="Arinti.Langfuse.Tracing.ContextPropagation" Version="1.0.0-alpha.1" />
<PackageVersion Include="Arinti.Langfuse.Tracing.ContextPropagation" Version="1.0.0-alpha.1" />
<PackageReference Include="Arinti.Langfuse.Tracing.ContextPropagation" />
paket add Arinti.Langfuse.Tracing.ContextPropagation --version 1.0.0-alpha.1
#r "nuget: Arinti.Langfuse.Tracing.ContextPropagation, 1.0.0-alpha.1"
#:package Arinti.Langfuse.Tracing.ContextPropagation@1.0.0-alpha.1
#addin nuget:?package=Arinti.Langfuse.Tracing.ContextPropagation&version=1.0.0-alpha.1&prerelease
#tool nuget:?package=Arinti.Langfuse.Tracing.ContextPropagation&version=1.0.0-alpha.1&prerelease
Arinti.Langfuse.Tracing.ContextPropagation
A small library that lets a single .NET process emit two coherent OpenTelemetry trace trees at the same time - one technical (Jaeger/Tempo/…), one semantic (Langfuse) - across async flow and across HTTP boundaries.
The mechanism lives in LangfuseTracing.cs and
LangfuseAwarePropagator.cs. See the
repository README for the project-level rationale; this document focuses on
how a trace flows through the code at runtime.
Core idea in one sentence
Langfuse-relevant spans are created from a dedicated ActivitySource ("Langfuse"), and
LangfuseTracing.StartActivity rewires their parent so each Langfuse span's parent is the
nearest Langfuse ancestor - never a technical span - both in-process (via AsyncLocal)
and across HTTP (via a langfuse entry on the W3C tracestate header).
How a trace flows
sequenceDiagram
autonumber
participant App as App code
participant LT as LangfuseTracing
participant ALocal as AsyncLocal<br/>(LangfuseCurrent)
participant Act as Activity.Current
participant Prop as LangfuseAwarePropagator
participant HC as HttpClient<br/>(technical span)
participant Srv as Server (next process)
Note over App, Act: 1. Inside a technical span (e.g. "Jaeger Workflow")
App ->> Act: AppSource.StartActivity("Jaeger Workflow")
Note over App, Srv: 2. Start a Langfuse span
App ->> LT: StartActivity("Langfuse Workflow")
LT ->> Act: Read Activity.Current → nonLangfuseParent
LT ->> ALocal: Read LangfuseCurrent → langfuseParent
LT ->> LT: ResolveParentContext()<br/>1) local Langfuse ancestor?<br/>2) tracestate from upstream?<br/>3) fall back to non-Langfuse ancestor
LT ->> Act: ActivitySource("Langfuse").StartActivity(parent = resolved)
LT ->> ALocal: LangfuseCurrent = newSpan
LT ->> Act: Activity.Current = nonLangfuseParent<br/>(hide Langfuse span from technical children<br/>and preserve the invariant that Current<br/>is always non-Langfuse when user code runs)
Note over App, Srv: 3. Outbound HTTP - technical span only
App ->> HC: HttpClient.GetAsync(...)
HC ->> Act: starts HTTP span under nonLangfuseParent<br/>(not under Langfuse span - it's hidden)
HC ->> Prop: Inject(activity, carrier, setter)
Prop ->> ALocal: Read LangfuseCurrent.SpanId
Prop ->> HC: traceparent: <technical parent><br/>tracestate: langfuse=p:<spanid>,...
HC ->> Srv: HTTP request
Note over Srv: 4. Server side
Srv ->> Srv: AspNetCore parses tracestate header<br/>onto root ASP.NET Activity.TraceStateString
Srv ->> LT: StartActivity("Langfuse Agent")
LT ->> LT: No local Langfuse ancestor →<br/>rule 2: read tracestate via Activity.TraceStateString<br/>(walks up the parent chain),<br/>parent = remote span id from `langfuse` entry,<br/>trace id = current non-Langfuse ancestor
Note over App, Srv: 5. Langfuse span ends
LT -->> ALocal: ActivityStopped → pop LangfuseCurrent<br/>(via Langfuse.Parent custom property)
Parent resolution rules
flowchart TD
Start([StartActivity called]) --> Q1{Local Langfuse<br/>ancestor in<br/>LangfuseCurrent?}
Q1 -- yes --> R1[Parent = langfuseParent.Context<br/><i>in-process chain stays linked</i>]
Q1 -- no --> Q2{Activity.Current's<br/>TraceStateString has<br/>a `langfuse` entry?}
Q2 -- yes --> R2[Parent = remote span id from tracestate<br/>TraceId = non-Langfuse ancestor's<br/><i>cross-process chain stays linked</i>]
Q2 -- no --> Q3{Any non-Langfuse<br/>ancestor at all?}
Q3 -- yes --> R3[Parent = non-Langfuse ancestor's Context<br/><i>trace id stays continuous, span is a Langfuse root</i>]
Q3 -- no --> R4[default ActivityContext<br/><i>brand-new trace</i>]
Usage
Minimal: a single Langfuse pipeline
// AddLangfuseTracing() adds the "Langfuse" source AND runs LangfuseTracing.Install() to
// register the activity listener and wrap DistributedContextPropagator.Current - idempotent.
TracerProvider langfuse = Sdk.CreateTracerProviderBuilder()
.AddLangfuseTracing()
.AddOtlpExporter(o => { /* Langfuse endpoint + auth */ })
.Build();
// Anywhere in app code - same API surface as ActivitySource.StartActivity:
using Activity? span = LangfuseTracing.StartActivity("Langfuse Agent", ActivityKind.Internal);
span?.SetTag("langfuse.observation.type", "agent");
// ... work ...
Typical: two pipelines side-by-side (Langfuse + technical)
The library is designed for this layout - one process, two TracerProviders, each
subscribed only to its own sources, so every span lands in exactly one backend.
ActivitySource appSource = new("MyApp");
// Technical pipeline: framework + app sources → Jaeger / Tempo / your APM
TracerProvider technical = Sdk.CreateTracerProviderBuilder()
.ConfigureResource(r => r.AddService("my-app"))
.AddHttpClientInstrumentation()
.AddAspNetCoreInstrumentation()
.AddSource(appSource.Name)
.AddOtlpExporter(o => o.Endpoint = new Uri("http://localhost:4317"))
.Build();
// Langfuse pipeline: only the "Langfuse" source → Langfuse OTLP endpoint
TracerProvider langfuse = Sdk.CreateTracerProviderBuilder()
.ConfigureResource(r => r.AddService("my-app"))
.AddLangfuseTracing()
.AddOtlpExporter(o =>
{
o.Endpoint = new Uri("https://cloud.langfuse.com/api/public/otel/v1/traces");
o.Protocol = OtlpExportProtocol.HttpProtobuf;
o.Headers = "Authorization=Basic <base64(pk:sk)>";
})
.Build();
// Now both trees record concurrently from the same call chain:
using (Activity? workflow = appSource.StartActivity("Workflow")) // technical
using (Activity? agent = LangfuseTracing.StartActivity("Agent")) // langfuse
using (Activity? internal_ = appSource.StartActivity("Internal step")) // technical, parented to Workflow
using (Activity? tool = LangfuseTracing.StartActivity("Tool")) // langfuse, parented to Agent
{
// ... work - outbound HTTP calls carry both traceparent and `langfuse=p:<id>` tracestate.
}
Wiring up without a TracerProviderBuilder
If you are configuring tracing outside the OpenTelemetry SDK (for example in low-level
tests that work directly with DistributedContextPropagator), call
LangfuseTracing.Install() directly - it is idempotent and is exactly what
AddLangfuseTracing() invokes under the hood.
LangfuseTracing.Install();
Technical spans keep using their own ActivitySources as normal. The two pipelines remain
fully independent - they just happen to share a trace_id.
What this library is not
- It does not configure exporters, samplers, or instrumentation - that is the host app's job.
- It does not filter, drop, or rewrite spans on export. Routing is done by
AddSource. - It is not a generalized multi-pipeline framework. It implements one specific pattern: a semantic subset tree alongside a technical tree, kept connected across async and HTTP.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net8.0
- Arinti.Langfuse.Tracing.SemanticConventions (>= 1.0.0-alpha.1)
- OpenTelemetry.Api (>= 1.15.3)
-
net9.0
- Arinti.Langfuse.Tracing.SemanticConventions (>= 1.0.0-alpha.1)
- OpenTelemetry.Api (>= 1.15.3)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on Arinti.Langfuse.Tracing.ContextPropagation:
| Package | Downloads |
|---|---|
|
Arinti.Langfuse.Tracing.MEAI
An IChatClient middleware that records a Langfuse `generation` observation per provider request, shaped per OTel GenAI semantic conventions (chat span). Plug into a ChatClientBuilder pipeline; works with any Microsoft.Extensions.AI adapter (OpenAI, Azure OpenAI, Mistral, Gemini, …). |
|
|
Arinti.Langfuse.Tracing.SemanticKernel
OpenTelemetry tracing primitives for Microsoft.SemanticKernel — filters, hooks, and helpers that shape SK pipeline activity into the OTel GenAI span conventions Langfuse ingests natively. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 1.0.0-alpha.1 | 452 | 7/10/2026 |
| 1.0.0-alpha.0 | 185 | 5/21/2026 |