Skip to content
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

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

Merged
merged 30 commits into from
Mar 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a4d5eb2
initial implementation
ZStriker19 Feb 11, 2025
e617a7e
Merge branch 'master' into zachg/baggage_support
ZStriker19 Feb 11, 2025
9d38187
changelog
ZStriker19 Feb 11, 2025
751ca10
trace_digest and ext
ZStriker19 Feb 11, 2025
d993b90
Merge branch 'master' into zachg/baggage_support
ZStriker19 Feb 12, 2025
f7292b3
settings_spec
ZStriker19 Feb 12, 2025
1fdf926
include in already existing tests
ZStriker19 Feb 12, 2025
bcc92f9
check for nil value and merge correctly
ZStriker19 Feb 12, 2025
fce842b
begin baggage specific testing
ZStriker19 Feb 12, 2025
1482be0
modify decoding and encoding logic and add tests
ZStriker19 Feb 13, 2025
bf52570
testing complete
ZStriker19 Feb 19, 2025
64cb8a6
add rbs
ZStriker19 Feb 19, 2025
4dbe74b
change to inject if either trace_id or baggage is present in digest
ZStriker19 Feb 19, 2025
383eeea
Merge branch 'master' into zachg/baggage_support
ZStriker19 Feb 19, 2025
5e720e5
Update CHANGELOG.md
ZStriker19 Feb 20, 2025
244b42f
add public method to access baggage hash
ZStriker19 Feb 20, 2025
11c8256
Merge branch 'zachg/baggage_support' of github.com:DataDog/dd-trace-r…
ZStriker19 Feb 20, 2025
166ea2e
update encoding to fit system-tests
ZStriker19 Feb 20, 2025
53087f8
init new TraceOperation for baggage if there is no active_trace already
ZStriker19 Feb 20, 2025
ec0d718
handle the potential of setting baggage before active trace
ZStriker19 Mar 12, 2025
3b1d0a3
don't parse any baggage if malformed items
ZStriker19 Mar 13, 2025
eb4bde6
Merge branch 'master' into zachg/baggage_support
ZStriker19 Mar 13, 2025
9ec3406
syntax recs
ZStriker19 Mar 13, 2025
641fc9f
ensure valid keys and values while encoding for inject
ZStriker19 Mar 13, 2025
7fbe7b8
docstring for baggage, resolving comments
ZStriker19 Mar 14, 2025
5b99284
typing for steepcheck
ZStriker19 Mar 14, 2025
45a1518
document bagage methods better
ZStriker19 Mar 14, 2025
a1a727b
Merge branch 'master' into zachg/baggage_support
ZStriker19 Mar 14, 2025
087b856
refactor
ZStriker19 Mar 17, 2025
8efab4f
Merge branch 'master' into zachg/baggage_support
ZStriker19 Mar 17, 2025
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
13 changes: 13 additions & 0 deletions lib/datadog/tracing.rb
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,19 @@ 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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As someone without an in-depth knowledge of the tracing part of the library, I am wondering what other attributes of Tracing would cause the trace to be created, and also whether this method is meant to be consumed by customers or by dd-trace-rb internally.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since baggage specifically is supposed to be able to be added independent of a span starting, we needed to make sure we created a traceoperation if there wasn't one already available for it. I don't know of any other attributes for which this must be true. The method is meant to be consumed by customers to access the baggage object. What would you recommend doing to make that more explicit than having # @public_api above it?

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
133 changes: 133 additions & 0 deletions lib/datadog/tracing/distributed/baggage.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# 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'
require 'cgi'

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

# We can't use uri encode because it incorrectly encodes some characters
def encode_item(item, safe_characters)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed it but I am not seeing a unit test for this method in the diff. Given that it performs custom escaping I would think a unit test would be appropriate with at least the special cases handled in the implementation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We test inject and extract with special characters in baggage_spec.rb, is that not sufficient?

# Strip whitespace and URL-encode the item
result = CGI.escape(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
27 changes: 24 additions & 3 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,8 +62,7 @@ def inject!(digest, data)
end

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

if digest.trace_id.nil?
if digest.trace_id.nil? && digest.baggage.nil?
::Datadog.logger.debug('Cannot inject distributed trace data: digest.trace_id is 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 || {}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diff appears to mix having baggage be always present, like on this line, and optional like on line 338 below in the same file. Given that baggage is "always propagated if present" I think it would make more sense to not vivify it absent an explicit instruction, i.e. either an explicit write or the baggage came via propagation, to save on processing effort downstream.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm so keep it nil at the start? However we'll still need to check if it's empty if a customer potentially sets a baggage item, and then removes it, we'd then have an empty hash. With regards to processing effort downstream, I don't think it would make a very large difference since as soon as inject is called for baggage we exit if the baggage value is nil or empty. Let me know if I'm misunderstanding.


# 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