datadog 2.33.0 → 2.34.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 (80) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +30 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +20 -0
  4. data/ext/datadog_profiling_native_extension/macos_sampler_thread.h +55 -0
  5. data/lib/datadog/appsec/component.rb +4 -1
  6. data/lib/datadog/appsec/compressed_json.rb +2 -2
  7. data/lib/datadog/appsec/contrib/aws_lambda/waf_addresses.rb +3 -3
  8. data/lib/datadog/appsec/contrib/rack/ext.rb +1 -1
  9. data/lib/datadog/core/configuration/components.rb +8 -1
  10. data/lib/datadog/core/configuration/settings.rb +6 -1
  11. data/lib/datadog/core/configuration/supported_configurations.rb +10 -0
  12. data/lib/datadog/core/environment/ext.rb +4 -0
  13. data/lib/datadog/core/environment/identity.rb +15 -1
  14. data/lib/datadog/core/environment/process.rb +48 -27
  15. data/lib/datadog/core/remote/client/capabilities.rb +11 -2
  16. data/lib/datadog/core/remote/transport/http/config.rb +5 -5
  17. data/lib/datadog/core/telemetry/request.rb +0 -2
  18. data/lib/datadog/core/transport/response.rb +1 -1
  19. data/lib/datadog/core/utils/{base64.rb → base64_codec.rb} +3 -2
  20. data/lib/datadog/core/utils/hash.rb +0 -23
  21. data/lib/datadog/core/utils/spawn_monkey_patch.rb +46 -16
  22. data/lib/datadog/data_streams/pathway_context.rb +3 -3
  23. data/lib/datadog/di/code_tracker.rb +43 -22
  24. data/lib/datadog/di/contrib/active_record.rb +6 -2
  25. data/lib/datadog/di/instrumenter.rb +24 -4
  26. data/lib/datadog/di/probe_notification_builder.rb +1 -1
  27. data/lib/datadog/di/remote.rb +4 -4
  28. data/lib/datadog/di/serializer.rb +5 -5
  29. data/lib/datadog/di/utils.rb +42 -14
  30. data/lib/datadog/opentelemetry/configuration/settings.rb +65 -0
  31. data/lib/datadog/opentelemetry/ext.rb +9 -0
  32. data/lib/datadog/opentelemetry/logs.rb +98 -0
  33. data/lib/datadog/opentelemetry/metrics.rb +10 -46
  34. data/lib/datadog/opentelemetry/sdk/configurator.rb +40 -0
  35. data/lib/datadog/opentelemetry/sdk/logs_exporter.rb +37 -0
  36. data/lib/datadog/opentelemetry/signal_configuration.rb +53 -0
  37. data/lib/datadog/opentelemetry.rb +1 -0
  38. data/lib/datadog/symbol_database/component.rb +409 -0
  39. data/lib/datadog/symbol_database/configuration.rb +2 -2
  40. data/lib/datadog/symbol_database/extractor.rb +29 -1
  41. data/lib/datadog/symbol_database/remote.rb +175 -0
  42. data/lib/datadog/symbol_database/scope_batcher.rb +8 -0
  43. data/lib/datadog/symbol_database/service_version.rb +11 -2
  44. data/lib/datadog/symbol_database/symbol.rb +6 -3
  45. data/lib/datadog/symbol_database/uploader.rb +62 -8
  46. data/lib/datadog/tracing/contrib/action_pack/action_dispatch/instrumentation.rb +8 -0
  47. data/lib/datadog/tracing/contrib/active_record/events/sql.rb +0 -4
  48. data/lib/datadog/tracing/contrib/active_support/cache/events/cache.rb +0 -4
  49. data/lib/datadog/tracing/contrib/active_support/cache/instrumentation.rb +0 -4
  50. data/lib/datadog/tracing/contrib/aws/instrumentation.rb +0 -5
  51. data/lib/datadog/tracing/contrib/dalli/instrumentation.rb +0 -5
  52. data/lib/datadog/tracing/contrib/elasticsearch/patcher.rb +0 -5
  53. data/lib/datadog/tracing/contrib/ethon/easy_patch.rb +0 -5
  54. data/lib/datadog/tracing/contrib/ethon/multi_patch.rb +0 -8
  55. data/lib/datadog/tracing/contrib/excon/middleware.rb +0 -5
  56. data/lib/datadog/tracing/contrib/ext.rb +2 -3
  57. data/lib/datadog/tracing/contrib/faraday/middleware.rb +0 -5
  58. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/client.rb +0 -5
  59. data/lib/datadog/tracing/contrib/grpc/datadog_interceptor/server.rb +0 -5
  60. data/lib/datadog/tracing/contrib/http/instrumentation.rb +0 -5
  61. data/lib/datadog/tracing/contrib/httpclient/instrumentation.rb +0 -5
  62. data/lib/datadog/tracing/contrib/httprb/instrumentation.rb +0 -5
  63. data/lib/datadog/tracing/contrib/mongodb/subscribers.rb +0 -5
  64. data/lib/datadog/tracing/contrib/mysql2/instrumentation.rb +0 -5
  65. data/lib/datadog/tracing/contrib/opensearch/patcher.rb +0 -5
  66. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +0 -5
  67. data/lib/datadog/tracing/contrib/presto/instrumentation.rb +0 -5
  68. data/lib/datadog/tracing/contrib/racecar/event.rb +0 -5
  69. data/lib/datadog/tracing/contrib/redis/tags.rb +0 -5
  70. data/lib/datadog/tracing/contrib/rest_client/request_patch.rb +0 -5
  71. data/lib/datadog/tracing/contrib/sequel/utils.rb +0 -5
  72. data/lib/datadog/tracing/contrib/trilogy/instrumentation.rb +0 -5
  73. data/lib/datadog/tracing/distributed/datadog_tags_codec.rb +0 -13
  74. data/lib/datadog/tracing/distributed/trace_context.rb +0 -28
  75. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  76. data/lib/datadog/tracing/span_operation.rb +13 -0
  77. data/lib/datadog/tracing/trace_operation.rb +22 -0
  78. data/lib/datadog/tracing/tracer.rb +6 -0
  79. data/lib/datadog/version.rb +1 -1
  80. metadata +12 -5
@@ -3,32 +3,62 @@
3
3
  module Datadog
4
4
  module Core
5
5
  module Utils
6
+ # Applies the Process.spawn wrapper used to merge additional environment variables
7
+ # into child processes.
6
8
  module SpawnMonkeyPatch
7
- # @param lineage_envs_provider [#call] returns a Hash of env vars to merge into the child process
8
- def self.apply!(lineage_envs_provider:)
9
- @lineage_envs_provider = lineage_envs_provider
9
+ # @param env_provider [#call] returns a Hash of env vars to merge into the child process
10
+ def self.apply!(env_provider:)
11
+ @env_provider = env_provider
12
+
13
+ # Idempotent: tests, reloads, or repeated Components init must not stack prepends.
14
+ return if ::Process.singleton_class.ancestors.include?(ProcessSpawnPatch)
15
+
10
16
  ::Process.singleton_class.prepend(ProcessSpawnPatch)
11
- true
12
17
  end
13
18
 
19
+ # Prepends `Process.spawn` to merge `env_provider` output into the child's environment hash.
14
20
  module ProcessSpawnPatch
15
- def spawn(*args, **opts)
16
- args.replace(SpawnMonkeyPatch.inject_lineage_envs(args))
17
- super
21
+ # The One and Only Correct Delegation Pattern
22
+ if RUBY_VERSION >= '3'
23
+ def spawn(*args, **kwargs) # steep:ignore DifferentMethodParameterKind
24
+ super(*SpawnMonkeyPatch.inject_envs(args), **kwargs)
25
+ end
26
+ else
27
+ def spawn(*args)
28
+ super(*SpawnMonkeyPatch.inject_envs(args))
29
+ end
30
+ ruby2_keywords :spawn if respond_to?(:ruby2_keywords, true)
18
31
  end
19
32
  end
20
33
 
21
- # Process.spawn(env?, cmd, ...): env is optional first arg (Hash). When present, merge
22
- # runtime_ids into it; when absent, prepend full ENV + runtime_ids so the child inherits both.
23
- def self.inject_lineage_envs(args)
24
- runtime_ids = @lineage_envs_provider.call
25
- env_provided = Hash === args.first
34
+ # Merge the env vars from `env_provider` with the optional env `Hash` from {Process.spawn}.
35
+ #
36
+ # `env` is the first argument when it is a {Hash}; see MRI `spawn([env, ] *args, options)`:
37
+ # https://docs.ruby-lang.org/en/master/Process.html#method-c-spawn
38
+ #
39
+ # When there is **no** leading env Hash, MRI inherits the parent's `ENV`; we prepend only the
40
+ # `env_provider` hash so spawned children see parent env plus injections.
41
+ #
42
+ # When callers pass `unsetenv_others: true`, MRI only forwards the explicitly passed env Hash;
43
+ # replacing a missing hash with DATADOG_ENV.to_h would wrongly carry over parent variables.
44
+ # Prepending only the provider hash preserves `unsetenv_others` semantics.
45
+ #
46
+ # See https://docs.ruby-lang.org/en/master/Process.html#module-Process-label-Environment+Variables+-28-3Aunsetenv_others-29
47
+ #
48
+ # NOTE: `::Hash` (not bare `Hash`) is required because this module is nested under
49
+ # `Datadog::Core::Utils`, and `Datadog::Core::Utils::Hash` exists.
50
+ # Bare `Hash` resolves to that module via Module.nesting, making `Hash === some_hash`
51
+ # silently return `false`. See https://github.com/DataDog/dd-trace-rb/issues/5621.
52
+ def self.inject_envs(args)
53
+ provided_env = @env_provider.call
26
54
 
27
- base_env = env_provided ? args.first : DATADOG_ENV.to_h
28
- env = base_env.merge(runtime_ids)
29
- rest = env_provided ? args.drop(1) : args
55
+ if ::Hash === args.first
56
+ args[0] = args.first.merge(provided_env)
57
+ else
58
+ args.unshift(provided_env)
59
+ end
30
60
 
31
- [env, *rest]
61
+ args
32
62
  end
33
63
  end
34
64
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'stringio'
4
- require_relative '../core/utils/base64'
4
+ require_relative '../core/utils/base64_codec'
5
5
 
6
6
  module Datadog
7
7
  module DataStreams
@@ -34,7 +34,7 @@ module Datadog
34
34
  end
35
35
 
36
36
  def encode_b64
37
- Core::Utils::Base64.strict_encode64(encode)
37
+ Core::Utils::Base64Codec.strict_encode64(encode)
38
38
  end
39
39
 
40
40
  # Decode pathway context from base64 encoded string
@@ -42,7 +42,7 @@ module Datadog
42
42
  return nil unless encoded_ctx && !encoded_ctx.empty?
43
43
 
44
44
  begin
45
- binary_data = Core::Utils::Base64.strict_decode64(encoded_ctx)
45
+ binary_data = Core::Utils::Base64Codec.strict_decode64(encoded_ctx)
46
46
  decode(binary_data)
47
47
  rescue ArgumentError => e
48
48
  # Invalid base64 encoding - may indicate version mismatch or corruption
@@ -249,23 +249,34 @@ module Datadog
249
249
  exact = registry[suffix]
250
250
  return [suffix, exact] if exact
251
251
 
252
- suffix = suffix.dup
253
- loop do
254
- inexact = []
255
- registry.each do |path, iseq|
256
- if Utils.path_matches_suffix?(path, suffix)
257
- inexact << [path, iseq]
252
+ # Normalize Windows-style backslash separators (DEBUG-5111) upfront
253
+ # so the suffix-shortening loop's "/+" regex can strip leading
254
+ # components on probes whose sourceFile uses backslashes.
255
+ suffix = Utils.normalize_windows_separators(suffix)
256
+
257
+ # Per the design comment in utils.rb, attempt case-sensitive
258
+ # matching first (steps 5-6) and only fall back to case-insensitive
259
+ # matching (steps 7-8) when no case-sensitive match is found.
260
+ [false, true].each do |case_insensitive|
261
+ working_suffix = suffix.dup
262
+ loop do
263
+ inexact = []
264
+ registry.each do |path, iseq|
265
+ if Utils.path_matches_suffix?(path, working_suffix, case_insensitive: case_insensitive)
266
+ inexact << [path, iseq]
267
+ end
258
268
  end
269
+ if inexact.length > 1
270
+ raise Error::MultiplePathsMatch, "Multiple paths matched requested suffix"
271
+ end
272
+ if inexact.any?
273
+ return inexact.first
274
+ end
275
+ break unless working_suffix.include?('/')
276
+ working_suffix.sub!(%r{.*/+}, '')
259
277
  end
260
- if inexact.length > 1
261
- raise Error::MultiplePathsMatch, "Multiple paths matched requested suffix"
262
- end
263
- if inexact.any?
264
- return inexact.first
265
- end
266
- return nil unless suffix.include?('/')
267
- suffix.sub!(%r{.*/+}, '')
268
278
  end
279
+ nil
269
280
  end
270
281
  end
271
282
 
@@ -365,15 +376,25 @@ module Datadog
365
376
  # Exact match.
366
377
  return suffix if paths.include?(suffix)
367
378
 
368
- # Suffix match.
369
- suffix = suffix.dup
370
- loop do
371
- matches = paths.select { |p| Utils.path_matches_suffix?(p, suffix) }
372
- raise Error::MultiplePathsMatch, "Multiple paths matched requested suffix" if matches.length > 1
373
- return matches.first if matches.any?
374
- return nil unless suffix.include?('/')
375
- suffix.sub!(%r{.*/+}, '')
379
+ # Normalize Windows-style backslash separators (DEBUG-5111) upfront
380
+ # so the suffix-shortening loop's "/+" regex can strip leading
381
+ # components on probes whose sourceFile uses backslashes.
382
+ suffix = Utils.normalize_windows_separators(suffix)
383
+
384
+ # Suffix match. Per the design comment in utils.rb, attempt
385
+ # case-sensitive matching first (steps 5-6) and only fall back to
386
+ # case-insensitive (steps 7-8) when no case-sensitive match is found.
387
+ [false, true].each do |case_insensitive|
388
+ working_suffix = suffix.dup
389
+ loop do
390
+ matches = paths.select { |p| Utils.path_matches_suffix?(p, working_suffix, case_insensitive: case_insensitive) }
391
+ raise Error::MultiplePathsMatch, "Multiple paths matched requested suffix" if matches.length > 1
392
+ return matches.first if matches.any?
393
+ break unless working_suffix.include?('/')
394
+ working_suffix.sub!(%r{.*/+}, '')
395
+ end
376
396
  end
397
+ nil
377
398
  end
378
399
  end
379
400
  end
@@ -23,7 +23,11 @@ Datadog::DI::Serializer.register(
23
23
  #
24
24
  # +depth+ is the remaining depth for serializing collections and objects.
25
25
  # It should always be an integer.
26
- # Reduce it by 1 when invoking +serialize_value+ on the contents of +value+.
26
+ # Pass it through to +serialize_value+ when the structure you produce
27
+ # represents +value+ directly (a transparent wrapper); +serialize_value+
28
+ # decrements depth itself when it recurses into Array/Hash/object entries.
29
+ # Decrement +depth+ only if your wrapper introduces real additional
30
+ # nesting levels in the output.
27
31
  # This serializer could also potentially do its own depth limiting.
28
32
  #
29
33
  # Steep: steep thinks all of the arguments are nil here
@@ -33,5 +37,5 @@ Datadog::DI::Serializer.register(
33
37
  attributes: value.attributes,
34
38
  new_record: value.new_record?,
35
39
  }
36
- serializer.serialize_value(value_to_serialize, depth: depth - 1, type: value.class)
40
+ serializer.serialize_value(value_to_serialize, depth: depth, type: value.class)
37
41
  end
@@ -106,7 +106,7 @@ module Datadog
106
106
  serializer = self.serializer
107
107
  method_name = probe.method_name
108
108
  loc = begin
109
- cls.instance_method(method_name).source_location
109
+ cls.instance_method(method_name).source_location # steep:ignore ArgumentTypeMismatch
110
110
  rescue NameError
111
111
  # The target method is not defined.
112
112
  # This could be because it will be explicitly defined later
@@ -618,12 +618,32 @@ module Datadog
618
618
  def raise_if_probe_in_loaded_features(probe, line_no, code_tracker)
619
619
  return unless probe.file
620
620
 
621
- # Find the loaded path matching the probe file.
621
+ # Find the loaded path matching the probe file. Case-sensitive
622
+ # matching is attempted first, with case-insensitive matching as a
623
+ # fallback (see the design comment in utils.rb, steps 5-8). Leading
624
+ # directory components are stripped so probes whose sourceFile carries
625
+ # a source-repo prefix that does not exist on disk still resolve to
626
+ # the loaded file — matching the behavior of
627
+ # CodeTracker#iseqs_for_path_suffix.
622
628
  loaded_path = if $LOADED_FEATURES.include?(probe.file)
623
629
  probe.file
624
630
  else
625
631
  # Expensive suffix check.
626
- $LOADED_FEATURES.find { |path| Utils.path_matches_suffix?(path, probe.file) }
632
+ suffix = Utils.normalize_windows_separators(probe.file)
633
+ found = nil #: ::String?
634
+ [false, true].each do |case_insensitive|
635
+ working_suffix = suffix.dup
636
+ loop do
637
+ found = $LOADED_FEATURES.find do |path|
638
+ Utils.path_matches_suffix?(path, working_suffix, case_insensitive: case_insensitive)
639
+ end
640
+ break if found
641
+ break unless working_suffix.include?('/')
642
+ working_suffix.sub!(%r{.*/+}, '')
643
+ end
644
+ break if found
645
+ end
646
+ found
627
647
  end
628
648
 
629
649
  return unless loaded_path
@@ -648,7 +668,7 @@ module Datadog
648
668
 
649
669
  # TODO test that this resolves qualified names e.g. A::B
650
670
  def symbolize_class_name(cls_name)
651
- Object.const_get(cls_name)
671
+ Object.const_get(cls_name) # steep:ignore ArgumentTypeMismatch
652
672
  rescue NameError => exc
653
673
  raise Error::DITargetNotDefined, "Class not defined: #{cls_name}: #{exc.class}: #{exc.message}"
654
674
  end
@@ -369,7 +369,7 @@ module Datadog
369
369
  rescue => exc
370
370
  evaluation_errors << {
371
371
  message: "#{exc.class}: #{exc.message}",
372
- expr: segment.dsl_expr,
372
+ expr: segment.dsl_expr, # steep:ignore NoMethod
373
373
  }
374
374
  '[evaluation error]'
375
375
  end.join
@@ -42,14 +42,14 @@ module Datadog
42
42
  changes.each do |change|
43
43
  case change.type
44
44
  when :insert
45
- add_probe(change.content, component)
45
+ add_probe(change.content, component) # steep:ignore NoMethod
46
46
  when :update
47
47
  # We do not implement updates at the moment, remove the
48
48
  # probe and reinstall.
49
- remove_probe(change.content, component)
50
- add_probe(change.content, component)
49
+ remove_probe(change.content, component) # steep:ignore NoMethod
50
+ add_probe(change.content, component) # steep:ignore NoMethod
51
51
  when :delete
52
- remove_probe(change.previous, component)
52
+ remove_probe(change.previous, component) # steep:ignore NoMethod
53
53
  else
54
54
  # This really should never happen since we generate the
55
55
  # change types in the library.
@@ -255,7 +255,7 @@ module Datadog
255
255
 
256
256
  serialized.update(value: value)
257
257
  when Array
258
- if depth < 0
258
+ if depth <= 0
259
259
  serialized.update(notCapturedReason: "depth")
260
260
  else
261
261
  max = settings.dynamic_instrumentation.max_capture_collection_size
@@ -271,7 +271,7 @@ module Datadog
271
271
  serialized.update(elements: entries)
272
272
  end
273
273
  when Hash
274
- if depth < 0
274
+ if depth <= 0
275
275
  serialized.update(notCapturedReason: "depth")
276
276
  else
277
277
  max = settings.dynamic_instrumentation.max_capture_collection_size
@@ -288,7 +288,7 @@ module Datadog
288
288
  serialized.update(entries: entries)
289
289
  end
290
290
  else
291
- if depth < 0
291
+ if depth <= 0
292
292
  serialized.update(notCapturedReason: "depth")
293
293
  else
294
294
  fields = {}
@@ -369,7 +369,7 @@ module Datadog
369
369
  when String
370
370
  serialize_string_or_symbol_for_message(value)
371
371
  when Symbol
372
- ':' + serialize_string_or_symbol_for_message(value)
372
+ ':' + serialize_string_or_symbol_for_message(value) # steep:ignore ArgumentTypeMismatch
373
373
  when Array
374
374
  return '...' if depth <= 0
375
375
 
@@ -482,7 +482,7 @@ module Datadog
482
482
  if max % 2 == 0
483
483
  upper += 1
484
484
  end
485
- value[0...max / 2 - 1] + '...' + value[upper...length]
485
+ value[0...max / 2 - 1] + '...' + value[upper...length] # steep:ignore NoMethod
486
486
  end
487
487
  else
488
488
  value
@@ -82,15 +82,31 @@ module Datadog
82
82
  # we just strip leading directory components from the "probe path"
83
83
  # until we get a match via a "suffix of the suffix".
84
84
 
85
+ # Returns +path+ with Windows-style backslash separators translated to
86
+ # forward slashes (DEBUG-5111). Used to normalize probe source paths
87
+ # that originate from IDE tooling running on Windows.
88
+ module_function def normalize_windows_separators(path)
89
+ path.tr('\\', '/')
90
+ end
91
+
85
92
  # Returns whether the provided +path+ matches the user-designated
86
93
  # file suffix (of a line probe).
87
94
  #
88
- # If suffix is an absolute path (i.e., it starts with a slash), the path
89
- # must be identical for it to match.
95
+ # Backslash separators in +suffix+ are translated to forward slashes
96
+ # (DEBUG-5111) so paths typed by IDE tooling on Windows can match.
97
+ # Case-insensitive matching (DEBUG-5107) is opt-in via
98
+ # +case_insensitive: true+; callers that orchestrate matching against a
99
+ # set of known paths perform case-sensitive comparisons first and only
100
+ # fall back to case-insensitive when no case-sensitive match is found
101
+ # (see the design comment above, steps 5-8).
102
+ #
103
+ # If suffix is an absolute path (i.e., it starts with a slash, possibly
104
+ # after backslash normalization), the path must be identical for it to
105
+ # match.
90
106
  #
91
107
  # If suffix is not an absolute path, the path matches if its suffix is
92
108
  # the provided suffix, at a path component boundary.
93
- module_function def path_matches_suffix?(path, suffix)
109
+ module_function def path_matches_suffix?(path, suffix, case_insensitive: false)
94
110
  if path.nil?
95
111
  raise ArgumentError, "nil path passed"
96
112
  end
@@ -98,6 +114,12 @@ module Datadog
98
114
  raise ArgumentError, "nil suffix passed"
99
115
  end
100
116
 
117
+ suffix = normalize_windows_separators(suffix)
118
+ if case_insensitive
119
+ path = path.downcase
120
+ suffix = suffix.downcase
121
+ end
122
+
101
123
  if suffix.start_with?('/')
102
124
  path == suffix
103
125
  else
@@ -105,11 +127,6 @@ module Datadog
105
127
  # has to be longer than the suffix. Require full component matches,
106
128
  # meaning either the first character of the suffix is a slash
107
129
  # or the previous character in the path is a slash.
108
- # For now only check for forward slashes for Unix-like OSes;
109
- # backslash is a legitimate character of a file name in Unix
110
- # therefore simply permitting forward or back slash is not
111
- # sufficient, we need to perform an OS check to know which
112
- # path separator to use.
113
130
  !!
114
131
  if path.length > suffix.length && path.end_with?(suffix)
115
132
  previous_char = path[path.length - suffix.length - 1]
@@ -125,15 +142,26 @@ module Datadog
125
142
  # +spec+. Attempts all of the fuzzy matches by stripping directories
126
143
  # from the front of +spec+. Does not consider othr known paths to
127
144
  # identify the case of (potentially) multiple matching paths for +spec+.
145
+ #
146
+ # Matching is attempted case-sensitively first (steps 5-6 in the design
147
+ # comment above) and only falls back to case-insensitive (steps 7-8)
148
+ # when no case-sensitive match is found.
128
149
  module_function def path_can_match_spec?(path, spec)
129
- return true if path_matches_suffix?(path, spec)
150
+ # Normalize Windows-style backslash separators (DEBUG-5111) so the
151
+ # suffix-shortening loop's "/+" regex can strip leading components.
152
+ spec = normalize_windows_separators(spec)
130
153
 
131
- spec = spec.dup
132
- loop do
133
- return false unless spec.include?('/')
134
- spec.sub!(%r{.*/+}, '')
135
- return true if path_matches_suffix?(path, spec)
154
+ [false, true].each do |case_insensitive|
155
+ working_spec = spec.dup
156
+ return true if path_matches_suffix?(path, working_spec, case_insensitive: case_insensitive)
157
+
158
+ loop do
159
+ break unless working_spec.include?('/')
160
+ working_spec.sub!(%r{.*/+}, '')
161
+ return true if path_matches_suffix?(path, working_spec, case_insensitive: case_insensitive)
162
+ end
136
163
  end
164
+ false
137
165
  end
138
166
  end
139
167
  end
@@ -152,6 +152,71 @@ module Datadog
152
152
  o.setter(&Settings.normalize_protocol('OTEL_EXPORTER_OTLP_METRICS_PROTOCOL'))
153
153
  end
154
154
  end
155
+
156
+ settings :logs do
157
+ option :enabled do |o|
158
+ o.type :bool
159
+ o.env 'DD_LOGS_OTEL_ENABLED'
160
+ o.default false
161
+ end
162
+
163
+ option :exporter do |o|
164
+ o.type :string
165
+ o.env 'OTEL_LOGS_EXPORTER'
166
+ o.default 'otlp'
167
+ end
168
+
169
+ option :endpoint do |o|
170
+ o.type :string, nilable: true
171
+ o.env 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT'
172
+ o.default nil
173
+ end
174
+
175
+ option :headers do |o|
176
+ o.skip_telemetry true
177
+ o.type :hash, nilable: true
178
+ o.env 'OTEL_EXPORTER_OTLP_LOGS_HEADERS'
179
+ o.default nil
180
+ o.env_parser(&Settings.headers_parser('OTEL_EXPORTER_OTLP_LOGS_HEADERS'))
181
+ end
182
+
183
+ option :timeout_millis do |o|
184
+ o.type :int, nilable: true
185
+ o.env 'OTEL_EXPORTER_OTLP_LOGS_TIMEOUT'
186
+ o.default 10_000
187
+ end
188
+
189
+ option :protocol do |o|
190
+ o.type :string, nilable: true
191
+ o.env 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'
192
+ o.default "http/protobuf"
193
+ o.setter(&Settings.normalize_protocol('OTEL_EXPORTER_OTLP_LOGS_PROTOCOL'))
194
+ end
195
+
196
+ option :max_queue_size do |o|
197
+ o.type :int
198
+ o.env 'OTEL_BLRP_MAX_QUEUE_SIZE'
199
+ o.default 2048
200
+ end
201
+
202
+ option :schedule_delay_millis do |o|
203
+ o.type :int
204
+ o.env 'OTEL_BLRP_SCHEDULE_DELAY'
205
+ o.default 1000
206
+ end
207
+
208
+ option :export_timeout_millis do |o|
209
+ o.type :int
210
+ o.env 'OTEL_BLRP_EXPORT_TIMEOUT'
211
+ o.default 30_000
212
+ end
213
+
214
+ option :max_export_batch_size do |o|
215
+ o.type :int
216
+ o.env 'OTEL_BLRP_MAX_EXPORT_BATCH_SIZE'
217
+ o.default 512
218
+ end
219
+ end
155
220
  end
156
221
  end
157
222
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module OpenTelemetry
5
+ module Ext
6
+ EXPORTER_NONE = 'none'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'ext'
4
+ require_relative 'signal_configuration'
5
+
6
+ module Datadog
7
+ module OpenTelemetry
8
+ class Logs
9
+ include SignalConfiguration
10
+
11
+ def self.initialize!(components)
12
+ new(components).configure_logs_sdk
13
+ true
14
+ rescue => exc
15
+ components.logger.warn(
16
+ "Failed to initialize OpenTelemetry logs: #{exc.class}: #{exc.message}\n#{(exc.backtrace || []).join("\n")}"
17
+ )
18
+ false
19
+ end
20
+
21
+ def initialize(components)
22
+ @logger = components.logger
23
+ @settings = components.settings
24
+ @agent_host = components.agent_settings.hostname
25
+ @agent_ssl = components.agent_settings.ssl
26
+ end
27
+
28
+ def configure_logs_sdk
29
+ provider = ::OpenTelemetry.logger_provider
30
+ provider.shutdown if provider.is_a?(::OpenTelemetry::SDK::Logs::LoggerProvider)
31
+
32
+ resource = create_resource
33
+ provider = ::OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
34
+ processor_configured = configure_log_record_processor(provider)
35
+ ::OpenTelemetry.logger_provider = provider
36
+
37
+ disable_log_injection if processor_configured
38
+ end
39
+
40
+ private
41
+
42
+ def disable_log_injection
43
+ @logger.warn('OTel logs enabled: disabling Datadog log injection to prevent duplicate trace correlation fields')
44
+ Datadog.configure do |c|
45
+ c.tracing.log_injection = false # steep:ignore
46
+ end
47
+ end
48
+
49
+ def configure_log_record_processor(provider)
50
+ exporter_name = @settings.opentelemetry.logs.exporter
51
+ return false if exporter_name == Ext::EXPORTER_NONE
52
+
53
+ configure_otlp_exporter(provider)
54
+ rescue => e
55
+ @logger.warn("Failed to configure OTLP logs exporter: #{e.class}: #{e.message}")
56
+ false
57
+ end
58
+
59
+ def default_logs_endpoint
60
+ "#{@agent_ssl ? 'https' : 'http'}://#{@agent_host}:4318/v1/logs"
61
+ end
62
+
63
+ def configure_otlp_exporter(provider)
64
+ require_relative 'sdk/logs_exporter'
65
+
66
+ logs_config = @settings.opentelemetry.logs
67
+ endpoint = config_or_exporter_fallback(
68
+ signal: :logs,
69
+ option_name: :endpoint,
70
+ computed_default: default_logs_endpoint
71
+ )
72
+ timeout = config_or_exporter_fallback(signal: :logs, option_name: :timeout_millis)
73
+ headers = config_or_exporter_fallback(signal: :logs, option_name: :headers)
74
+ # opentelemetry-logs-sdk only supports http/protobuf protocol as of 0.5.0.
75
+ config_or_exporter_fallback(signal: :logs, option_name: :protocol)
76
+
77
+ exporter = Datadog::OpenTelemetry::SDK::LogsExporter.new(
78
+ endpoint: endpoint,
79
+ timeout: timeout / 1000.0,
80
+ headers: headers
81
+ )
82
+
83
+ processor = ::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(
84
+ exporter,
85
+ max_queue_size: logs_config.max_queue_size,
86
+ schedule_delay: logs_config.schedule_delay_millis,
87
+ exporter_timeout: logs_config.export_timeout_millis,
88
+ max_export_batch_size: logs_config.max_export_batch_size
89
+ )
90
+ provider.add_log_record_processor(processor)
91
+ true
92
+ rescue LoadError => e
93
+ @logger.warn("Could not load OTLP logs exporter: #{e.class}: #{e.message}")
94
+ false
95
+ end
96
+ end
97
+ end
98
+ end