Skip to content

feat(propagation): baggage support and automatic propagation #4505

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Mar 19, 2025
Merged
14 changes: 14 additions & 0 deletions lib/datadog/tracing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,20 @@ def log_correlation
correlation.to_log_format
end

# Returns the baggage for the current trace.
#
# If there is no active trace, a new one is created.
#
# @return [Datadog::Tracing::Distributed::Baggage] The baggage for the current trace.
# @public_api
def baggage
# Baggage should not be dependent on there being an active trace.
# So we create a new TraceOperation if there isn't one.
active_trace = self.active_trace || tracer.continue_trace!(nil)
active_trace.baggage ||= {}
active_trace.baggage
end

# Gracefully shuts down the tracer.
#
# The public tracing API will still respond to method calls as usual
Expand Down
7 changes: 6 additions & 1 deletion lib/datadog/tracing/configuration/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ module Distributed
# W3C Trace Context
PROPAGATION_STYLE_TRACE_CONTEXT = 'tracecontext'

# W3C Baggage
# @see https://www.w3.org/TR/baggage/
PROPAGATION_STYLE_BAGGAGE = 'baggage'

PROPAGATION_STYLE_SUPPORTED = [PROPAGATION_STYLE_DATADOG, PROPAGATION_STYLE_B3_MULTI_HEADER,
PROPAGATION_STYLE_B3_SINGLE_HEADER, PROPAGATION_STYLE_TRACE_CONTEXT].freeze
PROPAGATION_STYLE_B3_SINGLE_HEADER, PROPAGATION_STYLE_TRACE_CONTEXT,
PROPAGATION_STYLE_BAGGAGE].freeze

# Sets both extract and inject propagation style tho the provided value.
# Has lower precedence than `DD_TRACE_PROPAGATION_STYLE_INJECT` or
Expand Down
4 changes: 3 additions & 1 deletion lib/datadog/tracing/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def self.extended(base)
#
# The tracer will try to find distributed headers in the order they are present in the list provided to this option.
# The first format to have valid data present will be used.
#
# Baggage style is a special case, as it will always be extracted in addition if present.
# @default `DD_TRACE_PROPAGATION_STYLE_EXTRACT` environment variable (comma-separated list),
# otherwise `['datadog','b3multi','b3']`.
# @return [Array<String>]
Expand All @@ -53,6 +53,7 @@ def self.extended(base)
[
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_DATADOG,
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT,
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE,
]
)
o.after_set do |styles|
Expand All @@ -74,6 +75,7 @@ def self.extended(base)
o.default [
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_DATADOG,
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT,
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE,
]
o.after_set do |styles|
# Make values case-insensitive
Expand Down
2 changes: 2 additions & 0 deletions lib/datadog/tracing/contrib/grpc/distributed/propagation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def initialize(
Tracing::Distributed::Datadog.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT =>
Tracing::Distributed::TraceContext.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE =>
Tracing::Distributed::Baggage.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_NONE => Tracing::Distributed::None.new
},
propagation_style_inject: propagation_style_inject,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def initialize(
Tracing::Distributed::Datadog.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT =>
Tracing::Distributed::TraceContext.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_NONE => Tracing::Distributed::None.new
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE =>
Tracing::Distributed::Baggage.new(fetcher: Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_NONE => Tracing::Distributed::None.new,
},
propagation_style_inject: propagation_style_inject,
propagation_style_extract: propagation_style_extract,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ def initialize(
Tracing::Distributed::Datadog.new(fetcher: Tracing::Distributed::Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_TRACE_CONTEXT =>
Tracing::Distributed::TraceContext.new(fetcher: Tracing::Distributed::Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_BAGGAGE =>
Tracing::Distributed::Baggage.new(fetcher: Tracing::Distributed::Fetcher),
Tracing::Configuration::Ext::Distributed::PROPAGATION_STYLE_NONE => Tracing::Distributed::None.new
},
propagation_style_inject: propagation_style_inject,
Expand Down
131 changes: 131 additions & 0 deletions lib/datadog/tracing/distributed/baggage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# frozen_string_literal: true

require_relative '../metadata/ext'
require_relative '../trace_digest'
require_relative 'datadog_tags_codec'
require_relative '../utils'
require_relative 'helpers'
require 'uri'

module Datadog
module Tracing
module Distributed
# W3C Baggage propagator implementation.
# The baggage header is propagated through `baggage`.
# @see https://www.w3.org/TR/baggage/
class Baggage
BAGGAGE_KEY = 'baggage'
DD_TRACE_BAGGAGE_MAX_ITEMS = 64
DD_TRACE_BAGGAGE_MAX_BYTES = 8192
SAFE_CHARACTERS_KEY = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$!#&'*+-.^_`|~"
SAFE_CHARACTERS_VALUE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789$!#&'()*+-./:<>?@[]^_`{|}~"

def initialize(
fetcher:,
baggage_key: BAGGAGE_KEY
)
@baggage_key = baggage_key
@fetcher = fetcher
end

def inject!(digest, data)
return if digest.nil? || digest.baggage.nil?

baggage_items = digest.baggage.reject { |k, v| k.nil? || v.nil? }
return if baggage_items.empty?

begin
if baggage_items.size > DD_TRACE_BAGGAGE_MAX_ITEMS
::Datadog.logger.warn('Baggage item limit exceeded, dropping excess items')
baggage_items = baggage_items.first(DD_TRACE_BAGGAGE_MAX_ITEMS)
end

encoded_items = []
total_size = 0

baggage_items.each do |key, value|
item = "#{encode_item(key, SAFE_CHARACTERS_KEY)}=#{encode_item(value, SAFE_CHARACTERS_VALUE)}"
item_size = item.bytesize + (encoded_items.empty? ? 0 : 1) # +1 for comma if not first item
if total_size + item_size > DD_TRACE_BAGGAGE_MAX_BYTES
::Datadog.logger.warn('Baggage header size exceeded, dropping excess items')
break # stop adding items when size limit is reached
end
encoded_items << item
total_size += item_size
end

# edge case where a single item is too large
return if encoded_items.empty?

header_value = encoded_items.join(',')
data[@baggage_key] = header_value
rescue => e
::Datadog.logger.warn("Failed to encode and inject baggage header: #{e.message}")
end
end

def extract(data)
fetcher = @fetcher.new(data)
data = fetcher[@baggage_key]
return unless data

baggage = parse_baggage_header(fetcher[@baggage_key])
return unless baggage

TraceDigest.new(
baggage: baggage,
)
end

private

def encode_item(item, safe_characters)
# Strip whitespace and URL-encode the item
result = URI.encode_www_form_component(item.strip)
# Replace '+' with '%20' for space encoding consistency with W3C spec
result = result.gsub('+', '%20')
# Selectively decode percent-encoded characters that are considered "safe" in W3C Baggage spec
result.gsub(/%[0-9A-F]{2}/) do |encoded|
if encoded.size >= 3 && encoded[1..2] =~ /\A[0-9A-F]{2}\z/
hex_str = encoded[1..2]
next encoded unless hex_str && !hex_str.empty?

# Convert hex representation back to character
char = [hex_str.hex].pack('C')
# Keep the character as-is if it's in the safe character set, otherwise keep it encoded
safe_characters.include?(char) ? char : encoded
else
encoded
end
end
end

# Parses a W3C Baggage header string into a hash of key-value pairs
# The header format follows the W3C Baggage specification:
# - Multiple baggage items are separated by commas
# - Each baggage item is a key-value pair separated by '='
# - Keys and values are URL-encoded
# - Returns an empty hash if the baggage header is malformed
#
# @param baggage_header [String] The W3C Baggage header string to parse
# @return [Hash<String, String>] A hash of decoded baggage items
def parse_baggage_header(baggage_header)
baggage = {}
baggages = baggage_header.split(',')
baggages.each do |key_value|
key, value = key_value.split('=', 2)
# If baggage is malformed, return an empty hash
return {} unless key && value

key = URI.decode_www_form_component(key.strip)
value = URI.decode_www_form_component(value.strip)
return {} if key.empty? || value.empty?

baggage[key] = value
end
baggage
end
end
end
end
end
29 changes: 25 additions & 4 deletions lib/datadog/tracing/distributed/propagation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
require_relative '../trace_digest'
require_relative '../trace_operation'
require_relative '../../core/telemetry/logger'
require_relative 'baggage'

module Datadog
module Tracing
Expand All @@ -26,9 +27,13 @@ def initialize(
)
@propagation_styles = propagation_styles
@propagation_extract_first = propagation_extract_first

@propagation_style_inject = propagation_style_inject.map { |style| propagation_styles[style] }
@propagation_style_extract = propagation_style_extract.map { |style| propagation_styles[style] }

# The baggage propagator is unique in that baggage should always be extracted, if present.
# Therefore we remove it from the `propagation_style_extract` list.
@baggage_propagator = @propagation_style_extract.find { |propagator| propagator.is_a?(Baggage) }
@propagation_style_extract.delete(@baggage_propagator) if @baggage_propagator
end

# inject! populates the env with span ID, trace ID and sampling priority
Expand Down Expand Up @@ -57,9 +62,8 @@ def inject!(digest, data)
end

digest = digest.to_digest if digest.respond_to?(:to_digest)

if digest.trace_id.nil?
::Datadog.logger.debug('Cannot inject distributed trace data: digest.trace_id is nil.')
if digest.trace_id.nil? && digest.baggage.nil?
::Datadog.logger.debug('Cannot inject distributed trace data: digest.trace_id and digest.baggage are both nil.')
return nil
end

Expand Down Expand Up @@ -138,12 +142,29 @@ def extract(data)
"Error extracting distributed trace data. Cause: #{e} Location: #{Array(e.backtrace).first}"
)
end
# Handle baggage after all other styles if present
extracted_trace_digest = propagate_baggage(data, extracted_trace_digest) if @baggage_propagator

extracted_trace_digest
end

private

def propagate_baggage(data, extracted_trace_digest)
if extracted_trace_digest
# Merge with baggage if present
digest = @baggage_propagator.extract(data)
if digest
extracted_trace_digest.merge(baggage: digest.baggage)
else
extracted_trace_digest
end
else
# Baggage is the only style
@baggage_propagator.extract(data)
end
end

def last_datadog_parent_id(headers, tracecontext_tags)
dd_propagator = @propagation_style_extract.find { |propagator| propagator.is_a?(Datadog) }
if tracecontext_tags&.fetch(
Expand Down
11 changes: 9 additions & 2 deletions lib/datadog/tracing/trace_digest.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ class TraceDigest
# This allows later propagation to include those unknown fields, as they can represent future versions of the spec
# sending data through this service. This value ends in a trailing `;` to facilitate serialization.
# @return [String]
# @!attribute [r] baggage
# The W3C "baggage" extracted from a distributed context. This field is a hash of key/value pairs.
# @return [Hash<String,String>]
# TODO: The documentation for the last attribute above won't be rendered.
# TODO: This might be a YARD bug as adding an attribute, making it now second-last attribute, renders correctly.
attr_reader \
Expand All @@ -102,7 +105,8 @@ class TraceDigest
:trace_flags,
:trace_state,
:trace_state_unknown_fields,
:span_remote
:span_remote,
:baggage

def initialize(
span_id: nil,
Expand All @@ -124,7 +128,8 @@ def initialize(
trace_flags: nil,
trace_state: nil,
trace_state_unknown_fields: nil,
span_remote: true
span_remote: true,
baggage: nil
)
@span_id = span_id
@span_name = span_name && span_name.dup.freeze
Expand All @@ -146,6 +151,7 @@ def initialize(
@trace_state = trace_state && trace_state.dup.freeze
@trace_state_unknown_fields = trace_state_unknown_fields && trace_state_unknown_fields.dup.freeze
@span_remote = span_remote
@baggage = baggage && baggage.dup.freeze
freeze
end

Expand Down Expand Up @@ -177,6 +183,7 @@ def merge(field_value_pairs)
trace_state: trace_state,
trace_state_unknown_fields: trace_state_unknown_fields,
span_remote: span_remote,
baggage: baggage
}.merge!(field_value_pairs)
)
end
Expand Down
8 changes: 6 additions & 2 deletions lib/datadog/tracing/trace_operation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ class TraceOperation
:rule_sample_rate,
:sample_rate,
:sampling_priority,
:remote_parent
:remote_parent,
:baggage

attr_reader \
:active_span_count,
Expand Down Expand Up @@ -76,7 +77,8 @@ def initialize(
trace_state: nil,
trace_state_unknown_fields: nil,
remote_parent: false,
tracer: nil
tracer: nil,
baggage: nil

)
# Attributes
Expand All @@ -101,6 +103,7 @@ def initialize(
@trace_state = trace_state
@trace_state_unknown_fields = trace_state_unknown_fields
@tracer = tracer
@baggage = baggage

# Generic tags
set_tags(tags) if tags
Expand Down Expand Up @@ -332,6 +335,7 @@ def to_digest
trace_state: @trace_state,
trace_state_unknown_fields: @trace_state_unknown_fields,
span_remote: @remote_parent && @active_span.nil?,
baggage: @baggage.nil? || @baggage.empty? ? nil : @baggage
).freeze
end

Expand Down
3 changes: 2 additions & 1 deletion lib/datadog/tracing/tracer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,8 @@ def build_trace(digest = nil)
trace_state: digest.trace_state,
trace_state_unknown_fields: digest.trace_state_unknown_fields,
remote_parent: digest.span_remote,
tracer: self
tracer: self,
baggage: digest.baggage
)
else
TraceOperation.new(
Expand Down
1 change: 1 addition & 0 deletions sig/datadog/tracing/configuration/ext.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ module Datadog
PROPAGATION_STYLE_B3_MULTI_HEADER: "b3multi"
PROPAGATION_STYLE_B3_SINGLE_HEADER: "b3"
PROPAGATION_STYLE_TRACE_CONTEXT: "tracecontext"
PROPAGATION_STYLE_BAGGAGE: "baggage"
ENV_PROPAGATION_STYLE: "DD_TRACE_PROPAGATION_STYLE"

ENV_PROPAGATION_STYLE_INJECT: "DD_TRACE_PROPAGATION_STYLE_INJECT"
Expand Down
Loading
Loading