datadog 2.42.0 → 2.44.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 (94) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +67 -2
  3. data/README.md +3 -0
  4. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +46 -5
  5. data/ext/datadog_profiling_native_extension/collectors_idle_sampling_helper.c +6 -7
  6. data/ext/datadog_profiling_native_extension/collectors_stack.c +6 -7
  7. data/ext/datadog_profiling_native_extension/collectors_stack.h +1 -1
  8. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +80 -36
  9. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +3 -2
  10. data/ext/datadog_profiling_native_extension/extconf.rb +4 -2
  11. data/ext/datadog_profiling_native_extension/heap_recorder.c +13 -0
  12. data/ext/datadog_profiling_native_extension/heap_recorder.h +4 -0
  13. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +34 -2
  14. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +1 -0
  15. data/ext/datadog_profiling_native_extension/profiling.c +1 -0
  16. data/ext/datadog_profiling_native_extension/stack_recorder.c +17 -4
  17. data/ext/datadog_profiling_native_extension/time_helpers.h +5 -4
  18. data/ext/libdatadog_api/di.c +8 -3
  19. data/ext/libdatadog_api/extconf.rb +1 -1
  20. data/ext/libdatadog_api/feature_flags.c +8 -0
  21. data/ext/libdatadog_api/init.c +3 -1
  22. data/ext/libdatadog_api/otel_thread_context.c +51 -15
  23. data/ext/libdatadog_api/trace_exporter.c +642 -22
  24. data/ext/libdatadog_extconf_helpers.rb +14 -9
  25. data/lib/datadog/ai_guard/component.rb +17 -8
  26. data/lib/datadog/ai_guard/configuration/ext.rb +2 -0
  27. data/lib/datadog/ai_guard/configuration.rb +13 -0
  28. data/lib/datadog/ai_guard/contrib/ruby_llm/chat_instrumentation.rb +48 -55
  29. data/lib/datadog/ai_guard/contrib/ruby_llm/integration.rb +1 -1
  30. data/lib/datadog/ai_guard/contrib/ruby_llm/message_adapter.rb +76 -0
  31. data/lib/datadog/ai_guard/contrib/ruby_llm/patcher.rb +1 -0
  32. data/lib/datadog/ai_guard/contrib/ruby_llm/redaction_change_set.rb +135 -0
  33. data/lib/datadog/ai_guard/evaluation/client.rb +50 -0
  34. data/lib/datadog/ai_guard/evaluation/content_part.rb +12 -0
  35. data/lib/datadog/ai_guard/evaluation/message.rb +25 -7
  36. data/lib/datadog/ai_guard/evaluation/message_builder.rb +31 -0
  37. data/lib/datadog/ai_guard/evaluation/no_op_result.rb +6 -6
  38. data/lib/datadog/ai_guard/evaluation/outcome.rb +24 -0
  39. data/lib/datadog/ai_guard/evaluation/request.rb +4 -60
  40. data/lib/datadog/ai_guard/evaluation/response.rb +33 -0
  41. data/lib/datadog/ai_guard/evaluation/result.rb +11 -18
  42. data/lib/datadog/ai_guard/evaluation/tool_call.rb +15 -2
  43. data/lib/datadog/ai_guard/evaluation.rb +25 -11
  44. data/lib/datadog/ai_guard/ext.rb +1 -0
  45. data/lib/datadog/ai_guard/{api_client.rb → http_client.rb} +6 -4
  46. data/lib/datadog/ai_guard/metrics/telemetry.rb +43 -0
  47. data/lib/datadog/ai_guard/redaction/replacements.rb +97 -0
  48. data/lib/datadog/ai_guard/redaction/result.rb +29 -0
  49. data/lib/datadog/ai_guard/redaction.rb +80 -0
  50. data/lib/datadog/ai_guard.rb +58 -57
  51. data/lib/datadog/core/configuration/components.rb +9 -1
  52. data/lib/datadog/core/configuration/settings.rb +5 -2
  53. data/lib/datadog/core/configuration/supported_configurations.rb +8 -0
  54. data/lib/datadog/core/remote/client/capabilities.rb +10 -4
  55. data/lib/datadog/core/remote/component.rb +9 -2
  56. data/lib/datadog/core/remote/worker.rb +8 -0
  57. data/lib/datadog/di/instrumenter.rb +1 -1
  58. data/lib/datadog/di/probe_manager.rb +10 -5
  59. data/lib/datadog/di/proc_responder.rb +1 -1
  60. data/lib/datadog/open_feature/agentless/configuration_source.rb +246 -0
  61. data/lib/datadog/open_feature/agentless/parser.rb +43 -0
  62. data/lib/datadog/open_feature/agentless/response.rb +19 -0
  63. data/lib/datadog/open_feature/agentless/transport.rb +103 -0
  64. data/lib/datadog/open_feature/component.rb +57 -1
  65. data/lib/datadog/open_feature/configuration/agentless_endpoint.rb +90 -0
  66. data/lib/datadog/open_feature/configuration/source/resolution.rb +22 -0
  67. data/lib/datadog/open_feature/configuration/source.rb +43 -0
  68. data/lib/datadog/open_feature/configuration.rb +118 -0
  69. data/lib/datadog/open_feature/native_evaluator.rb +1 -14
  70. data/lib/datadog/open_feature/remote.rb +5 -5
  71. data/lib/datadog/open_feature/transport.rb +5 -0
  72. data/lib/datadog/opentelemetry/signal_configuration.rb +13 -9
  73. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +2 -2
  74. data/lib/datadog/profiling/collectors/idle_sampling_helper.rb +5 -7
  75. data/lib/datadog/profiling/collectors/info.rb +2 -6
  76. data/lib/datadog/symbol_database/extractor.rb +84 -102
  77. data/lib/datadog/tracing/component.rb +2 -0
  78. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  79. data/lib/datadog/tracing/configuration/settings.rb +6 -0
  80. data/lib/datadog/tracing/context.rb +24 -2
  81. data/lib/datadog/tracing/context_provider.rb +9 -5
  82. data/lib/datadog/tracing/contrib/grape/endpoint.rb +32 -5
  83. data/lib/datadog/tracing/contrib/sequel/jdbc_connection_string.rb +140 -0
  84. data/lib/datadog/tracing/contrib/sequel/utils.rb +49 -62
  85. data/lib/datadog/tracing/metadata/ext.rb +3 -0
  86. data/lib/datadog/tracing/otel_thread_context.rb +78 -9
  87. data/lib/datadog/tracing/tracer.rb +15 -2
  88. data/lib/datadog/tracing/transport/native.rb +18 -26
  89. data/lib/datadog/tracing/transport/span_events_negotiation.rb +41 -0
  90. data/lib/datadog/tracing/transport/trace_formatter.rb +13 -3
  91. data/lib/datadog/tracing/transport/traces.rb +3 -23
  92. data/lib/datadog/version.rb +1 -1
  93. metadata +25 -7
  94. data/lib/datadog/ai_guard/evaluation/content_builder.rb +0 -31
@@ -10,7 +10,6 @@ module Datadog
10
10
  class IdleSamplingHelper
11
11
  # @rbs @worker_thread: untyped
12
12
  # @rbs @start_stop_mutex: ::Thread::Mutex
13
- # @rbs @thread_context_collector: Datadog::Profiling::Collectors::ThreadContext?
14
13
 
15
14
  private
16
15
 
@@ -18,15 +17,14 @@ module Datadog
18
17
 
19
18
  public
20
19
 
21
- #: (thread_context_collector: Datadog::Profiling::Collectors::ThreadContext) -> void
22
- def initialize(thread_context_collector:)
20
+ #: () -> void
21
+ def initialize
23
22
  @worker_thread = nil
24
23
  @start_stop_mutex = Mutex.new
25
- @thread_context_collector = thread_context_collector
26
24
  end
27
25
 
28
- #: () -> (nil | true)
29
- def start
26
+ #: (CpuAndWallTimeWorker cpu_and_wall_time_worker) -> true?
27
+ def start(cpu_and_wall_time_worker)
30
28
  @start_stop_mutex.synchronize do
31
29
  return if @worker_thread&.alive?
32
30
 
@@ -39,7 +37,7 @@ module Datadog
39
37
  @worker_thread = Thread.new do
40
38
  Thread.current.name = self.class.name
41
39
 
42
- self.class._native_idle_sampling_loop(self, @thread_context_collector)
40
+ self.class._native_idle_sampling_loop(self, cpu_and_wall_time_worker)
43
41
 
44
42
  Datadog.logger.debug("IdleSamplingHelper thread stopping cleanly")
45
43
  rescue Exception => e # rubocop:disable Lint/RescueException
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
- require "time"
5
3
  require "libdatadog"
4
+ require_relative "../../core/utils/time"
6
5
 
7
6
  module Datadog
8
7
  module Profiling
@@ -71,10 +70,7 @@ module Datadog
71
70
  # Instead of trying to figure out real process start time by checking
72
71
  # /proc or some other complex/non-portable way, approximate start time
73
72
  # by time of requirement of this file.
74
- #
75
- # Note: this does not use Core::Utils::Time.now because this constant
76
- # gets initialized before a user has a chance to configure the library.
77
- START_TIME = Time.now.utc.freeze
73
+ START_TIME = Datadog::Core::Utils::Time.now.utc.freeze
78
74
 
79
75
  #: () -> ::Hash[::Symbol, untyped]
80
76
  def collect_platform_info
@@ -43,7 +43,7 @@ module Datadog
43
43
  # These are expected failures — no logging needed.
44
44
  #
45
45
  # 2. **Method-level rescues** (`rescue => e` with logging):
46
- # Catch failures in extract_method_scope, find_source_file, etc. Log at debug
46
+ # Catch failures in build_class_method_scopes, find_source_file, etc. Log at debug
47
47
  # for post-hoc diagnosis, return nil or empty array. One bad method/module
48
48
  # doesn't kill the entire class extraction.
49
49
  #
@@ -133,16 +133,16 @@ module Datadog
133
133
  return nil unless Module === mod
134
134
  mod_name = safe_mod_name(mod)
135
135
  return nil unless mod_name
136
-
137
- return nil unless user_code_module?(mod)
136
+ return nil unless user_code_module_name?(mod)
138
137
 
139
138
  source_file = find_source_file(mod)
140
139
  return nil unless source_file
140
+ return nil unless user_code_path?(source_file)
141
141
 
142
142
  inner_scope = if Class === mod
143
- extract_class_scope(mod)
143
+ extract_class_scope(mod, source_file)
144
144
  else
145
- extract_module_scope(mod)
145
+ extract_module_scope(mod, source_file)
146
146
  end
147
147
 
148
148
  wrap_in_file_scope(source_file, [inner_scope])
@@ -275,10 +275,12 @@ module Datadog
275
275
  false
276
276
  end
277
277
 
278
- # Check if module is from user code (not gems or stdlib)
279
- # @param mod [Module] The module to check
280
- # @return [Boolean] true if user code
281
- def user_code_module?(mod)
278
+ # Return whether +mod+ passes the name-based user-code exclusions
279
+ # (named, outside Datadog, not a Ruby root class). Does not resolve
280
+ # a source file.
281
+ # @param mod [Module]
282
+ # @return [Boolean]
283
+ def user_code_module_name?(mod)
282
284
  mod_name = safe_mod_name(mod)
283
285
  return false unless mod_name
284
286
 
@@ -297,6 +299,15 @@ module Datadog
297
299
  return false if mod.equal?(Object) || mod.equal?(BasicObject) ||
298
300
  mod.equal?(Kernel) || mod.equal?(Module) || mod.equal?(Class)
299
301
 
302
+ true
303
+ end
304
+
305
+ # Check if module is from user code (not gems or stdlib)
306
+ # @param mod [Module] The module to check
307
+ # @return [Boolean] true if user code
308
+ def user_code_module?(mod)
309
+ return false unless user_code_module_name?(mod)
310
+
300
311
  source_file = find_source_file(mod)
301
312
  return false unless source_file
302
313
 
@@ -331,6 +342,25 @@ module Datadog
331
342
  true
332
343
  end
333
344
 
345
+ # Resolve the UnboundMethod +mod+ itself declares for +method_name+,
346
+ # skipping prepended wrapper modules (e.g. a DI method-logpoint wrapper)
347
+ # that shadow it.
348
+ #
349
+ # @param mod [Module] Module or class expected to declare the method
350
+ # @param method_name [Symbol] Name of the method to resolve
351
+ # @return [UnboundMethod] Method whose owner is +mod+, or the
352
+ # prepend-resolved method when +mod+ owns no method in the super chain
353
+ def declared_instance_method(mod, method_name)
354
+ original_method = mod.instance_method(method_name)
355
+ method = original_method
356
+ until method.owner.equal?(mod)
357
+ super_method = method.super_method
358
+ return original_method unless super_method
359
+ method = super_method
360
+ end
361
+ method
362
+ end
363
+
334
364
  # Find source file for a module.
335
365
  # Prefers user code paths over gem/stdlib paths. ActiveRecord models have
336
366
  # generated methods (autosave callbacks) whose source is in the gem, but
@@ -349,7 +379,7 @@ module Datadog
349
379
 
350
380
  # Try instance methods first
351
381
  mod.instance_methods(false).each do |method_name|
352
- method = mod.instance_method(method_name)
382
+ method = declared_instance_method(mod, method_name)
353
383
  location = method.source_location
354
384
  next unless location
355
385
 
@@ -357,10 +387,15 @@ module Datadog
357
387
  return path if user_code_path?(path)
358
388
 
359
389
  fallback ||= path # steep:ignore
390
+ rescue Exception => e # standard:disable Lint/RescueException
391
+ Datadog::DI.reraise_if_fatal(e)
392
+ @logger.debug { "symdb: error resolving #{safe_mod_name(mod)}##{method_name}: #{e.class}: #{e.message}" }
360
393
  end
361
394
 
362
395
  # Try singleton methods
363
396
  mod.singleton_methods(false).each do |method_name|
397
+ # Method-logpoint probes prepend instance methods only, so singleton
398
+ # methods are never shadowed by a DI wrapper.
364
399
  method = mod.method(method_name)
365
400
  location = method.source_location
366
401
  next unless location
@@ -369,6 +404,9 @@ module Datadog
369
404
  return path if user_code_path?(path)
370
405
 
371
406
  fallback ||= path # steep:ignore
407
+ rescue Exception => e # standard:disable Lint/RescueException
408
+ Datadog::DI.reraise_if_fatal(e)
409
+ @logger.debug { "symdb: error resolving #{safe_mod_name(mod)}.#{method_name}: #{e.class}: #{e.message}" }
372
410
  end
373
411
 
374
412
  # Use const_source_location to find where this class/module is declared.
@@ -460,10 +498,9 @@ module Datadog
460
498
  # Extract MODULE scope (without file_hash — that belongs on the FILE root scope).
461
499
  # Does not include nested classes — nesting is handled by extract_all via FQN splitting.
462
500
  # @param mod [Module] The module
501
+ # @param source_file [String] Source file resolved by the caller
463
502
  # @return [Scope] The module scope
464
- def extract_module_scope(mod)
465
- source_file = find_source_file(mod)
466
-
503
+ def extract_module_scope(mod, source_file)
467
504
  Scope.new(
468
505
  scope_type: "MODULE",
469
506
  name: safe_mod_name(mod),
@@ -476,11 +513,11 @@ module Datadog
476
513
 
477
514
  # Extract CLASS scope
478
515
  # @param klass [Class] The class
516
+ # @param source_file [String] Source file resolved by the caller
479
517
  # @return [Scope] The class scope
480
- def extract_class_scope(klass)
481
- methods = klass.instance_methods(false)
482
- start_line, end_line = calculate_class_line_range(klass, methods)
483
- source_file = find_source_file(klass)
518
+ def extract_class_scope(klass, source_file)
519
+ method_scopes = build_class_method_scopes(klass)
520
+ start_line, end_line = line_range_from_scopes(method_scopes)
484
521
 
485
522
  Scope.new(
486
523
  scope_type: "CLASS",
@@ -489,36 +526,20 @@ module Datadog
489
526
  start_line: start_line,
490
527
  end_line: end_line,
491
528
  language_specifics: build_class_language_specifics(klass),
492
- scopes: extract_method_scopes(klass),
529
+ scopes: method_scopes,
493
530
  symbols: extract_scope_symbols(klass)
494
531
  )
495
532
  end
496
533
 
497
- # Calculate class line range from method locations.
498
- # Start from the earliest method start, end at the latest method end (derived
499
- # from iseq trace_points so methods spanning multiple lines aren't truncated).
500
- # @param klass [Class] The class
501
- # @param methods [Array<Symbol>] Method names
534
+ # Derive a [start_line, end_line] range from method scopes, ignoring
535
+ # UNKNOWN sentinel values. The end is the max of method end_lines so a
536
+ # multi-line method's last line is not truncated to its start_line.
537
+ # @param method_scopes [Array<Scope>]
502
538
  # @return [Array<Integer, Integer>] [start_line, end_line]
503
- def calculate_class_line_range(klass, methods)
504
- starts = []
505
- ends = []
506
- methods.each do |method_name|
507
- method = klass.instance_method(method_name)
508
- location = method.source_location
509
- next unless location && location[0]
510
- starts << location[1]
511
- _ranges, method_end = extract_targetable_lines(method, location[1])
512
- ends << method_end
513
- end
514
-
515
- return [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE] if starts.empty?
516
-
517
- [starts.min, ends.max]
518
- rescue Exception => e # standard:disable Lint/RescueException
519
- Datadog::DI.reraise_if_fatal(e)
520
- @logger.debug { "symdb: error calculating line range for #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
521
- [UNKNOWN_MIN_LINE, UNKNOWN_MAX_LINE]
539
+ def line_range_from_scopes(method_scopes)
540
+ starts = method_scopes.map(&:start_line).reject { |line| line == UNKNOWN_MIN_LINE }
541
+ ends = method_scopes.map(&:end_line).reject { |line| line == UNKNOWN_MAX_LINE }
542
+ [starts.min || UNKNOWN_MIN_LINE, ends.max || UNKNOWN_MAX_LINE]
522
543
  end
523
544
 
524
545
  # Build language specifics for CLASS
@@ -566,66 +587,33 @@ module Datadog
566
587
  {}
567
588
  end
568
589
 
569
- # Extract method scopes from a class
570
- # @param klass [Class] The class
571
- # @return [Array<Scope>] Method scopes
572
- def extract_method_scopes(klass)
573
- scopes = []
574
-
575
- # Get all instance methods (public, protected, private)
576
- all_instance_methods = klass.instance_methods(false) +
590
+ # Build a METHOD scope for each user-code instance method +klass+
591
+ # declares.
592
+ # @param klass [Class]
593
+ # @return [Array<Scope>]
594
+ def build_class_method_scopes(klass)
595
+ method_names = (klass.instance_methods(false) +
577
596
  klass.protected_instance_methods(false) +
578
- klass.private_instance_methods(false)
579
- all_instance_methods.uniq!
597
+ klass.private_instance_methods(false)).uniq
580
598
 
581
- all_instance_methods.each do |method_name|
582
- method_scope = extract_method_scope(klass, method_name, :instance)
583
- scopes << method_scope if method_scope
584
- end
599
+ Core::Utils::EnumerableCompat.filter_map(method_names) do |method_name|
600
+ method = declared_instance_method(klass, method_name)
601
+ location = method.source_location
602
+ next unless location
603
+ next unless user_code_path?(location[0])
585
604
 
586
- scopes
605
+ build_instance_method_scope(klass, method_name, method)
606
+ rescue Exception => e # standard:disable Lint/RescueException
607
+ Datadog::DI.reraise_if_fatal(e)
608
+ @logger.debug { "symdb: error resolving #{safe_mod_name(klass)}##{method_name}: #{e.class}: #{e.message}" }
609
+ nil
610
+ end
587
611
  rescue Exception => e # standard:disable Lint/RescueException
588
612
  Datadog::DI.reraise_if_fatal(e)
589
613
  @logger.debug { "symdb: failed to extract methods from #{safe_mod_name(klass)}: #{e.class}: #{e.message}" }
590
614
  []
591
615
  end
592
616
 
593
- # Extract a single method scope
594
- # @param klass [Class] The class
595
- # @param method_name [Symbol] Method name
596
- # @param method_type [Symbol] :instance or :class
597
- # @return [Scope, nil] Method scope or nil
598
- def extract_method_scope(klass, method_name, method_type)
599
- method = klass.instance_method(method_name)
600
- location = method.source_location
601
-
602
- return nil unless location # Skip methods without source location
603
-
604
- source_file, line = location
605
- return nil unless user_code_path?(source_file) # Skip gem/stdlib methods
606
-
607
- targetable_lines, end_line = extract_targetable_lines(method, line)
608
-
609
- Scope.new(
610
- scope_type: "METHOD",
611
- name: method_name.to_s,
612
- source_file: source_file,
613
- start_line: line,
614
- end_line: end_line,
615
- targetable_lines: targetable_lines,
616
- language_specifics: {
617
- visibility: method_visibility(klass, method_name),
618
- method_type: method_type.to_s,
619
- arity: method.arity,
620
- },
621
- symbols: extract_method_parameters(method)
622
- )
623
- rescue Exception => e # standard:disable Lint/RescueException
624
- Datadog::DI.reraise_if_fatal(e)
625
- @logger.debug { "symdb: failed to extract method #{safe_mod_name(klass)}##{method_name}: #{e.class}: #{e.message}" }
626
- nil
627
- end
628
-
629
617
  # Get method visibility
630
618
  # @param klass [Class] The class
631
619
  # @param method_name [Symbol] Method name
@@ -808,7 +796,7 @@ module Datadog
808
796
  # visibilities without an intermediate merged array.
809
797
  [mod.instance_methods(false), mod.private_instance_methods(false)].each do |method_names|
810
798
  method_names.each do |method_name|
811
- method = mod.instance_method(method_name)
799
+ method = declared_instance_method(mod, method_name)
812
800
  loc = method.source_location
813
801
  next unless loc
814
802
  next unless user_code_path?(loc[0])
@@ -852,7 +840,7 @@ module Datadog
852
840
  # live only as long as the tree node holds them; they are released when
853
841
  # convert_tree_to_scope finishes building the file's Scope.
854
842
  method_infos = Core::Utils::EnumerableCompat.filter_map(method_names) do |name|
855
- method = mod.instance_method(name)
843
+ method = declared_instance_method(mod, name)
856
844
  # Pass 1 (build_per_file_index) recorded this method under file_path.
857
845
  # If the method has been redefined in another file between the two
858
846
  # passes (e.g. a class reopened during a Rails reload while extract_all
@@ -989,13 +977,7 @@ module Datadog
989
977
  # Recurse into child scopes (nested modules/classes)
990
978
  child_scopes = node[:children].values.map { |child| convert_node_to_scope(child) }
991
979
 
992
- # Compute line range: start from the earliest method start, end at the latest
993
- # method end. Using max(start_line) would underreport the class's end_line for
994
- # classes whose last method spans multiple lines.
995
- starts = method_scopes.map(&:start_line).reject { |l| l == UNKNOWN_MIN_LINE } # steep:ignore
996
- ends = method_scopes.map(&:end_line).reject { |l| l == UNKNOWN_MAX_LINE } # steep:ignore
997
- start_line = starts.empty? ? UNKNOWN_MIN_LINE : starts.min
998
- end_line = ends.empty? ? UNKNOWN_MAX_LINE : ends.max
980
+ start_line, end_line = line_range_from_scopes(method_scopes)
999
981
 
1000
982
  # Extract symbols (constants, class variables) if we have the actual module object
1001
983
  symbols = node[:mod] ? extract_scope_symbols(node[:mod]) : []
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "otel_thread_context"
3
4
  require_relative "tracer"
4
5
  require_relative "flush"
5
6
  require_relative "sync_writer"
@@ -42,6 +43,7 @@ module Datadog
42
43
  default_service: settings.service,
43
44
  enabled: settings.tracing.enabled,
44
45
  logger: logger,
46
+ otel_thread_context: OTelThreadContext.build(settings.tracing),
45
47
  trace_flush: trace_flush,
46
48
  sampler: sampler_delegator,
47
49
  span_sampler: build_span_sampler(settings),
@@ -13,6 +13,7 @@ module Datadog
13
13
  ENV_BAGGAGE_TAG_KEYS = "DD_TRACE_BAGGAGE_TAG_KEYS"
14
14
  ENV_TRACE_ID_128_BIT_GENERATION_ENABLED = "DD_TRACE_128_BIT_TRACEID_GENERATION_ENABLED"
15
15
  ENV_NATIVE_SPAN_EVENTS = "DD_TRACE_NATIVE_SPAN_EVENTS"
16
+ ENV_OTEL_CONTEXT_ENABLED = "DD_TRACE_OTEL_CTX_ENABLED"
16
17
  ENV_RESOURCE_RENAMING_ENABLED = "DD_TRACE_RESOURCE_RENAMING_ENABLED"
17
18
  ENV_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT"
18
19
  ENV_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED"
@@ -255,6 +255,12 @@ module Datadog
255
255
  o.type :bool
256
256
  end
257
257
 
258
+ option :otel_thread_context_enabled do |o|
259
+ o.env Tracing::Configuration::Ext::ENV_OTEL_CONTEXT_ENABLED
260
+ o.default true
261
+ o.type :bool
262
+ end
263
+
258
264
  # Configures an alternative trace transport behavior, where
259
265
  # traces can be sent to the agent and backend before all spans
260
266
  # have finished.
@@ -20,9 +20,15 @@ module Datadog
20
20
  attr_reader \
21
21
  :active_trace
22
22
 
23
+ attr_writer \
24
+ :otel_thread_context
25
+
23
26
  def initialize(
24
- trace: nil
27
+ trace: nil,
28
+ otel_thread_context: nil
25
29
  )
30
+ @otel_thread_context = otel_thread_context
31
+ @active_trace = nil
26
32
  activate!(trace)
27
33
  end
28
34
 
@@ -54,14 +60,30 @@ module Datadog
54
60
  # Creates a copy of the context, when forked.
55
61
  def fork_clone
56
62
  forked_trace = @active_trace&.fork_clone
57
- self.class.new(trace: forked_trace)
63
+
64
+ self.class.new(
65
+ trace: forked_trace,
66
+ otel_thread_context: @otel_thread_context
67
+ )
58
68
  end
59
69
 
60
70
  private
61
71
 
62
72
  def set_active_trace!(trace)
73
+ previous_trace = @active_trace
74
+
63
75
  # Don't retain finished traces
64
76
  @active_trace = (trace && !trace.finished?) ? trace : nil
77
+
78
+ return @active_trace if previous_trace == @active_trace
79
+
80
+ if @active_trace
81
+ @otel_thread_context&.update_from_trace_op(@active_trace)
82
+ elsif previous_trace
83
+ @otel_thread_context&.clear
84
+ end
85
+
86
+ @active_trace
65
87
  end
66
88
  end
67
89
  end
@@ -12,12 +12,15 @@ module Datadog
12
12
  # @see https://ruby-doc.org/core-3.1.2/Thread.html#method-i-5B-5D Thread attributes are fiber-local
13
13
  class DefaultContextProvider
14
14
  # Initializes the default context provider with a fiber-bound context.
15
- def initialize
16
- @context = FiberLocalContext.new
15
+ def initialize(otel_thread_context: nil)
16
+ @otel_thread_context = otel_thread_context
17
+ @context = FiberLocalContext.new(otel_thread_context: otel_thread_context)
17
18
  end
18
19
 
19
20
  # Sets the current context.
20
21
  def context=(ctx)
22
+ ctx.otel_thread_context = @otel_thread_context if ctx.respond_to?(:otel_thread_context=)
23
+
21
24
  @context.local = ctx
22
25
  end
23
26
 
@@ -48,10 +51,11 @@ module Datadog
48
51
  class FiberLocalContext
49
52
  # To support multiple tracers simultaneously, each {Datadog::Tracing::FiberLocalContext}
50
53
  # instance has its own fiber-local variable.
51
- def initialize
54
+ def initialize(otel_thread_context: nil)
55
+ @otel_thread_context = otel_thread_context
52
56
  @key = :"datadog_context_#{FiberLocalContext.next_instance_id}"
53
57
 
54
- self.local = Context.new
58
+ self.local = Context.new(otel_thread_context: otel_thread_context)
55
59
  end
56
60
 
57
61
  # Override the fiber-local context with a new context.
@@ -61,7 +65,7 @@ module Datadog
61
65
 
62
66
  # Return the fiber-local context.
63
67
  def local(storage = Thread.current)
64
- storage[@key] ||= Context.new
68
+ storage[@key] ||= Context.new(otel_thread_context: @otel_thread_context)
65
69
  end
66
70
 
67
71
  # Ensure two instances of {FiberLocalContext} do not conflict.
@@ -15,6 +15,7 @@ module Datadog
15
15
  module Endpoint
16
16
  KEY_RUN = "datadog_grape_endpoint_run"
17
17
  KEY_RENDER = "datadog_grape_endpoint_render"
18
+ FORMAT_SEGMENT = /\(\.:?\w+\)\z/
18
19
 
19
20
  class << self
20
21
  def subscribe
@@ -43,8 +44,8 @@ module Datadog
43
44
  # collect endpoint details
44
45
  endpoint = payload.fetch(:endpoint)
45
46
  env = payload.fetch(:env)
46
- api_view = api_view(endpoint.options[:for])
47
- request_method = endpoint.options.fetch(:method).first
47
+ api_view = api_view(endpoint_api(endpoint))
48
+ request_method = endpoint_request_method(endpoint)
48
49
  path = endpoint_expand_path(endpoint)
49
50
  resource = "#{api_view} #{request_method} #{path}"
50
51
 
@@ -65,12 +66,16 @@ module Datadog
65
66
  span.set_tag(Tracing::Metadata::Ext::TAG_SVC_SRC, Ext::TAG_COMPONENT)
66
67
 
67
68
  if (grape_route = env["grape.routing_args"]) && grape_route[:route_info]
69
+ route_info = grape_route[:route_info]
70
+ # Grape 2.3 through 3.0 return nil from GreedyRoute#path; the
71
+ # path lives at pattern.path in those versions.
72
+ route_path = route_info.path || route_info.pattern&.path
68
73
  trace.set_tag(
69
74
  Tracing::Metadata::Ext::HTTP::TAG_ROUTE,
70
75
  # here we are removing the format from the path:
71
76
  # e.g. /path/to/resource(.json) => /path/to/resource
72
77
  # e.g. /path/to/resource(.:format) => /path/to/resource
73
- grape_route[:route_info].path&.gsub(/\(\.:?\w+\)\z/, "")
78
+ route_path&.gsub(FORMAT_SEGMENT, "")
74
79
  )
75
80
 
76
81
  trace.set_tag(Tracing::Metadata::Ext::HTTP::TAG_ROUTE_PATH, env["SCRIPT_NAME"])
@@ -96,8 +101,8 @@ module Datadog
96
101
  begin
97
102
  # collect endpoint details
98
103
  endpoint = payload.fetch(:endpoint)
99
- api_view = api_view(endpoint.options[:for])
100
- request_method = endpoint.options.fetch(:method).first
104
+ api_view = api_view(endpoint_api(endpoint))
105
+ request_method = endpoint_request_method(endpoint)
101
106
  path = endpoint_expand_path(endpoint)
102
107
 
103
108
  trace.resource = span.resource
@@ -266,8 +271,25 @@ module Datadog
266
271
  end
267
272
  end
268
273
 
274
+ # Grape 4 moved three values off the endpoint's public options Hash: the
275
+ # API class became the #api reader (ruby-grape/grape#2778), and the verb
276
+ # and path are read off the route (#2775, #2776). Each reader prefers the
277
+ # options Hash, so Grape 1.x through 3.x resolve as they did before.
278
+ def endpoint_api(endpoint)
279
+ endpoint.options.fetch(:for) { endpoint.api }
280
+ end
281
+
282
+ def endpoint_request_method(endpoint)
283
+ request_methods = endpoint.options[:method]
284
+ return request_methods.first if request_methods
285
+
286
+ endpoint.routes.first&.request_method
287
+ end
288
+
269
289
  def endpoint_expand_path(endpoint)
270
290
  route_path = endpoint.options[:path]
291
+ return endpoint_route_path(endpoint) if route_path.nil? || route_path.empty?
292
+
271
293
  namespace = endpoint.routes.first&.namespace || ""
272
294
 
273
295
  path = (namespace.split("/") + route_path)
@@ -277,6 +299,11 @@ module Datadog
277
299
  path
278
300
  end
279
301
 
302
+ # A compiled path already carries the namespace, so it is used whole.
303
+ def endpoint_route_path(endpoint)
304
+ endpoint.routes.first&.path&.sub(FORMAT_SEGMENT, "") || "/"
305
+ end
306
+
280
307
  def service_name
281
308
  datadog_configuration[:service_name]
282
309
  end