datadog 2.41.0 → 2.43.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -1
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/NativeExtensionDesign.md +12 -24
  5. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +120 -61
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +11 -3
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +156 -108
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +7 -3
  9. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +0 -10
  10. data/ext/datadog_profiling_native_extension/extconf.rb +52 -93
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +351 -255
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +50 -31
  13. data/ext/datadog_profiling_native_extension/native_extension_helpers.rb +0 -24
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +518 -442
  15. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +15 -3
  16. data/ext/datadog_profiling_native_extension/profiling.c +2 -0
  17. data/ext/datadog_profiling_native_extension/ruby_helpers.c +1 -79
  18. data/ext/datadog_profiling_native_extension/ruby_helpers.h +0 -7
  19. data/ext/datadog_profiling_native_extension/stack_recorder.c +106 -61
  20. data/ext/datadog_profiling_native_extension/stack_recorder.h +12 -4
  21. data/ext/datadog_profiling_native_extension/time_helpers.h +2 -1
  22. data/ext/libdatadog_api/datadog_ruby_common.h +0 -10
  23. data/ext/libdatadog_api/di.c +10 -0
  24. data/ext/libdatadog_api/extconf.rb +3 -0
  25. data/ext/libdatadog_api/init.c +2 -0
  26. data/ext/libdatadog_api/otel_thread_context.c +247 -0
  27. data/ext/libdatadog_api/otel_thread_context.h +5 -0
  28. data/ext/libdatadog_api/trace_exporter.c +642 -22
  29. data/ext/libdatadog_extconf_helpers.rb +1 -1
  30. data/lib/datadog/appsec/assets/blocked.html +1 -108
  31. data/lib/datadog/core/configuration/components.rb +1 -0
  32. data/lib/datadog/core/crashtracking/component.rb +5 -1
  33. data/lib/datadog/data_streams/pathway_context.rb +20 -22
  34. data/lib/datadog/data_streams/processor.rb +31 -0
  35. data/lib/datadog/di/instrumenter.rb +41 -1
  36. data/lib/datadog/di/logger.rb +2 -2
  37. data/lib/datadog/di/probe.rb +9 -1
  38. data/lib/datadog/di/probe_notification_builder.rb +1 -0
  39. data/lib/datadog/di/remote.rb +3 -3
  40. data/lib/datadog/open_feature/evaluation_engine.rb +29 -3
  41. data/lib/datadog/open_feature/exposures/event.rb +10 -3
  42. data/lib/datadog/open_feature/ext.rb +19 -0
  43. data/lib/datadog/open_feature/flag_evaluation/aggregator.rb +236 -80
  44. data/lib/datadog/open_feature/flag_evaluation/writer.rb +179 -68
  45. data/lib/datadog/open_feature/hooks/flag_eval_evp_hook.rb +24 -21
  46. data/lib/datadog/open_feature/native_evaluator.rb +33 -6
  47. data/lib/datadog/open_feature/noop_evaluator.rb +5 -0
  48. data/lib/datadog/open_feature/provider.rb +11 -2
  49. data/lib/datadog/open_feature/transport.rb +5 -0
  50. data/lib/datadog/opentelemetry/sdk/propagator.rb +1 -1
  51. data/lib/datadog/opentelemetry/trace.rb +3 -0
  52. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +3 -0
  53. data/lib/datadog/profiling/collectors/info.rb +2 -6
  54. data/lib/datadog/profiling/collectors/thread_context.rb +0 -4
  55. data/lib/datadog/profiling/component.rb +8 -16
  56. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +1 -0
  57. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  58. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  59. data/lib/datadog/tracing/distributed/baggage.rb +0 -1
  60. data/lib/datadog/tracing/distributed/datadog.rb +3 -3
  61. data/lib/datadog/tracing/distributed/propagation.rb +3 -0
  62. data/lib/datadog/tracing/distributed/trace_context.rb +14 -271
  63. data/lib/datadog/tracing/distributed/trace_state/datadog.rb +233 -0
  64. data/lib/datadog/tracing/distributed/trace_state/ext.rb +44 -0
  65. data/lib/datadog/tracing/distributed/trace_state/open_telemetry.rb +156 -0
  66. data/lib/datadog/tracing/distributed/trace_state.rb +121 -0
  67. data/lib/datadog/tracing/otel_thread_context.rb +34 -0
  68. data/lib/datadog/tracing/remote.rb +195 -27
  69. data/lib/datadog/tracing/sampling/rule_sampler.rb +2 -0
  70. data/lib/datadog/tracing/trace_digest.rb +22 -4
  71. data/lib/datadog/tracing/trace_operation.rb +22 -10
  72. data/lib/datadog/tracing/tracer.rb +5 -5
  73. data/lib/datadog/tracing/transport/native.rb +17 -25
  74. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  75. data/lib/datadog/tracing/transport/traces.rb +2 -22
  76. data/lib/datadog/version.rb +1 -1
  77. metadata +16 -8
  78. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -69
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../core/utils"
4
3
  require_relative "helpers"
4
+ require_relative "trace_state"
5
5
 
6
6
  module Datadog
7
7
  module Tracing
@@ -30,7 +30,7 @@ module Datadog
30
30
  if (traceparent = build_traceparent(digest))
31
31
  data[@traceparent_key] = traceparent
32
32
 
33
- if (tracestate = build_tracestate(digest))
33
+ if (tracestate = TraceState.serialize_digest(digest))
34
34
  data[@tracestate_key] = tracestate
35
35
  end
36
36
  end
@@ -45,11 +45,12 @@ module Datadog
45
45
 
46
46
  return unless trace_id # Could not parse traceparent
47
47
 
48
- tracestate, sampling_priority, origin, ts_parent_id, tags, unknown_fields = extract_tracestate(
49
- fetcher[@tracestate_key]
50
- )
48
+ trace_state = TraceState.extract(fetcher[@tracestate_key])
49
+ dd = trace_state.datadog
50
+ ot = trace_state.open_telemetry
51
51
 
52
- sampling_priority = parse_priority_sampling(sampled, sampling_priority) do |decision|
52
+ tags = dd.tags
53
+ sampling_priority = parse_priority_sampling(sampled, dd.sampling_priority) do |decision|
53
54
  case decision
54
55
  when String
55
56
  tags ||= {}
@@ -61,17 +62,20 @@ module Datadog
61
62
 
62
63
  tags ||= {}
63
64
  tags[Tracing::Metadata::Ext::Distributed::TAG_DD_PARENT_ID] =
64
- ts_parent_id || Tracing::Metadata::Ext::Distributed::DD_PARENT_ID_DEFAULT
65
+ dd.ts_parent_id || Tracing::Metadata::Ext::Distributed::DD_PARENT_ID_DEFAULT
65
66
 
66
67
  TraceDigest.new(
67
68
  span_id: parent_id,
68
69
  trace_id: trace_id,
69
- trace_origin: origin,
70
+ trace_origin: dd.origin,
70
71
  trace_sampling_priority: sampling_priority,
71
72
  trace_distributed_tags: tags,
72
73
  trace_flags: trace_flags,
73
- trace_state: tracestate,
74
- trace_state_unknown_fields: unknown_fields,
74
+ trace_state: trace_state.unknown_vendors,
75
+ trace_state_unknown_fields: dd.unknown_fields,
76
+ trace_otel_random_value: ot.random_value,
77
+ trace_otel_threshold: ot.threshold,
78
+ trace_otel_unknown_fields: ot.unknown_fields,
75
79
  span_remote: true,
76
80
  )
77
81
  end
@@ -121,133 +125,6 @@ module Datadog
121
125
  trace_flags
122
126
  end
123
127
 
124
- # @see https://www.w3.org/TR/trace-context/#tracestate-header
125
- def build_tracestate(digest)
126
- tracestate = +"dd="
127
- append_dd(tracestate, last_dd_parent_id(digest))
128
- append_dd(tracestate, "s:#{digest.trace_sampling_priority};") if digest.trace_sampling_priority
129
- append_dd(tracestate, "o:#{serialize_origin(digest.trace_origin)};") if digest.trace_origin
130
-
131
- # Replacing this by safe navigation seems to have a different behaviour on Rubies <= 3.0.
132
- # It cause a LocalJumpError in the CI.
133
- if digest.trace_distributed_tags # rubocop:disable Style/SafeNavigation
134
- digest.trace_distributed_tags.each do |name, value|
135
- tag = "t.#{serialize_tag_key(name)}:#{serialize_tag_value(value)};"
136
-
137
- # If tracestate size limit is exceeded, drop the remaining data.
138
- break unless append_dd(tracestate, tag)
139
- end
140
- end
141
-
142
- append_dd(tracestate, digest.trace_state_unknown_fields) if digest.trace_state_unknown_fields
143
- vendors = split_tracestate(digest.trace_state)
144
-
145
- # Is there any Datadog-specific information to propagate.
146
- # Check for > 3 size because the empty prefix `dd=` has 3 characters.
147
- if tracestate.bytesize > 3
148
- # Propagate upstream tracestate with `dd=...` appended to the list
149
- tracestate.chop! # Removes trailing `;` from Datadog trace state string.
150
-
151
- if vendors && !vendors.empty?
152
- # Delete existing `dd=` tracestate fields, if present.
153
- vendors.reject! { |v| v.start_with?("dd=") }
154
-
155
- # Ensure the list has at most 31 elements, as we need to prepend Datadog's
156
- # entry and the limit is 32 elements total.
157
- vendors.first(TRACESTATE_MAX_LIST_MEMBERS - 1).each do |vendor|
158
- break if tracestate.bytesize + vendor.bytesize + 1 > TRACESTATE_MAX_SIZE_LIMIT
159
-
160
- tracestate << "," << vendor
161
- end
162
- end
163
-
164
- tracestate.to_s
165
- elsif vendors && !vendors.empty?
166
- vendors.join(",")
167
- end
168
- end
169
-
170
- # Appends a Datadog tracestate field when it fits.
171
- def append_dd(tracestate, field)
172
- return true if field.empty?
173
-
174
- # We add 1 to the limit because of the trailing semicolon, which will be removed before returning.
175
- return false if tracestate.bytesize + field.bytesize > (TRACESTATE_VALUE_SIZE_LIMIT + 1)
176
-
177
- tracestate << field
178
- true
179
- end
180
-
181
- def last_dd_parent_id(digest)
182
- if !digest.span_remote
183
- span_id = digest.span_id || 0 # Fall back to zero (invalid) if not present
184
- "p:#{format("%016x", span_id)};"
185
- elsif digest.trace_distributed_tags&.key?(Tracing::Metadata::Ext::Distributed::TAG_DD_PARENT_ID)
186
- "p:#{digest.trace_distributed_tags[Tracing::Metadata::Ext::Distributed::TAG_DD_PARENT_ID]};"
187
- else
188
- ""
189
- end
190
- end
191
-
192
- # If any characters in <origin_value> are invalid, replace each invalid character with 0x5F (underscore).
193
- # Invalid characters are: characters outside the ASCII range 0x20 to 0x7E,
194
- # 0x2C (comma), 0x3B (semi-colon), and 0x7E (tilde).
195
- # Then, remap 0x3D (equals) to 0x7E (tilde)
196
- def serialize_origin(value)
197
- # DEV: It's unlikely that characters will be out of range, as they mostly
198
- # DEV: come from Datadog-controlled sources.
199
- # DEV: Trying to `match?` is measurably faster than a `gsub` that does not match.
200
- value = if INVALID_ORIGIN_CHARS.match?(value)
201
- value.gsub(INVALID_ORIGIN_CHARS, "_")
202
- else
203
- value
204
- end
205
-
206
- if REMAP_ORIGIN_CHARS.match?(value)
207
- value.gsub(REMAP_ORIGIN_CHARS, "~")
208
- else
209
- value
210
- end
211
- end
212
-
213
- # Serialize `_dd.p.{key}` by first removing the `_dd.p.` prefix.
214
- # Then replacing invalid characters with `_`.
215
- #
216
- # The argument `name` is always frozen.
217
- # Returns a new String object for the serialized key.
218
- def serialize_tag_key(name)
219
- key = name.delete_prefix(Tracing::Metadata::Ext::Distributed::TAGS_PREFIX)
220
-
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
- key.gsub!(INVALID_TAG_KEY_CHARS, "_") if INVALID_TAG_KEY_CHARS.match?(key)
225
-
226
- key
227
- end
228
-
229
- # Replaces invalid characters with `_`, then replaces `=` with `~`.
230
- #
231
- # The argument `value` belongs to {TraceDigest}, thus should not be directly modified.
232
- # Returns a new String object for the serialized value.
233
- def serialize_tag_value(value)
234
- # DEV: It's unlikely that characters will be out of range, as they mostly
235
- # DEV: come from Datadog-controlled sources.
236
- # DEV: Trying to `match?` is measurably faster than a `gsub` that does not match.
237
- ret = if INVALID_TAG_VALUE_CHARS.match?(value)
238
- value.gsub(INVALID_TAG_VALUE_CHARS, "_")
239
- else
240
- value
241
- end
242
-
243
- # DEV: Checking for an unlikely '=' is faster than a no-op `tr`.
244
- if ret.include?("=")
245
- ret.tr("=", "~")
246
- else
247
- ret
248
- end
249
- end
250
-
251
128
  def extract_traceparent(traceparent)
252
129
  trace_id, parent_id, trace_flags = parse_traceparent_string(traceparent)
253
130
 
@@ -285,78 +162,6 @@ module Datadog
285
162
  trace_flags & TRACE_FLAGS_SAMPLED
286
163
  end
287
164
 
288
- # @return [nil] when the tracestate is absent or has no vendor entries.
289
- # @return [String] when no `dd=` entry is present, the joined vendor list.
290
- # @return [Array(String, Integer, String, String, Hash, String)] when a `dd=`
291
- # entry is present: [tracestate without `dd=`, sampling_priority, origin,
292
- # ts_parent_id, tags, unknown_fields]. All elements past the first may be nil.
293
- def extract_tracestate(tracestate)
294
- vendors = split_tracestate(tracestate)
295
- return unless vendors && !vendors.empty?
296
-
297
- tracestate = vendors.join(",")
298
-
299
- # Find Datadog's `dd=` tracestate field.
300
- idx = vendors.index { |v| v.start_with?("dd=") }
301
- return tracestate unless idx
302
-
303
- # Delete `dd=` prefix
304
- dd_tracestate = vendors.delete_at(idx)
305
- dd_tracestate.slice!(0..2)
306
-
307
- origin, sampling_priority, ts_parent_id, tags, unknown_fields = extract_datadog_fields(dd_tracestate)
308
-
309
- [vendors.join(","), sampling_priority, origin, ts_parent_id, tags, unknown_fields]
310
- end
311
-
312
- def extract_datadog_fields(dd_tracestate)
313
- sampling_priority = nil
314
- origin = nil
315
- ts_parent_id = nil
316
- tags = nil
317
- unknown_fields = nil
318
-
319
- # DEV: Since Ruby 2.6 `split` can receive a block, so `each` can be removed then.
320
- dd_tracestate.split(";").each do |pair|
321
- key, value = pair.split(":", 2)
322
- case key
323
- when "s"
324
- sampling_priority = begin
325
- Integer(value)
326
- rescue
327
- nil
328
- end
329
- when "o"
330
- origin = value
331
- when "p"
332
- ts_parent_id = value
333
- when /^t\./
334
- key.slice!(0..1) # Delete `t.` prefix
335
-
336
- # Ignore the high order 64 bit trace id propagation tag to avoid confusion,
337
- # the single source of truth is from traceparent
338
- next if key == Tracing::Metadata::Ext::Distributed::TID
339
-
340
- value = deserialize_tag_value(value)
341
-
342
- tags ||= {}
343
- tags["#{Tracing::Metadata::Ext::Distributed::TAGS_PREFIX}#{key}"] = value
344
- else
345
- unknown_fields ||= +""
346
- unknown_fields << pair
347
- unknown_fields << ";"
348
- end
349
- end
350
-
351
- [origin, sampling_priority, ts_parent_id, tags, unknown_fields]
352
- end
353
-
354
- # Restore `~` back to `=`.
355
- def deserialize_tag_value(value)
356
- value.tr!("~", "=")
357
- value
358
- end
359
-
360
165
  # If `sampled` and `sampling_priority` disagree, `sampled` overrides the decision.
361
166
  # @return [Integer] one of the {Datadog::Tracing::Sampling::Ext::Priority} values
362
167
  # @yieldparam the new decision maker (either :drop or a new decision maker String value).
@@ -382,47 +187,9 @@ module Datadog
382
187
  end
383
188
  end
384
189
 
385
- # We MUST NOT propagate partial members, but we SHOULD try
386
- # to parse as much of the tracestate as possible.
387
- def split_tracestate(tracestate)
388
- return unless tracestate
389
-
390
- remove_last_member = false
391
- if tracestate.bytesize > TRACESTATE_MAX_SIZE_LIMIT
392
- # We parse 1 byte over the limit to detect if the last member
393
- # is a partial member (toss) or ends exactly at the limit (keep).
394
- remove_last_member = tracestate.byteslice(TRACESTATE_MAX_SIZE_LIMIT, 1) != ","
395
-
396
- # To ensure we don't have a trailing partial UTF-8 codepoint, we keep one extra byte
397
- # and safely remove it with `#chop`.
398
- # `#chop` walks back the string until it finds a valid character boundary and deletes
399
- # from there.
400
- tracestate = tracestate.byteslice(0, TRACESTATE_MAX_SIZE_LIMIT + 1) #: String
401
- tracestate = tracestate.chop
402
- end
403
-
404
- tracestate = ::Datadog::Core::Utils.utf8_encode(tracestate, placeholder: nil)
405
- return unless tracestate
406
-
407
- vendors = tracestate.split(",", TRACESTATE_MAX_LIST_MEMBERS + 1)
408
- if vendors.length > TRACESTATE_MAX_LIST_MEMBERS || remove_last_member
409
- vendors.pop
410
- end
411
-
412
- vendors.each(&:strip!)
413
- vendors.pop while vendors.last == ""
414
- vendors
415
- end
416
-
417
190
  TRACEPARENT_MAX_SIZE_LIMIT = 512
418
191
  private_constant :TRACEPARENT_MAX_SIZE_LIMIT
419
192
 
420
- TRACESTATE_MAX_SIZE_LIMIT = 512
421
- private_constant :TRACESTATE_MAX_SIZE_LIMIT
422
-
423
- TRACESTATE_MAX_LIST_MEMBERS = 32
424
- private_constant :TRACESTATE_MAX_LIST_MEMBERS
425
-
426
193
  # Version 0xFF is invalid as per spec
427
194
  # @see https://www.w3.org/TR/trace-context/#version
428
195
  INVALID_VERSION = "ff"
@@ -437,30 +204,6 @@ module Datadog
437
204
  # @see https://www.w3.org/TR/trace-context/#trace-flags
438
205
  TRACE_FLAGS_SAMPLED = 0b00000001
439
206
  private_constant :TRACE_FLAGS_SAMPLED
440
-
441
- # The limit is inclusive: sizes *greater* than 256 are disallowed.
442
- # @see https://www.w3.org/TR/trace-context/#value
443
- TRACESTATE_VALUE_SIZE_LIMIT = 256
444
- private_constant :TRACESTATE_VALUE_SIZE_LIMIT
445
-
446
- # Replace all characters with `_`, except ASCII characters 0x20-0x7E.
447
- # Additionally, `,`, ';', and `~` must also be replaced by `_`.
448
- INVALID_ORIGIN_CHARS = /[\u0000-\u0019,;~\u007F-\u{10FFFF}]/.freeze
449
- private_constant :INVALID_ORIGIN_CHARS
450
-
451
- # Additionally, remap `=` to `~`
452
- REMAP_ORIGIN_CHARS = /=/.freeze
453
- private_constant :REMAP_ORIGIN_CHARS
454
-
455
- # Replace all characters with `_`, except ASCII characters 0x21-0x7E.
456
- # Additionally, `,` and `=` must also be replaced by `_`.
457
- INVALID_TAG_KEY_CHARS = /[\u0000-\u0020,=\u007F-\u{10FFFF}]/.freeze
458
- private_constant :INVALID_TAG_KEY_CHARS
459
-
460
- # Replace all characters with `_`, except ASCII characters 0x20-0x7D.
461
- # Additionally, `,` and `;` must also be replaced by `_`.
462
- INVALID_TAG_VALUE_CHARS = /[\u0000-\u001F,;\u007E-\u{10FFFF}]/.freeze
463
- private_constant :INVALID_TAG_VALUE_CHARS
464
207
  end
465
208
  end
466
209
  end
@@ -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