datadog 2.41.0 → 2.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +36 -2
  3. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +110 -61
  5. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +141 -103
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  9. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  10. data/ext/datadog_profiling_native_extension/heap_recorder.c +338 -255
  11. data/ext/datadog_profiling_native_extension/heap_recorder.h +46 -31
  12. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +509 -440
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  15. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  16. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  18. data/ext/datadog_profiling_native_extension/stack_recorder.c +93 -61
  19. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  20. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  21. data/ext/libdatadog_api/di.c +10 -0
  22. data/ext/libdatadog_api/extconf.rb +3 -0
  23. data/ext/libdatadog_api/init.c +2 -0
  24. data/ext/libdatadog_api/otel_thread_context.c +232 -0
  25. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  26. data/ext/libdatadog_extconf_helpers.rb +1 -1
  27. data/lib/datadog/appsec/assets/blocked.html +1 -108
  28. data/lib/datadog/core/configuration/components.rb +1 -0
  29. data/lib/datadog/core/crashtracking/component.rb +5 -1
  30. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  31. data/lib/datadog/data_streams/processor.rb +31 -0
  32. data/lib/datadog/di/instrumenter.rb +41 -1
  33. data/lib/datadog/di/logger.rb +2 -2
  34. data/lib/datadog/di/probe.rb +9 -1
  35. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  36. data/lib/datadog/di/remote.rb +3 -3
  37. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  38. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  39. data/lib/datadog/open_feature/ext.rb +19 -0
  40. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  41. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  42. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  43. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  44. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  45. data/lib/datadog/open_feature/provider.rb +11 -2
  46. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  47. data/lib/datadog/opentelemetry/trace.rb +3 -0
  48. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  49. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  50. data/lib/datadog/profiling/component.rb +8 -16
  51. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  52. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  53. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  54. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  55. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  56. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  57. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  58. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  59. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  60. data/lib/datadog/tracing/otel_thread_context.rb +30 -0
  61. data/lib/datadog/tracing/remote.rb +195 -27
  62. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  63. data/lib/datadog/tracing/trace_digest.rb +22 -4
  64. data/lib/datadog/tracing/trace_operation.rb +22 -10
  65. data/lib/datadog/tracing/tracer.rb +5 -5
  66. data/lib/datadog/version.rb +1 -1
  67. metadata +14 -8
  68. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -0,0 +1,233 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ext"
4
+
5
+ module Datadog
6
+ module Tracing
7
+ module Distributed
8
+ class TraceState
9
+ # Encodes and decodes Datadog propagation state.
10
+ class Datadog
11
+ class << self
12
+ def from_digest(digest)
13
+ ts_parent_id = if !digest.span_remote
14
+ format("%016x", digest.span_id || 0)
15
+ elsif digest.trace_distributed_tags&.key?(Tracing::Metadata::Ext::Distributed::TAG_DD_PARENT_ID)
16
+ digest.trace_distributed_tags[Tracing::Metadata::Ext::Distributed::TAG_DD_PARENT_ID]
17
+ end
18
+
19
+ new(
20
+ sampling_priority: digest.trace_sampling_priority,
21
+ origin: digest.trace_origin,
22
+ ts_parent_id: ts_parent_id,
23
+ tags: digest.trace_distributed_tags,
24
+ unknown_fields: digest.trace_state_unknown_fields,
25
+ )
26
+ end
27
+
28
+ def from_tracestate_member(value)
29
+ return new unless value
30
+
31
+ # @type var sampling_priority: ::Integer?
32
+ # @type var origin: ::String?
33
+ # @type var ts_parent_id: ::String?
34
+ # @type var tags: ::Hash[::String, ::String]?
35
+ # @type var unknown_fields: ::String?
36
+ sampling_priority = nil
37
+ origin = nil
38
+ ts_parent_id = nil
39
+ tags = nil
40
+ unknown_fields = nil
41
+
42
+ value.split(";").each do |pair|
43
+ key, field = pair.split(":", 2)
44
+ case key
45
+ when "s"
46
+ sampling_priority = begin
47
+ Integer(field)
48
+ rescue
49
+ nil
50
+ end
51
+ when "o"
52
+ origin = field
53
+ when "p"
54
+ ts_parent_id = field
55
+ when /^t\./
56
+ key = key.to_s
57
+ field = field.to_s
58
+ key.slice!(0..1)
59
+ next if key == Tracing::Metadata::Ext::Distributed::TID
60
+
61
+ field.tr!("~", "=")
62
+ current_tags = tags || {}
63
+ current_tags["#{Tracing::Metadata::Ext::Distributed::TAGS_PREFIX}#{key}"] = field
64
+ tags = current_tags
65
+ else
66
+ current_unknown_fields = unknown_fields || +""
67
+ current_unknown_fields << pair << ";"
68
+ unknown_fields = current_unknown_fields
69
+ end
70
+ end
71
+
72
+ new(
73
+ sampling_priority: sampling_priority,
74
+ origin: origin,
75
+ ts_parent_id: ts_parent_id,
76
+ tags: tags,
77
+ unknown_fields: unknown_fields,
78
+ )
79
+ end
80
+
81
+ # Serializes a {Hash<String,String>} into a `x-datadog-tags`-compatible
82
+ # String.
83
+ #
84
+ # @param tags [Hash<String,String>] trace tag hash
85
+ # @return [String] serialized tags hash
86
+ # @raise [EncodingError] if tags cannot be serialized to the `x-datadog-tags` format
87
+ def encode(tags)
88
+ tags.map do |raw_key, raw_value|
89
+ key = raw_key.to_s
90
+ value = raw_value.to_s
91
+
92
+ unless Ext::Datadog::VALID_KEY_CHARS.match?(key)
93
+ raise EncodingError, "Invalid key `#{key}` for value `#{value}`"
94
+ end
95
+ unless Ext::Datadog::VALID_VALUE_CHARS.match?(value)
96
+ raise EncodingError, "Invalid value `#{value}` for key `#{key}`"
97
+ end
98
+
99
+ "#{key}=#{value.strip}"
100
+ end.join(",")
101
+ rescue => e
102
+ raise EncodingError, "Error encoding tags `#{tags}`: `#{e.class}: #{e.message}`"
103
+ end
104
+
105
+ # Deserializes a `x-datadog-tags`-formatted String into a {Hash<String,String>}.
106
+ #
107
+ # @param string [String] tags as serialized by {#encode}
108
+ # @return [Hash<String,String>] decoded input as a hash of strings
109
+ # @raise [DecodingError] if string does not conform to the `x-datadog-tags` format
110
+ def decode(string)
111
+ result = (string.split(",").map do |raw_tag|
112
+ raw_tag.split("=", 2).tap do |raw_key, raw_value|
113
+ key = raw_key.to_s
114
+ value = raw_value.to_s
115
+
116
+ raise DecodingError, "Invalid key: #{key}" unless Ext::Datadog::VALID_KEY_CHARS.match?(key)
117
+ raise DecodingError, "Invalid value: #{value}" unless Ext::Datadog::VALID_VALUE_CHARS.match?(value)
118
+
119
+ value.strip!
120
+ end
121
+ end).to_h
122
+
123
+ raise DecodingError, "Invalid empty tags: #{string}" if result.empty? && !string.empty?
124
+
125
+ result
126
+ end
127
+ end
128
+
129
+ # An error occurred during distributed tags encoding.
130
+ # See {#message} for more information.
131
+ class EncodingError < StandardError
132
+ end
133
+
134
+ # An error occurred during distributed tags decoding.
135
+ # See {#message} for more information.
136
+ class DecodingError < StandardError
137
+ end
138
+
139
+ attr_reader :sampling_priority, :origin, :ts_parent_id, :tags, :unknown_fields
140
+
141
+ def initialize(sampling_priority: nil, origin: nil, ts_parent_id: nil, tags: nil, unknown_fields: nil)
142
+ @sampling_priority = sampling_priority
143
+ @origin = origin
144
+ @ts_parent_id = ts_parent_id
145
+ @tags = tags
146
+ @unknown_fields = unknown_fields
147
+ end
148
+
149
+ # Builds the `dd=` tracestate member.
150
+ def to_s
151
+ vendor = +"dd="
152
+ if ts_parent_id
153
+ field = "p:#{ts_parent_id};"
154
+ vendor << field if vendor.bytesize + field.bytesize <= (Ext::TRACESTATE_VALUE_SIZE_LIMIT + 1)
155
+ end
156
+ if sampling_priority
157
+ field = "s:#{sampling_priority};"
158
+ vendor << field if vendor.bytesize + field.bytesize <= (Ext::TRACESTATE_VALUE_SIZE_LIMIT + 1)
159
+ end
160
+ if origin
161
+ field = "o:#{serialize_origin(origin)};"
162
+ vendor << field if vendor.bytesize + field.bytesize <= (Ext::TRACESTATE_VALUE_SIZE_LIMIT + 1)
163
+ end
164
+
165
+ # Replacing this with safe navigation causes a LocalJumpError on Rubies <= 3.0.
166
+ if tags # rubocop:disable Style/SafeNavigation
167
+ tags.each do |name, value|
168
+ tag_field = "t.#{serialize_tag_key(name)}:#{serialize_tag_value(value)};"
169
+ break if vendor.bytesize + tag_field.bytesize > (Ext::TRACESTATE_VALUE_SIZE_LIMIT + 1)
170
+
171
+ vendor << tag_field
172
+ end
173
+ end
174
+
175
+ if unknown_fields && vendor.bytesize + unknown_fields.bytesize <= (Ext::TRACESTATE_VALUE_SIZE_LIMIT + 1)
176
+ vendor << unknown_fields
177
+ end
178
+ return "" if vendor.bytesize == 3
179
+
180
+ vendor.chop!
181
+ vendor
182
+ end
183
+
184
+ private
185
+
186
+ # If any characters in <origin_value> are invalid, replace each invalid character with 0x5F (underscore).
187
+ # Invalid characters are: characters outside the ASCII range 0x20 to 0x7E,
188
+ # 0x2C (comma), 0x3B (semi-colon), and 0x7E (tilde).
189
+ # Then, remap 0x3D (equals) to 0x7E (tilde)
190
+ def serialize_origin(value)
191
+ # DEV: It's unlikely that characters will be out of range, as they mostly
192
+ # DEV: come from Datadog-controlled sources.
193
+ # DEV: Trying to `match?` is measurably faster than a `gsub` that does not match.
194
+ invalid_chars = Ext::Datadog::INVALID_ORIGIN_CHARS
195
+ value = invalid_chars.match?(value) ? value.gsub(invalid_chars, "_") : value
196
+ remapped_chars = Ext::Datadog::REMAP_ORIGIN_CHARS
197
+ remapped_chars.match?(value) ? value.gsub(remapped_chars, "~") : value
198
+ end
199
+
200
+ # Serialize `_dd.p.{key}` by first removing the `_dd.p.` prefix.
201
+ # Then replacing invalid characters with `_`.
202
+ #
203
+ # The argument `name` is always frozen.
204
+ # Returns a new String object for the serialized key.
205
+ def serialize_tag_key(name)
206
+ key = name.delete_prefix(Tracing::Metadata::Ext::Distributed::TAGS_PREFIX)
207
+
208
+ # DEV: It's unlikely that characters will be out of range, as they mostly
209
+ # DEV: come from Datadog-controlled sources.
210
+ # DEV: Trying to `match?` is measurably faster than a `gsub!` that does not match.
211
+ invalid_chars = Ext::Datadog::INVALID_TAG_KEY_CHARS
212
+ key.gsub!(invalid_chars, "_") if invalid_chars.match?(key)
213
+ key
214
+ end
215
+
216
+ # Replaces invalid characters with `_`, then replaces `=` with `~`.
217
+ #
218
+ # The argument `value` belongs to {TraceDigest}, thus should not be directly modified.
219
+ # Returns a new String object for the serialized value.
220
+ def serialize_tag_value(value)
221
+ # DEV: It's unlikely that characters will be out of range, as they mostly
222
+ # DEV: come from Datadog-controlled sources.
223
+ # DEV: Trying to `match?` is measurably faster than a `gsub` that does not match.
224
+ invalid_chars = Ext::Datadog::INVALID_TAG_VALUE_CHARS
225
+ serialized = invalid_chars.match?(value) ? value.gsub(invalid_chars, "_") : value
226
+ # DEV: Checking for an unlikely '=' is faster than a no-op `tr`.
227
+ serialized.include?("=") ? serialized.tr("=", "~") : serialized
228
+ end
229
+ end
230
+ end
231
+ end
232
+ end
233
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ require_relative "../../sampling/ext"
6
+
7
+ module Datadog
8
+ module Tracing
9
+ module Distributed
10
+ class TraceState
11
+ module Ext
12
+ TRACESTATE_MAX_SIZE_LIMIT = 512
13
+ TRACESTATE_MAX_LIST_VENDORS = 32
14
+ TRACESTATE_VALUE_SIZE_LIMIT = 256
15
+
16
+ module Datadog
17
+ # ASCII characters 32-126, except `,`, `=`, and ` `. At least one character.
18
+ VALID_KEY_CHARS = /\A(?:(?![,= ])[\u0020-\u007E])+\Z/.freeze
19
+ # ASCII characters 32-126, except `,`. At least one character.
20
+ VALID_VALUE_CHARS = /\A(?:(?!,)[\u0020-\u007E])+\Z/.freeze
21
+ INVALID_ORIGIN_CHARS = /[\u0000-\u0019,;~\u007F-\u{10FFFF}]/.freeze
22
+ REMAP_ORIGIN_CHARS = /=/.freeze
23
+ INVALID_TAG_KEY_CHARS = /[\u0000-\u0020,=\u007F-\u{10FFFF}]/.freeze
24
+ INVALID_TAG_VALUE_CHARS = /[\u0000-\u001F,;\u007E-\u{10FFFF}]/.freeze
25
+ end
26
+
27
+ module OpenTelemetry
28
+ MAX_THRESHOLD = 1 << 56
29
+ MAX_ENCODABLE_THRESHOLD = MAX_THRESHOLD - 1
30
+ UINT64_MODULO = 1 << 64
31
+ UINT64_MASK = UINT64_MODULO - 1
32
+ VALID_THRESHOLD = /\A[0-9a-f]{1,14}\z/
33
+ VALID_RANDOM_VALUE = /\A[0-9a-f]{14}\z/
34
+ NON_PROBABILITY_DECISIONS = Set[
35
+ Sampling::Ext::Decision::MANUAL,
36
+ Sampling::Ext::Decision::ASM,
37
+ Sampling::Ext::Decision::AI_GUARD,
38
+ ].freeze
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../sampling/rate_sampler"
4
+ require_relative "ext"
5
+
6
+ module Datadog
7
+ module Tracing
8
+ module Distributed
9
+ class TraceState
10
+ # Encodes consistent probability sampling in the OpenTelemetry `ot=` tracestate member.
11
+ # @see https://opentelemetry.io/docs/specs/otel/trace/tracestate-probability-sampling/
12
+ class OpenTelemetry
13
+ class << self
14
+ def from_digest(digest, propagate_sampling: true)
15
+ new(
16
+ random_value: (digest.trace_otel_random_value if propagate_sampling),
17
+ threshold: (digest.trace_otel_threshold if propagate_sampling),
18
+ unknown_fields: digest.trace_otel_unknown_fields,
19
+ )
20
+ end
21
+
22
+ # Parses known fields and retains unknown fields for propagation.
23
+ def from_tracestate_member(value)
24
+ return new unless value
25
+
26
+ # @type var random_value: ::String?
27
+ # @type var threshold: ::String?
28
+ random_value = nil
29
+ threshold = nil
30
+ unknown = []
31
+
32
+ value.split(";").each do |pair|
33
+ key, field = pair.split(":", 2)
34
+ case key
35
+ when "rv"
36
+ random_value = field
37
+ when "th"
38
+ threshold = field
39
+ else
40
+ unknown << pair
41
+ end
42
+ end
43
+
44
+ threshold = nil if threshold && !Ext::OpenTelemetry::VALID_THRESHOLD.match?(threshold)
45
+ random_value = nil if random_value && !Ext::OpenTelemetry::VALID_RANDOM_VALUE.match?(random_value)
46
+
47
+ new(
48
+ random_value: random_value,
49
+ threshold: threshold,
50
+ unknown_fields: unknown.empty? ? nil : "#{unknown.join(";")};",
51
+ )
52
+ end
53
+ end
54
+
55
+ attr_reader :random_value, :threshold, :unknown_fields
56
+
57
+ def initialize(random_value: nil, threshold: nil, unknown_fields: nil)
58
+ @random_value = random_value
59
+ @threshold = threshold
60
+ @unknown_fields = unknown_fields
61
+ end
62
+
63
+ # Resolves fields that represent the trace's current sampling decision.
64
+ def outbound(
65
+ trace_id:,
66
+ sampling_priority:,
67
+ decision_maker:,
68
+ applied_rate:,
69
+ rate_limiter_rate:,
70
+ distributed_sampling_priority:
71
+ )
72
+ if Ext::OpenTelemetry::NON_PROBABILITY_DECISIONS.include?(decision_maker)
73
+ return self.class.new(random_value: random_value, unknown_fields: unknown_fields)
74
+ end
75
+
76
+ trace_kept = sampling_priority && sampling_priority >= Sampling::Ext::Priority::AUTO_KEEP
77
+ if rate_limiter_rate && !trace_kept
78
+ return self.class.new(random_value: random_value, unknown_fields: unknown_fields)
79
+ end
80
+
81
+ return self if random_value || threshold
82
+ return self.class.new(unknown_fields: unknown_fields) if distributed_sampling_priority || !applied_rate
83
+
84
+ outbound_threshold = threshold_for(applied_rate)
85
+ outbound_random_value = reconcile_random_value(
86
+ random_value_for(trace_id),
87
+ outbound_threshold,
88
+ !!trace_kept
89
+ )
90
+ self.class.new(
91
+ random_value: format_random_value(outbound_random_value),
92
+ threshold: format_threshold(outbound_threshold),
93
+ unknown_fields: unknown_fields,
94
+ )
95
+ end
96
+
97
+ def to_s
98
+ fields = []
99
+ fields << "rv:#{random_value}" if random_value
100
+ fields << "th:#{threshold}" if threshold
101
+ fields.concat(unknown_fields.chomp(";").split(";")) if unknown_fields
102
+
103
+ value = +""
104
+ fields.each do |field|
105
+ separator = value.empty? ? "" : ";"
106
+ break if value.bytesize + separator.bytesize + field.bytesize > Ext::TRACESTATE_VALUE_SIZE_LIMIT
107
+
108
+ value << separator << field
109
+ end
110
+ value.empty? ? "" : "ot=#{value}"
111
+ end
112
+
113
+ private
114
+
115
+ # Converts Datadog's Knuth hash into OTel's 56-bit random value.
116
+ def random_value_for(trace_id)
117
+ hash = (trace_id * Sampling::RateSampler::KNUTH_FACTOR) % Ext::OpenTelemetry::UINT64_MODULO
118
+ (~hash & Ext::OpenTelemetry::UINT64_MASK) >> 8
119
+ end
120
+
121
+ # Converts a sample rate into a 56-bit rejection threshold.
122
+ def threshold_for(rate)
123
+ threshold = ((1.0 - rate) * Ext::OpenTelemetry::MAX_THRESHOLD).round
124
+ return 0 if threshold < 0
125
+ return Ext::OpenTelemetry::MAX_ENCODABLE_THRESHOLD if threshold > Ext::OpenTelemetry::MAX_ENCODABLE_THRESHOLD
126
+
127
+ threshold
128
+ end
129
+
130
+ # Preserve Datadog's 64-bit decision after expressing it with 56-bit OTel values.
131
+ def reconcile_random_value(random_value, threshold, kept)
132
+ if kept && random_value < threshold
133
+ threshold
134
+ elsif !kept && random_value >= threshold
135
+ [threshold - 1, 0].max
136
+ else
137
+ random_value
138
+ end
139
+ end
140
+
141
+ def format_random_value(random_value)
142
+ format("%014x", random_value)
143
+ end
144
+
145
+ # OTel thresholds omit trailing zero nibbles but retain leading zeroes.
146
+ def format_threshold(threshold)
147
+ return "0" if threshold.zero?
148
+
149
+ trailing_nibbles = ((threshold & -threshold).bit_length - 1) / 4
150
+ (threshold >> (trailing_nibbles * 4)).to_s(16).rjust(14 - trailing_nibbles, "0")
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../core/utils"
4
+ require_relative "trace_state/ext"
5
+ require_relative "trace_state/datadog"
6
+ require_relative "trace_state/open_telemetry"
7
+
8
+ module Datadog
9
+ module Tracing
10
+ module Distributed
11
+ # Parsed vendor state from a W3C `tracestate` header.
12
+ # @api private
13
+ class TraceState
14
+ class << self
15
+ def from_digest(digest, propagate_sampling: true)
16
+ new(
17
+ unknown_vendors: digest.trace_state,
18
+ datadog: Datadog.from_digest(digest),
19
+ open_telemetry: OpenTelemetry.from_digest(digest, propagate_sampling: propagate_sampling),
20
+ )
21
+ end
22
+
23
+ def serialize_digest(digest)
24
+ serialize(
25
+ unknown_vendors: digest.trace_state,
26
+ datadog: Datadog.from_digest(digest),
27
+ open_telemetry: OpenTelemetry.from_digest(digest),
28
+ )
29
+ end
30
+
31
+ # Parses owned members while retaining other vendors unchanged.
32
+ def extract(tracestate)
33
+ vendors = split(tracestate) || []
34
+ datadog_index = vendors.index { |vendor| vendor.start_with?("dd=") }
35
+ datadog_value = vendors.delete_at(datadog_index).delete_prefix("dd=") if datadog_index
36
+ otel_index = vendors.index { |vendor| vendor.start_with?("ot=") }
37
+ otel_value = vendors.delete_at(otel_index).delete_prefix("ot=") if otel_index
38
+
39
+ new(
40
+ unknown_vendors: vendors.empty? ? nil : vendors.join(","),
41
+ datadog: Datadog.from_tracestate_member(datadog_value),
42
+ open_telemetry: OpenTelemetry.from_tracestate_member(otel_value),
43
+ )
44
+ end
45
+
46
+ # Partial members must not be propagated, but complete members within the limits are retained.
47
+ def split(tracestate)
48
+ return if tracestate.nil? || tracestate.empty?
49
+
50
+ remove_last_vendor = false
51
+ if tracestate.bytesize > Ext::TRACESTATE_MAX_SIZE_LIMIT
52
+ remove_last_vendor = tracestate.byteslice(Ext::TRACESTATE_MAX_SIZE_LIMIT, 1) != ","
53
+ tracestate = tracestate.byteslice(0, Ext::TRACESTATE_MAX_SIZE_LIMIT + 1) #: String
54
+ tracestate = tracestate.chop
55
+ end
56
+
57
+ tracestate = ::Datadog::Core::Utils.utf8_encode(tracestate, placeholder: nil)
58
+ return unless tracestate
59
+
60
+ vendors = tracestate.split(",", Ext::TRACESTATE_MAX_LIST_VENDORS + 1)
61
+ vendors.pop if vendors.length > Ext::TRACESTATE_MAX_LIST_VENDORS || remove_last_vendor
62
+ vendors.each(&:strip!)
63
+ vendors.pop while vendors.last == ""
64
+ vendors
65
+ end
66
+
67
+ private
68
+
69
+ def serialize(unknown_vendors:, datadog:, open_telemetry:)
70
+ leading_vendors = select_leading_vendors(datadog, open_telemetry)
71
+ vendors = split(unknown_vendors)
72
+
73
+ if leading_vendors.empty?
74
+ return unless vendors && !vendors.empty?
75
+
76
+ return vendors.join(",")
77
+ end
78
+
79
+ vendors&.reject! { |vendor| vendor.start_with?("dd=", "ot=") }
80
+
81
+ tracestate = leading_vendors.join(",")
82
+ if vendors && !vendors.empty?
83
+ vendors.first(Ext::TRACESTATE_MAX_LIST_VENDORS - leading_vendors.size).each do |vendor|
84
+ break if tracestate.bytesize + vendor.bytesize + 1 > Ext::TRACESTATE_MAX_SIZE_LIMIT
85
+
86
+ tracestate << "," << vendor
87
+ end
88
+ end
89
+
90
+ tracestate
91
+ end
92
+
93
+ def select_leading_vendors(datadog, open_telemetry)
94
+ leading_vendors = []
95
+ add_leading_vendor(leading_vendors, datadog)
96
+ add_leading_vendor(leading_vendors, open_telemetry)
97
+
98
+ leading_vendors
99
+ end
100
+
101
+ def add_leading_vendor(leading_vendors, state)
102
+ vendor = state.to_s
103
+ return if vendor.empty?
104
+
105
+ combined_size = leading_vendors.sum { |current| current.bytesize } +
106
+ leading_vendors.size + vendor.bytesize
107
+ leading_vendors << vendor if combined_size <= Ext::TRACESTATE_MAX_SIZE_LIMIT
108
+ end
109
+ end
110
+
111
+ attr_reader :unknown_vendors, :datadog, :open_telemetry
112
+
113
+ def initialize(unknown_vendors: nil, datadog: nil, open_telemetry: nil)
114
+ @unknown_vendors = unknown_vendors
115
+ @datadog = datadog || Datadog.new
116
+ @open_telemetry = open_telemetry || OpenTelemetry.new
117
+ end
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module Tracing
5
+ # Publishes a per-thread OpenTelemetry context record into a thread-local slot,
6
+ # so an out-of-process reader (e.g. the eBPF profiler) can discover it.
7
+ #
8
+ # See the "OTel Thread Context" OTEP:
9
+ # https://github.com/open-telemetry/opentelemetry-specification/blob/main/oteps/profiles/4947-thread-ctx.md
10
+ #
11
+ # Native functions are implemented in ext/libdatadog_api/otel_thread_context.c
12
+ module OTelThreadContext
13
+ class << self
14
+ def supported?
15
+ Datadog::Core::LIBDATADOG_API_FAILURE.nil? && _native_supported?
16
+ end
17
+
18
+ def enable!
19
+ return false unless supported?
20
+
21
+ _native_enable
22
+ end
23
+
24
+ def set(trace_id:, span_id:, local_root_span_id:)
25
+ _native_set(trace_id, span_id, local_root_span_id)
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end