Description
Baggage Proposal
Introduction
Baggage is an API to enable passing key value pairs between contexts for web calls. There are two related specs:
a) W3C baggage propogation https://www.w3.org/TR/baggage/
b) OpenTelemetry baggage API https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/baggage/api.md
https://opentelemetry.io/docs/specs/otel/baggage/api/
Existing Baggage API
.NET has APIs on Activity for managing baggage:
public System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string,string?>> Baggage { get; }
public System.Diagnostics.Activity AddBaggage (string key, string? value);
public System.Diagnostics.Activity SetBaggage (string key, string? value);
The premise is that baggage can be added to an activity. Child activities will inherit the baggage fron the parent, but with a variation of copy on write semantics so that changes to the child are not replicated back to the parent.
Problems with the existing Baggage Support
- The Baggage API on Activity doesn't work as its specified to - Setting a baggage with the same key as an existing item will not override the value, it just adds a new value.
- Baggage is tied to Activity - this seems to be more of a theoretical problem than specific functionality is broken because of it. An implicit root activity is created in .NET even if it is not flagged to be recorded.
- OTel specs baggage to be separate from Tracing, but given .NET has Activity built-in, the concern seems to be more about spec divergance, rather than limiting functionality
- OTel has the concept of scopes for baggage - the scenario for scopes is to be able to create a new scope that does not inherit the baggage of the parent scope, so the parent baggage is not passed to a downstream service when that service is not trusted with that baggage info.
- W3C standard changed the header to "baggage". ASP.NET understands both
baggage
andCorrelation-Context
but http client LegacyPropagator only understands Correlation-Context so is sending the wrong header. - W3C spec for baggage enables multipe related values to be attached to a property name, but are not the value of the property. #5677
- Encoding issues
- Updated OTel Spec Specify allowed characters for Baggage keys and values open-telemetry/opentelemetry-specification#3801
Goals
- Introduce API changes to .NET runtime/extensions that support Baggage in a W3C compliant way and APIs that are sympathetic to the goals expressed in the OTel specifications
- Keep the 3 existing APIs on Activity and map them to the new functionality
- Support propogation
- Reading using both the
baggage
and legacyCorrelation-Context
headers, - writing using the
baggage
header as part of HttpClient (change in functionality)
- Reading using both the
- Enable contexts to be created to enable more control over baggage propogation and data hiding
- Enable the implementation to be wrapped/used by the OTEL.NET library
Non-goals
- Port the existing support from OTel.NET to runtime/extensions
Related Issues
.NET Runtime
- Support W3C Baggage propagation without Activity #103174
- [API Proposal]: System.Diagnostics.ActivityContext.Current ability or similar #86966
- Allow creation of a root Activity when Activity.Current is not null #65528
- Duplicated activity baggage when calling SetBaggage on parent and current activity #59496
- Support W3C Baggage proposed standard in HttpHandlerDiagnosticListener #45496
OpenTelemetry .NET
- BaggagePropagator handle baggage properties #5677
- Baggage propagation is not possible for instrumentations without depending on OpenTelemetry.Api package #5667
Concepts for discussion
This is no where near final, but I want to put a stake in the ground that we can then argue over :-)
immutability
The OTel spec for baggage talks about immutability, which has led Java to create a baggage API using the builder pattern. For example
// Access current baggage with Baggage.current()
// output => context baggage: {}
Baggage currentBaggage = Baggage.current();
System.out.println("current baggage: " + asString(currentBaggage));
// ...or from a Context
currentBaggage = Baggage.fromContext(current());
// Baggage has a variety of methods for manipulating and reading data.
// Convert to builder and add entries:
Baggage newBaggage =
Baggage.current().toBuilder()
.put("shopId", "abc123")
.put("shopName", "opentelemetry-demo", BaggageEntryMetadata.create("metadata"))
.build();
// ...or uncomment to start from empty
// newBaggage = Baggage.empty().toBuilder().put("shopId", "abc123").build();
// output => new baggage: {shopId=abc123(), shopName=opentelemetry-demo(metadata)}
System.out.println("new baggage: " + asString(newBaggage));
The problem with this approach is that it isn't really compatible with the existing Activity.SetBaggage()
set of methods, that I think we should endeavor to keep compatibility with.
The scenario where this is important is when a new baggage context is created, which inherits from the parent context. If there are changes to the parent context after the fork, should those be visible to the child. Eg I set a value in Actviity1, create a child Activity2, and then in another task update Activity1. I think that Activity2 should have a snapshot of the state when it was created.
Currently we try to be clever, and have a heirarchy of baggage state. I am thinking that we can probably achieve a good experience using copy semantics for child contexts. If baggage is a list of key value pairs, which are strings, and strings are immutable in .NET, then copying the list will not be excessive in memory consumption as its just pointers to the string values, which can't change. Within a context, values can be added and changed. Only when a new context is created is a new snapshot created.
The other altrenatives would be:
- always create a copy on every write - is inefficient unless you use a builder pattern, even then unrelated code is going to cause duplication
- have pointers to child baggage contexts and use change notifications to force duplication when non-leaf edits are made
Context storage
The OTel specs are based around the idea of a context, and then baggage and spans are stored as part of the context. In .NET we already have Activity as a built in type, and its automatically created by ASP.NET and (hopefully other infrastructure). I don't think we should go back and re-imagine having Activity as a child of a new context, so we should work with what we have.
My suggestions
- Within the scope of an Activity, store the baggage(s) in the activity.
- if not in an activity, then use an AsyncLocal
Why not just always use an async local? The scenarios for baggage will have a very high overlap with Activity and as most access will be via Activity, and Activities will need to snapshot the baggage on creation, we might as well store the data there.
Proposal
-
Add a
Baggage
class to store baggage data- Has Add/get/set functionality
- Also optional metadata string
- implements IEnumerable<KeyValuePair<string,string?>> so it can be returned by Activity.Baggage
-
Add a
BaggageContext
static class to manage baggage contexts- stores data in Activity or Asynclocal if not in an Activity context
- why? - so we don't need to have as many context's flying around
- static
BaggageContext.Current
would return the current baggage- Activity.Baggage would return
Baggage.Current
- Activity.Baggage would return
- Has Push/Pop semantics for managing the contexts, so new ones can be created with child activities or on demand
- Activity creation will create a new context, using the parent activity (if avaiable) for baggage parent
- Baggage can be dependent on their parent or fresh
- When created from a parent, it does a shallow copy of the parent baggage
- Fresh context will not pull through any values from the parent context
- stores data in Activity or Asynclocal if not in an Activity context
-
Add a new W3C propogator that reads both
baggage
andCorrelation-Context
but serializes using thebaggage
header and is compliant with standards for string serialization/escaping.