Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 51 additions & 2 deletions src/OpenTelemetry.Api/Baggage.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ namespace OpenTelemetry;
/// </remarks>
public readonly struct Baggage : IEquatable<Baggage>
{
private static readonly RuntimeContextSlot<BaggageHolder> RuntimeContextSlot = RuntimeContext.RegisterSlot<BaggageHolder>("otel.baggage");
private static readonly RuntimeContextSlot<BaggageHolder?> RuntimeContextSlot = RuntimeContext.RegisterSlot<BaggageHolder?>("otel.baggage");

private static readonly Dictionary<string, string> EmptyBaggage = [];
private static readonly bool UseSafeFlow = !IsUnsafeLegacyFlowEnabled();

private readonly Dictionary<string, string> baggage;

Expand Down Expand Up @@ -56,11 +58,35 @@ internal Baggage(Dictionary<string, string> baggage)
/// // the 'newKey1' change is lost.
/// Baggage.Current = newBaggage;
/// </code>
/// <para/>
/// By default, updates affect only the current execution context and contexts
/// subsequently forked from it. Changes made in an awaited asynchronous method
/// do not flow back to its caller.
/// <para/>
/// Setting the <c>OpenTelemetry.Baggage.AllowUnsafeLegacyFlow</c> AppContext
/// switch to <c>true</c> before the first use of <see cref="Baggage"/> restores
/// legacy sharing of updates across inherited contexts. An unset or
/// <c>false</c> switch keeps the isolated behavior.
/// Legacy sharing is insecure for independent operations because their baggage
/// can overwrite each other's. The setting is read once and is intended only
/// as a temporary migration aid.
/// </remarks>
public static Baggage Current
{
get => RuntimeContextSlot.Get()?.Baggage ?? default;
set => EnsureBaggageHolder().Baggage = value;
set
{
if (UseSafeFlow)
{
// Inherited contexts share the holder reference. Publish a replacement
// instead of mutating it so existing contexts keep their own baggage.
RuntimeContextSlot.Set(value.Count == 0 ? null : new BaggageHolder { Baggage = value });
}
else
{
EnsureBaggageHolder().Baggage = value;
}
}
}

/// <summary>
Expand Down Expand Up @@ -147,6 +173,11 @@ public static Dictionary<string, string>.Enumerator GetEnumerator(Baggage baggag
[SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")]
public static Baggage SetBaggage(string name, string? value, Baggage baggage = default)
{
if (UseSafeFlow)
{
return Current = (baggage == default ? Current : baggage).SetBaggage(name, value);
}

var baggageHolder = EnsureBaggageHolder();
lock (baggageHolder)
{
Expand All @@ -166,6 +197,11 @@ public static Baggage SetBaggage(string name, string? value, Baggage baggage = d
[SuppressMessage("roslyn", "RS0026", Justification = "TODO: fix APIs that violate the backcompt requirement - multiple overloads with optional parameters: https://github.com/dotnet/roslyn/blob/main/docs/Adding%20Optional%20Parameters%20in%20Public%20API.md.")]
public static Baggage SetBaggage(IEnumerable<KeyValuePair<string, string?>> baggageItems, Baggage baggage = default)
{
if (UseSafeFlow)
{
return Current = (baggage == default ? Current : baggage).SetBaggage(baggageItems);
}

var baggageHolder = EnsureBaggageHolder();
lock (baggageHolder)
{
Expand All @@ -184,6 +220,11 @@ public static Baggage SetBaggage(IEnumerable<KeyValuePair<string, string?>> bagg
/// <remarks>Note: The <see cref="Baggage"/> returned will be set as the new <see cref="Current"/> instance.</remarks>
public static Baggage RemoveBaggage(string name, Baggage baggage = default)
{
if (UseSafeFlow)
{
return Current = (baggage == default ? Current : baggage).RemoveBaggage(name);
}

var baggageHolder = EnsureBaggageHolder();
lock (baggageHolder)
{
Expand All @@ -201,6 +242,11 @@ public static Baggage RemoveBaggage(string name, Baggage baggage = default)
/// <remarks>Note: The <see cref="Baggage"/> returned will be set as the new <see cref="Current"/> instance.</remarks>
public static Baggage ClearBaggage(Baggage baggage = default)
{
if (UseSafeFlow)
{
return Current = default;
}

var baggageHolder = EnsureBaggageHolder();
lock (baggageHolder)
{
Expand Down Expand Up @@ -423,6 +469,9 @@ public override int GetHashCode()
return hash;
}

private static bool IsUnsafeLegacyFlowEnabled()
=> AppContext.TryGetSwitch("OpenTelemetry.Baggage.AllowUnsafeLegacyFlow", out var enabled) && enabled;

private static BaggageHolder EnsureBaggageHolder()
{
var baggageHolder = RuntimeContextSlot.Get();
Expand Down
6 changes: 6 additions & 0 deletions src/OpenTelemetry.Api/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ Notes](../../RELEASENOTES.md).

## Unreleased

* **Breaking Change**: Isolate ambient baggage updates between execution contexts.
Changes made in awaited async methods no longer update their caller's baggage.
Enable the `OpenTelemetry.Baggage.AllowUnsafeLegacyFlow` AppContext switch at
startup to temporarily restore insecure legacy sharing.
([#8042](https://github.com/open-telemetry/opentelemetry-dotnet/pull/8042))

## 1.21.0-rc.2

Released 2026-Nov-09
Expand Down
99 changes: 99 additions & 0 deletions src/OpenTelemetry.Api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,105 @@ Baggage.ClearBaggage();
The recommended way to add Baggage is to use the `Baggage.SetBaggage()` API.
OpenTelemetry users should not use the `Activity.AddBaggage` method.

#### Baggage flow across asynchronous calls

By default, `Baggage.Current` flows into child execution contexts as a snapshot.
Assigning `Baggage.Current` or calling the static `SetBaggage`, `RemoveBaggage`,
or `ClearBaggage` methods updates the current execution context and contexts
subsequently forked from it. Previously forked contexts keep their own baggage.
The same behavior applies when a static method receives an explicit `Baggage`
argument. This describes the default `AsyncLocal` runtime context carrier;
applications that select another `RuntimeContext.ContextSlotType` retain that
carrier's propagation behavior.

Changes made inside an awaited asynchronous method do not update the caller's
baggage. To propagate enrichment back to a caller, return the updated baggage
and assign it in the caller's context:

```csharp
Baggage.Current = await EnrichBaggageAsync(Baggage.Current);
await SendRequestAsync();

static async Task<Baggage> EnrichBaggageAsync(Baggage baggage)
{
var tenantId = await LookupTenantIdAsync();
return baggage.SetBaggage("tenant.id", tenantId);
}
```

Returning a new `Baggage` value from an instance method still requires assigning
that value if it should become current. Synchronous helper calls update their
caller's current execution context as before.

##### Temporary compatibility option: insecure legacy flow

Earlier versions shared a mutable baggage holder between inherited execution
contexts. That allowed awaited helpers and parallel tasks to update their
caller's baggage. Applications that require time to migrate can restore that
behavior by enabling the `OpenTelemetry.Baggage.AllowUnsafeLegacyFlow`
[AppContext switch](https://learn.microsoft.com/dotnet/api/system.appcontext)
before the first use of `Baggage`.

For SDK-style .NET applications, add the switch to the application's project
file so the host configures it at startup:

```xml
<ItemGroup>
<RuntimeHostConfigurationOption
Include="OpenTelemetry.Baggage.AllowUnsafeLegacyFlow"
Value="true" />
</ItemGroup>
```

This generates an entry in the application's `.runtimeconfig.json`
`runtimeOptions.configProperties`. For .NET Framework applications that use
`App.config`, configure the switch as follows:

```xml
<configuration>
<runtime>
<AppContextSwitchOverrides
value="OpenTelemetry.Baggage.AllowUnsafeLegacyFlow=true" />
</runtime>
</configuration>
```

For ASP.NET applications on .NET Framework, configure the switch in the
application's `Web.config` using `appSettings`:

```xml
<configuration>
<appSettings>
<add key="AppContext.SetSwitch:OpenTelemetry.Baggage.AllowUnsafeLegacyFlow"
value="true" />
</appSettings>
</configuration>
```

Applications can also set the switch in startup code before any component
uses `Baggage`:

```csharp
AppContext.SetSwitch("OpenTelemetry.Baggage.AllowUnsafeLegacyFlow", true);
```

An unset or `false` switch keeps the isolated behavior. The selected mode is
cached when `Baggage` initializes. Configure it at startup and restart the
application after changing it. SDK configuration, dependency injection, and
later `AppContext.SetSwitch` calls do not update the cached mode.

> [!WARNING]
> The legacy flow is insecure for independent operations. A write can change
> baggage seen by an operation that is already running, its parent, or its siblings.
> This can mix baggage belonging to different users and propagate it with the
> wrong operation. The compatibility option restores this risk; it does not
> provide isolation between requests or messages. Use it only temporarily while
> migrating applications, then remove it.

This changes the default behavior of existing applications. The compatibility
option provides a migration path; enabling it does not make the new default
backward compatible.

## Introduction to OpenTelemetry .NET Tracing API

.NET runtime had `Activity` class for a long time, which was meant to be used
Expand Down
Loading
Loading