Skip to content

Commit

Permalink
Merge pull request #2543 from DataDog/tonycthsu/128bits-trace-id-prop…
Browse files Browse the repository at this point in the history
…agation

128 bits trace id
  • Loading branch information
TonyCTHsu authored Mar 3, 2023
2 parents 0e7c257 + b618b70 commit 3514f9f
Show file tree
Hide file tree
Showing 31 changed files with 878 additions and 281 deletions.
1 change: 1 addition & 0 deletions docs/GettingStarted.md
Original file line number Diff line number Diff line change
Expand Up @@ -2140,6 +2140,7 @@ end
| `tracing.sampling.default_rate` | `DD_TRACE_SAMPLE_RATE` | `nil` | Sets the trace sampling rate between `0.0` (0%) and `1.0` (100%). See [Application-side sampling](#application-side-sampling) for details. |
| `tracing.sampling.rate_limit` | `DD_TRACE_RATE_LIMIT` | `100` (per second) | Sets a maximum number of traces per second to sample. Set a rate limit to avoid the ingestion volume overages in the case of traffic spikes. |
| `tracing.sampling.span_rules` | `DD_SPAN_SAMPLING_RULES`,`ENV_SPAN_SAMPLING_RULES_FILE` | `nil` | Sets [Single Span Sampling](#single-span-sampling) rules. These rules allow you to keep spans even when their respective traces are dropped. |
| `tracing.trace_id_128_bit_generation_enabled` | `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` | `false` | `true` to generate 128 bits trace ID and `false` to generate 64 bits trace ID |
| `tracing.report_hostname` | `DD_TRACE_REPORT_HOSTNAME` | `false` | Adds hostname tag to traces. |
| `tracing.test_mode.enabled` | `DD_TRACE_TEST_MODE_ENABLED` | `false` | Enables or disables test mode, for use of tracing in test suites. |
| `tracing.test_mode.trace_flush` | | `nil` | Object that determines trace flushing behavior. |
Expand Down
2 changes: 2 additions & 0 deletions lib/datadog/tracing/configuration/ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ module Configuration
# e.g. Env vars, default values, enums, etc...
module Ext
ENV_ENABLED = 'DD_TRACE_ENABLED'.freeze
ENV_TRACE_ID_128_BIT_GENERATION_ENABLED = 'DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED'.freeze

# @public_api
module Analytics
Expand All @@ -14,6 +15,7 @@ module Analytics
# @public_api
module Correlation
ENV_LOGS_INJECTION_ENABLED = 'DD_LOGS_INJECTION'.freeze
ENV_TRACE_ID_128_BIT_LOGGING_ENABLED = 'DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED'.freeze
end

# @public_api
Expand Down
20 changes: 20 additions & 0 deletions lib/datadog/tracing/configuration/settings.rb
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ def self.extended(base)
o.lazy
end

# Enable 128 bit trace id generation.
#
# @default `DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED` environment variable, otherwise `false`
# @return [Boolean]
option :trace_id_128_bit_generation_enabled do |o|
o.default { env_to_bool(Tracing::Configuration::Ext::ENV_TRACE_ID_128_BIT_GENERATION_ENABLED, false) }
o.lazy
end

# Enable 128 bit trace id injected for logging.
#
# @default `DD_TRACE_128_BIT_TRACEID_LOGGING_ENABLED` environment variable, otherwise `false`
# @return [Boolean]
#
# It is not supported by our backend yet. Do not enable it.
option :trace_id_128_bit_logging_enabled do |o|
o.default { env_to_bool(Tracing::Configuration::Ext::Correlation::ENV_TRACE_ID_128_BIT_LOGGING_ENABLED, false) }
o.lazy
end

# A custom tracer instance.
#
# It must respect the contract of {Datadog::Tracing::Tracer}.
Expand Down
16 changes: 15 additions & 1 deletion lib/datadog/tracing/correlation.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require_relative '../core'
require_relative 'utils'
require_relative 'metadata/ext'

module Datadog
module Tracing
Expand Down Expand Up @@ -64,11 +66,23 @@ def to_log_format
attributes << "#{LOG_ATTR_ENV}=#{env}" unless env.nil?
attributes << "#{LOG_ATTR_SERVICE}=#{service}"
attributes << "#{LOG_ATTR_VERSION}=#{version}" unless version.nil?
attributes << "#{LOG_ATTR_TRACE_ID}=#{trace_id}"
attributes << "#{LOG_ATTR_TRACE_ID}=#{logging_trace_id}"
attributes << "#{LOG_ATTR_SPAN_ID}=#{span_id}"
attributes.join(' ')
end
end

private

def logging_trace_id
@logging_trace_id ||=
if Datadog.configuration.tracing.trace_id_128_bit_logging_enabled &&
!Tracing::Utils::TraceId.to_high_order(@trace_id).zero?
Kernel.format('%032x', trace_id)
else
Tracing::Utils::TraceId.to_low_order(@trace_id)
end
end
end

module_function
Expand Down
17 changes: 12 additions & 5 deletions lib/datadog/tracing/distributed/b3_multi.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require_relative 'helpers'
require_relative '../trace_digest'
require_relative '../utils'

module Datadog
module Tracing
Expand Down Expand Up @@ -45,13 +46,19 @@ def inject!(digest, data = {})
def extract(data)
# DEV: B3 doesn't have "origin"
fetcher = @fetcher.new(data)
trace_id = fetcher.id(@trace_id_key, base: 16)
span_id = fetcher.id(@span_id_key, base: 16)
# We don't need to try and convert sampled since B3 supports 0/1 (AUTO_REJECT/AUTO_KEEP)
sampling_priority = fetcher.number(@sampled_key)

trace_id = Helpers.parse_hex_id(fetcher[@trace_id_key])

# Return early if this propagation is not valid
return unless trace_id && span_id
return if trace_id.nil? || trace_id <= 0 || trace_id > Tracing::Utils::TraceId::MAX

span_id = Helpers.parse_hex_id(fetcher[@span_id_key])

# Return early if this propagation is not valid
return if span_id.nil? || span_id <= 0 || span_id >= Tracing::Utils::EXTERNAL_MAX_ID

# We don't need to try and convert sampled since B3 supports 0/1 (AUTO_REJECT/AUTO_KEEP)
sampling_priority = Helpers.parse_decimal_id(fetcher[@sampled_key])

TraceDigest.new(
trace_id: trace_id,
Expand Down
13 changes: 8 additions & 5 deletions lib/datadog/tracing/distributed/b3_single.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,15 @@ def extract(env)
return unless value

parts = value.split('-')
trace_id = Helpers.value_to_id(parts[0], base: 16) unless parts.empty?
span_id = Helpers.value_to_id(parts[1], base: 16) if parts.length > 1
sampling_priority = Helpers.value_to_number(parts[2]) if parts.length > 2
trace_id = Helpers.parse_hex_id(parts[0]) unless parts.empty?
# Return early if this propagation is not valid
return if trace_id.nil? || trace_id <= 0 || trace_id > Tracing::Utils::TraceId::MAX

# Return if this propagation is not valid
return unless trace_id && span_id
span_id = Helpers.parse_hex_id(parts[1]) if parts.length > 1
# Return early if this propagation is not valid
return if span_id.nil? || span_id <= 0 || span_id >= Tracing::Utils::EXTERNAL_MAX_ID

sampling_priority = Helpers.parse_decimal_id(parts[2]) if parts.length > 2

TraceDigest.new(
span_id: span_id,
Expand Down
68 changes: 58 additions & 10 deletions lib/datadog/tracing/distributed/datadog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
require_relative '../metadata/ext'
require_relative '../trace_digest'
require_relative 'datadog_tags_codec'
require_relative '../utils'
require_relative 'helpers'

module Datadog
module Tracing
Expand Down Expand Up @@ -38,21 +40,26 @@ def initialize(
def inject!(digest, data)
return if digest.nil?

data[@trace_id_key] = digest.trace_id.to_s
data[@trace_id_key] = Tracing::Utils::TraceId.to_low_order(digest.trace_id).to_s

data[@parent_id_key] = digest.span_id.to_s
data[@sampling_priority_key] = digest.trace_sampling_priority.to_s if digest.trace_sampling_priority
data[@origin_key] = digest.trace_origin.to_s if digest.trace_origin

inject_tags(digest, data)
build_tags(digest).tap do |tags|
inject_tags!(tags, data) unless tags.empty?
end

data
end

def extract(data)
fetcher = @fetcher.new(data)
trace_id = fetcher.id(@trace_id_key)
parent_id = fetcher.id(@parent_id_key)
sampling_priority = fetcher.number(@sampling_priority_key)

trace_id = parse_trace_id(fetcher)
parent_id = parse_parent_id(fetcher)

sampling_priority = Helpers.parse_decimal_id(fetcher[@sampling_priority_key])
origin = fetcher[@origin_key]

# Return early if this propagation is not valid
Expand All @@ -63,6 +70,10 @@ def extract(data)

trace_distributed_tags = extract_tags(fetcher)

# If trace id is 128 bits long,
# Concatentated high order 64 bit hex-encoded `tid` tag
trace_id = extract_trace_id!(trace_id, trace_distributed_tags)

TraceDigest.new(
span_id: parent_id,
trace_id: trace_id,
Expand All @@ -74,20 +85,57 @@ def extract(data)

private

def parse_trace_id(fetcher_object)
trace_id = Helpers.parse_decimal_id(fetcher_object[@trace_id_key])

return unless trace_id
return if trace_id <= 0 || trace_id >= Tracing::Utils::EXTERNAL_MAX_ID

trace_id
end

def parse_parent_id(fetcher_object)
parent_id = Helpers.parse_decimal_id(fetcher_object[@parent_id_key])

return unless parent_id
return if parent_id <= 0 || parent_id >= Tracing::Utils::EXTERNAL_MAX_ID

parent_id
end

def build_tags(digest)
high_order = Tracing::Utils::TraceId.to_high_order(digest.trace_id)
tags = digest.trace_distributed_tags || {}

return tags if high_order == 0

tags.merge(Tracing::Metadata::Ext::Distributed::TAG_TID => high_order.to_s(16))
end

# Side effect: Remove high order 64 bit hex-encoded `tid` tag from distributed tags
def extract_trace_id!(trace_id, tags)
return trace_id unless tags
return trace_id unless (high_order = tags.delete(Tracing::Metadata::Ext::Distributed::TAG_TID))

Tracing::Utils::TraceId.concatenate(high_order.to_i(16), trace_id)
end

# Export trace distributed tags through the `x-datadog-tags` key.
#
# DEV: This method accesses global state (the active trace) to record its error state as a trace tag.
# DEV: This means errors cannot be reported if there's not active span.
# DEV: Ideally, we'd have a dedicated error reporting stream for all of ddtrace.
def inject_tags(digest, data)
return if digest.trace_distributed_tags.nil? || digest.trace_distributed_tags.empty?
def inject_tags!(tags, data)
return set_tags_propagation_error(reason: 'disabled') if tags_disabled?

tags = DatadogTagsCodec.encode(digest.trace_distributed_tags)
encoded_tags = DatadogTagsCodec.encode(tags)

return set_tags_propagation_error(reason: 'inject_max_size') if tags_too_large?(tags.size, scenario: 'inject')
return set_tags_propagation_error(reason: 'inject_max_size') if tags_too_large?(
encoded_tags.size,
scenario: 'inject'
)

data[@tags_key] = tags
data[@tags_key] = encoded_tags
rescue => e
set_tags_propagation_error(reason: 'encoding_error')
::Datadog.logger.warn(
Expand Down
8 changes: 0 additions & 8 deletions lib/datadog/tracing/distributed/fetcher.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,6 @@ def initialize(data)
def [](key)
@data[key]
end

def id(key, base: 10)
Helpers.value_to_id(self[key], base: base)
end

def number(key, base: 10)
Helpers.value_to_number(self[key], base: base)
end
end
end
end
Expand Down
54 changes: 21 additions & 33 deletions lib/datadog/tracing/distributed/helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,54 +20,42 @@ def self.clamp_sampling_priority(sampling_priority)
sampling_priority
end

def self.truncate_base16_number(value)
def self.parse_decimal_id(value)
return unless value

value = value.to_s

num = value.to_i

# Ensure the parsed number is the same as the original string value
# e.g. We want to make sure to throw away `'nan'.to_i == 0`
return unless num.to_s(10) == value

num
end

def self.parse_hex_id(value)
return unless value

# Lowercase if we want to parse base16 e.g. 3E8 => 3e8
# DEV: Ruby will parse `3E8` just fine, but to test
# `num.to_s(base) == value` we need to lowercase
value = value.downcase

# Truncate to trailing 16 characters if length is greater than 16
# https://github.com/apache/incubator-zipkin/blob/21fe362899fef5c593370466bc5707d3837070c2/zipkin/src/main/java/zipkin2/storage/StorageComponent.java#L49-L53
# DEV: This ensures we truncate B3 128-bit trace and span ids to 64-bit
value = value[value.length - 16, 16] if value.length > 16
value = value.to_s.downcase

# Remove any leading zeros
# DEV: When we call `num.to_s(16)` later Ruby will not add leading zeros
# for us so we want to make sure the comparision will work as expected
# DEV: regex, remove all leading zeros up until we find the last 0 in the string
# or we find the first non-zero, this allows `'0000' -> '0'` and `'00001' -> '1'`
value.sub(/^0*(?=(0$)|[^0])/, '')
end

def self.value_to_id(value, base: 10)
id = value_to_number(value, base: base)

# Return early if we could not parse a number
return if id.nil?

# Zero or greater than max allowed value of 2**64
return if id.zero? || id > Tracing::Utils::EXTERNAL_MAX_ID

id < 0 ? id + (2**64) : id
end

def self.value_to_number(value, base: 10)
# It's important to make a difference between no data and zero.
return if value.nil?

# Be sure we have a string
value = value.to_s

# If we are parsing base16 number then truncate to 64-bit
value = Helpers.truncate_base16_number(value) if base == 16
value = value.sub(/^0*(?=(0$)|[^0])/, '')

# Convert value to an integer
# DEV: Ruby `.to_i` will return `0` if a number could not be parsed
num = value.to_i(base)
num = value.to_i(16)

# Ensure the parsed number is the same as the original string value
# e.g. We want to make sure to throw away `'nan'.to_i == 0`
return unless num.to_s(base) == value
return unless num.to_s(16) == value

num
end
Expand Down
Loading

0 comments on commit 3514f9f

Please sign in to comment.