datadog 2.39.0 → 2.40.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -1
  3. data/ext/datadog_profiling_native_extension/collectors_stack.c +218 -147
  4. data/ext/datadog_profiling_native_extension/collectors_stack.h +16 -2
  5. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +12 -7
  6. data/ext/datadog_profiling_native_extension/crashtracking_runtime_stacks.c +5 -4
  7. data/ext/datadog_profiling_native_extension/datadog_ruby_common.c +19 -11
  8. data/ext/datadog_profiling_native_extension/datadog_ruby_common.h +7 -0
  9. data/ext/datadog_profiling_native_extension/extconf.rb +12 -7
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +259 -65
  11. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +16 -10
  12. data/ext/libdatadog_api/datadog_ruby_common.c +19 -11
  13. data/ext/libdatadog_api/datadog_ruby_common.h +7 -0
  14. data/ext/libdatadog_api/init.c +4 -0
  15. data/ext/libdatadog_api/trace_exporter.c +974 -0
  16. data/ext/libdatadog_api/trace_exporter.h +5 -0
  17. data/lib/datadog/ai_guard/evaluation.rb +1 -0
  18. data/lib/datadog/ai_guard/ext.rb +2 -0
  19. data/lib/datadog/appsec/configuration.rb +9 -0
  20. data/lib/datadog/appsec/contrib/devise/patcher.rb +1 -1
  21. data/lib/datadog/appsec/contrib/rack/patcher.rb +1 -1
  22. data/lib/datadog/appsec/contrib/rack/request_body_middleware.rb +6 -3
  23. data/lib/datadog/appsec/contrib/rails/request_middleware.rb +2 -1
  24. data/lib/datadog/appsec/contrib/sinatra/request_middleware.rb +2 -1
  25. data/lib/datadog/core/configuration/settings.rb +33 -41
  26. data/lib/datadog/core/configuration/supported_configurations.rb +3 -0
  27. data/lib/datadog/core/environment/gc.rb +1 -1
  28. data/lib/datadog/core/environment/vm_cache.rb +1 -1
  29. data/lib/datadog/core/environment/yjit.rb +2 -2
  30. data/lib/datadog/core/telemetry/event/synth_app_client_configuration_change.rb +1 -1
  31. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +80 -12
  32. data/lib/datadog/core/utils/sequence.rb +3 -6
  33. data/lib/datadog/core/utils/time.rb +16 -54
  34. data/lib/datadog/di/el/compiler.rb +65 -12
  35. data/lib/datadog/di/el/evaluator.rb +82 -2
  36. data/lib/datadog/di/el/expression.rb +10 -2
  37. data/lib/datadog/di/probe_builder.rb +6 -6
  38. data/lib/datadog/profiling/collectors/thread_context.rb +6 -13
  39. data/lib/datadog/profiling/component.rb +1 -0
  40. data/lib/datadog/profiling.rb +4 -0
  41. data/lib/datadog/tracing/component.rb +21 -0
  42. data/lib/datadog/tracing/configuration/ext.rb +1 -0
  43. data/lib/datadog/tracing/configuration/settings.rb +16 -0
  44. data/lib/datadog/tracing/contrib/hanami/router_tracing.rb +2 -1
  45. data/lib/datadog/tracing/contrib/kafka/events/connection/request.rb +4 -0
  46. data/lib/datadog/tracing/contrib/kafka/events/produce_operation/send_messages.rb +7 -2
  47. data/lib/datadog/tracing/contrib/kafka/events/producer/deliver_messages.rb +7 -2
  48. data/lib/datadog/tracing/contrib/sequel/database.rb +1 -5
  49. data/lib/datadog/tracing/distributed/propagation_policy.rb +5 -0
  50. data/lib/datadog/tracing/sync_writer.rb +8 -3
  51. data/lib/datadog/tracing/tracer.rb +8 -0
  52. data/lib/datadog/tracing/transport/native/response.rb +71 -0
  53. data/lib/datadog/tracing/transport/native.rb +363 -0
  54. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  55. data/lib/datadog/tracing/writer.rb +16 -3
  56. data/lib/datadog/version.rb +1 -1
  57. metadata +9 -5
@@ -16,8 +16,19 @@ module Datadog
16
16
  #
17
17
  # @api private
18
18
  class Compiler
19
+ # Compiles +ast+ into eval'able Ruby source.
20
+ #
21
+ # Returns the compiled source and companion compiled Regexp
22
+ # objects.
23
+ #
24
+ # @param ast [untyped] expression AST from the probe definition.
25
+ # @return [Array(String, Array<Regexp>)] the compiled Ruby source and
26
+ # the precompiled Regexps, indexed in the order the compiled code
27
+ # references them.
19
28
  def compile(ast)
20
- compile_partial(ast)
29
+ regexps = []
30
+ code = compile_partial(ast, regexps)
31
+ [code, regexps]
21
32
  end
22
33
 
23
34
  private
@@ -37,9 +48,13 @@ module Datadog
37
48
  len isEmpty isUndefined
38
49
  ].freeze # steep:ignore IncompatibleAssignment
39
50
 
51
+ # `matches` is also a two-argument method but is special-cased in
52
+ # #compile_partial so that regular expressions can be precompiled
53
+ # once, so it is not listed here.
54
+ #
40
55
  # Steep: https://github.com/soutaro/steep/issues/363
41
56
  TWO_ARG_METHODS = %w[
42
- startsWith endsWith contains matches
57
+ startsWith endsWith contains
43
58
  getmember index instanceof
44
59
  ].freeze # steep:ignore IncompatibleAssignment
45
60
 
@@ -49,13 +64,18 @@ module Datadog
49
64
  "or" => "||",
50
65
  }.freeze
51
66
 
52
- def compile_partial(ast)
67
+ # @param ast [untyped] AST node to compile.
68
+ # @param regexps [Array<Regexp>] output array that collects the
69
+ # companion precompiled Regexp objects.
70
+ # @return [String] compiled Ruby source for +ast+.
71
+ def compile_partial(ast, regexps)
53
72
  case ast
54
73
  when Hash
55
- if ast.length != 1
74
+ entry = ast.first
75
+ if ast.length != 1 || entry.nil?
56
76
  raise DI::Error::InvalidExpression, "Expected hash of length 1: #{ast}"
57
77
  end
58
- op, target = ast.first
78
+ op, target = entry
59
79
  case op
60
80
  when "ref"
61
81
  unless String === target
@@ -94,20 +114,33 @@ module Datadog
94
114
  end
95
115
  when *SINGLE_ARG_METHODS
96
116
  method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
97
- "#{method_name}(#{compile_partial(target)}, '#{var_name_maybe(target)}')"
117
+ "#{method_name}(#{compile_partial(target, regexps)}, '#{var_name_maybe(target)}')"
118
+ when "matches"
119
+ unless Array === target && target.length == 2
120
+ raise DI::Error::InvalidExpression, "Improper matches syntax"
121
+ end
122
+ first, second = target
123
+ if String === second
124
+ # Match against a literal regular expression (string).
125
+ index = precompile_regexp(second, regexps)
126
+ "matches_compiled(#{compile_partial(first, regexps)}, #{index})"
127
+ else
128
+ # Match against a (complex) expression.
129
+ "matches(#{compile_partial(first, regexps)}, (#{compile_partial(second, regexps)}))"
130
+ end
98
131
  when *TWO_ARG_METHODS
99
132
  method_name = op.gsub(/[A-Z]/) { |m| "_#{m.downcase}" }
100
133
  unless Array === target && target.length == 2
101
134
  raise DI::Error::InvalidExpression, "Improper #{op} syntax"
102
135
  end
103
136
  first, second = target
104
- "#{method_name}(#{compile_partial(first)}, (#{compile_partial(second)}))"
137
+ "#{method_name}(#{compile_partial(first, regexps)}, (#{compile_partial(second, regexps)}))"
105
138
  when *MULTI_ARG_METHODS.keys
106
139
  unless Array === target && target.length >= 1
107
140
  raise DI::Error::InvalidExpression, "Improper #{op} syntax"
108
141
  end
109
142
  compiled_targets = target.map do |item|
110
- "(#{compile_partial(item)})"
143
+ "(#{compile_partial(item, regexps)})"
111
144
  end
112
145
  compiled_op = MULTI_ARG_METHODS[op]
113
146
  "(#{compiled_targets.join(" #{compiled_op} ")})"
@@ -115,18 +148,21 @@ module Datadog
115
148
  unless Array === target && target.length == 3
116
149
  raise DI::Error::InvalidExpression, "Improper #{op} syntax"
117
150
  end
118
- "#{op}(#{target.map { |arg| "(#{compile_partial(arg)})" }.join(", ")})"
151
+ "#{op}(#{target.map { |arg| "(#{compile_partial(arg, regexps)})" }.join(", ")})"
119
152
  when "not"
120
- "!(#{compile_partial(target)})"
153
+ "!(#{compile_partial(target, regexps)})"
121
154
  when *OPERATORS.keys
122
155
  unless Array === target && target.length == 2
123
156
  raise DI::Error::InvalidExpression, "Improper #{op} syntax"
124
157
  end
125
158
  first, second = target
126
159
  operator = OPERATORS.fetch(op)
127
- "(#{compile_partial(first)}) #{operator} (#{compile_partial(second)})"
160
+ "(#{compile_partial(first, regexps)}) #{operator} (#{compile_partial(second, regexps)})"
128
161
  when "any", "all", "filter"
129
- "#{op}(#{compile_partial(target.first)}) { |current_item, current_key, current_value| #{compile_partial(target.last)} }"
162
+ unless Array === target && target.length == 2
163
+ raise DI::Error::InvalidExpression, "Improper #{op} syntax"
164
+ end
165
+ "#{op}(#{compile_partial(target.first, regexps)}) { |current_item, current_key, current_value| #{compile_partial(target.last, regexps)} }"
130
166
  else
131
167
  raise DI::Error::InvalidExpression, "Unknown operation: #{op}"
132
168
  end
@@ -162,6 +198,23 @@ module Datadog
162
198
  def escape(needle)
163
199
  needle.gsub("\\") { "\\\\" }.gsub('"') { "\\\"" }.gsub("#") { "\\#" }
164
200
  end
201
+
202
+ # Compile a literal regular expression +regexp_str+ at
203
+ # instrumentation time. Append it to +regexps+, returning its index for
204
+ # Evaluator#matches_compiled to look up.
205
+ #
206
+ # @param regexp_str [String] regular expression source.
207
+ # @param regexps [Array<Regexp>] output array to append the compiled
208
+ # regexp to.
209
+ # @return [Integer] index into +regexps+.
210
+ # @raise [DI::Error::InvalidExpression] if +needle+ is not a valid regexp.
211
+ def precompile_regexp(regexp_str, regexps)
212
+ index = regexps.length
213
+ regexps << Evaluator.compile_regexp(regexp_str)
214
+ index
215
+ rescue RegexpError => exc
216
+ raise DI::Error::InvalidExpression, "Invalid regular expression in matches: #{exc.class}: #{exc.message}"
217
+ end
165
218
  end
166
219
  end
167
220
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../../ruby_version"
4
+
5
+ require "timeout" if Datadog::RubyVersion.is?("< 3.2")
6
+
3
7
  module Datadog
4
8
  module DI
5
9
  module EL
@@ -7,6 +11,20 @@ module Datadog
7
11
  #
8
12
  # @api private
9
13
  class Evaluator
14
+ # Maximum wall-clock time allowed for evaluating a single `matches`
15
+ # operator.
16
+ MATCHES_TIMEOUT_SECONDS = 0.5
17
+
18
+ # @param regexps [Array<Regexp>] Regexps precompiled from literal
19
+ # `matches` patterns, looked up by index by #matches_compiled.
20
+ # Empty when the expression has no `matches` pattern with
21
+ # a direct regular expression argument.
22
+ def initialize(regexps = [])
23
+ @regexps = regexps
24
+ end
25
+
26
+ attr_reader :regexps
27
+
10
28
  def ref(var)
11
29
  @context.fetch(var)
12
30
  end
@@ -48,10 +66,72 @@ module Datadog
48
66
  end
49
67
  end
50
68
 
69
+ # Build a Regexp from a pattern string. On Ruby 3.2+ the per-match
70
+ # timeout (MATCHES_TIMEOUT_SECONDS) is baked into the compiled
71
+ # Regexp via the in-engine `timeout:` keyword, which interrupts the
72
+ # matcher at every backtrack step and reliably bounds matcher
73
+ # runtime regardless of pattern shape. On older Rubies the Regexp
74
+ # carries no timeout; the bound is applied at match time by
75
+ # #bounded_match? instead.
76
+ #
77
+ # @param needle [String] regexp source.
78
+ # @return [Regexp] compiled regexp, with baked-in timeout on Ruby 3.2+.
79
+ if Datadog::RubyVersion.is?(">= 3.2")
80
+ def self.compile_regexp(needle)
81
+ Regexp.new(needle, timeout: MATCHES_TIMEOUT_SECONDS)
82
+ end
83
+ else
84
+ def self.compile_regexp(needle)
85
+ Regexp.compile(needle)
86
+ end
87
+ end
88
+
89
+ # Match +haystack+ against a regexp whose needle is computed at
90
+ # evaluation time, so the Regexp cannot be precompiled. Literal needles are
91
+ # precompiled by the Compiler and dispatched to #matches_compiled.
92
+ #
93
+ # @param haystack [String] string to match against.
94
+ # @param needle [String] regexp source.
95
+ # @return [Boolean] whether the haystack matches the regexp.
96
+ # @raise [Regexp::TimeoutError] (Ruby 3.2+) regexp engine exceeded MATCHES_TIMEOUT_SECONDS.
97
+ # @raise [Timeout::Error] (Ruby < 3.2) Timeout.timeout fired.
51
98
  def matches(haystack, needle)
52
- re = Regexp.compile(needle)
53
- !!(haystack =~ re)
99
+ bounded_match?(Evaluator.compile_regexp(needle), haystack)
100
+ end
101
+
102
+ # Match +haystack+ against a Regexp precompiled at expression-compile
103
+ # time, looked up by index in +regexps+.
104
+ #
105
+ # @param haystack [String] string to match against.
106
+ # @param index [Integer] position of the precompiled Regexp in +regexps+.
107
+ # @return [Boolean] whether the haystack matches the regexp.
108
+ # @raise [Regexp::TimeoutError] (Ruby 3.2+) regexp engine exceeded MATCHES_TIMEOUT_SECONDS.
109
+ # @raise [Timeout::Error] (Ruby < 3.2) Timeout.timeout fired.
110
+ def matches_compiled(haystack, index)
111
+ bounded_match?(regexps.fetch(index), haystack)
112
+ end
113
+
114
+ # Match +haystack+ against +re+, bounded at MATCHES_TIMEOUT_SECONDS
115
+ # wall-clock.
116
+ #
117
+ # @param re [Regexp] regexp to match against.
118
+ # @param haystack [String] string to match against.
119
+ # @return [Boolean] whether the haystack matches the regexp.
120
+ if Datadog::RubyVersion.is?(">= 3.2")
121
+ def bounded_match?(re, haystack)
122
+ # Uses Regexp#match? rather than =~ so that the
123
+ # thread-local match data ($~, $1, ...) is not mutated as a side
124
+ # effect of DI expression evaluation.
125
+ re.match?(haystack)
126
+ end
127
+ else
128
+ def bounded_match?(re, haystack)
129
+ Timeout.timeout(MATCHES_TIMEOUT_SECONDS) do
130
+ re.match?(haystack)
131
+ end
132
+ end
54
133
  end
134
+ private :bounded_match?
55
135
 
56
136
  def getmember(object, field)
57
137
  object.instance_variable_get("@#{field}")
@@ -7,7 +7,12 @@ module Datadog
7
7
  #
8
8
  # @api private
9
9
  class Expression
10
- def initialize(dsl_expr, compiled_expr)
10
+ # @param dsl_expr [String] human-readable DSL form, kept for debugging.
11
+ # @param compiled_expr [String] Ruby source produced by Compiler#compile.
12
+ # @param regexps [Array<Regexp>] precompiled `matches` regexps (see
13
+ # Compiler#precompile_regexp), the second element returned by
14
+ # Compiler#compile.
15
+ def initialize(dsl_expr, compiled_expr, regexps = [])
11
16
  unless String === compiled_expr
12
17
  raise ArgumentError, "compiled_expr must be a string"
13
18
  end
@@ -23,7 +28,10 @@ module Datadog
23
28
  end
24
29
  RUBY
25
30
  end
26
- @evaluator = cls.new
31
+ # cls inherits Evaluator#initialize(regexps), but Steep types
32
+ # Class#new as () -> untyped and cannot see that initializer
33
+ # through this dynamically created subclass.
34
+ @evaluator = cls.new(regexps) # steep:ignore UnexpectedPositionalArgument
27
35
  end
28
36
 
29
37
  attr_reader :dsl_expr
@@ -45,8 +45,8 @@ module Datadog
45
45
  unless cond_spec["dsl"] && cond_spec["json"]
46
46
  raise ArgumentError, "Malformed condition specification for probe: #{config}"
47
47
  end
48
- compiled = EL::Compiler.new.compile(cond_spec["json"])
49
- EL::Expression.new(cond_spec["dsl"], compiled)
48
+ compiled, regexps = EL::Compiler.new.compile(cond_spec["json"])
49
+ EL::Expression.new(cond_spec["dsl"], compiled, regexps)
50
50
  end
51
51
  capture_expressions = build_capture_expressions(config["captureExpressions"])
52
52
  capture_expressions = dedup_capture_expressions(capture_expressions, config["id"], logger)
@@ -100,8 +100,8 @@ module Datadog
100
100
  unless Hash === expr_spec && expr_spec["dsl"] && expr_spec["json"]
101
101
  raise ArgumentError, "captureExpressions entry #{name}: missing or malformed expr"
102
102
  end
103
- compiled = EL::Compiler.new.compile(expr_spec["json"])
104
- expr = EL::Expression.new(expr_spec["dsl"], compiled)
103
+ compiled, regexps = EL::Compiler.new.compile(expr_spec["json"])
104
+ expr = EL::Expression.new(expr_spec["dsl"], compiled, regexps)
105
105
  limits = build_capture_limits(entry["capture"])
106
106
  CaptureExpression.new(name: name, expr: expr, limits: limits)
107
107
  end
@@ -152,8 +152,8 @@ module Datadog
152
152
  unless dsl = segment["dsl"]
153
153
  raise ArgumentError, "Missing dsl for json in segment: #{segment}"
154
154
  end
155
- compiled = EL::Compiler.new.compile(ast)
156
- EL::Expression.new(dsl, compiled)
155
+ compiled, regexps = EL::Compiler.new.compile(ast)
156
+ EL::Expression.new(dsl, compiled, regexps)
157
157
  else
158
158
  # TODO report to telemetry?
159
159
  end
@@ -21,7 +21,8 @@ module Datadog
21
21
  endpoint_collection_enabled:,
22
22
  waiting_for_gvl_threshold_ns:,
23
23
  otel_context_enabled:,
24
- native_filenames_enabled:
24
+ native_filenames_enabled:,
25
+ show_classes:
25
26
  )
26
27
  tracer_context_key = safely_extract_context_key_from(tracer)
27
28
  self.class._native_initialize(
@@ -32,7 +33,8 @@ module Datadog
32
33
  endpoint_collection_enabled: endpoint_collection_enabled,
33
34
  waiting_for_gvl_threshold_ns: waiting_for_gvl_threshold_ns,
34
35
  otel_context_enabled: otel_context_enabled,
35
- native_filenames_enabled: validate_native_filenames(native_filenames_enabled),
36
+ native_filenames_enabled: native_filenames_enabled,
37
+ show_classes: show_classes,
36
38
  overhead_filename: __FILE__,
37
39
  )
38
40
  end
@@ -45,6 +47,7 @@ module Datadog
45
47
  waiting_for_gvl_threshold_ns: 10_000_000,
46
48
  otel_context_enabled: false,
47
49
  native_filenames_enabled: true,
50
+ show_classes: false,
48
51
  trigger_global_reset: true,
49
52
  **options
50
53
  )
@@ -56,6 +59,7 @@ module Datadog
56
59
  waiting_for_gvl_threshold_ns: waiting_for_gvl_threshold_ns,
57
60
  otel_context_enabled: otel_context_enabled,
58
61
  native_filenames_enabled: native_filenames_enabled,
62
+ show_classes: show_classes,
59
63
  **options,
60
64
  )
61
65
 
@@ -90,17 +94,6 @@ module Datadog
90
94
  context = provider.instance_variable_get(:@context)
91
95
  context&.instance_variable_get(:@key)
92
96
  end
93
-
94
- def validate_native_filenames(native_filenames_enabled)
95
- if native_filenames_enabled && !Datadog::Profiling::Collectors::Stack._native_filenames_available?
96
- Datadog.logger.debug(
97
- "Native filenames are enabled, but the required dladdr API was not available. Disabling native filenames."
98
- )
99
- false
100
- else
101
- native_filenames_enabled
102
- end
103
- end
104
97
  end
105
98
  end
106
99
  end
@@ -105,6 +105,7 @@ module Datadog
105
105
  waiting_for_gvl_threshold_ns: settings.profiling.advanced.waiting_for_gvl_threshold_ns,
106
106
  otel_context_enabled: settings.profiling.advanced.preview_otel_context_enabled,
107
107
  native_filenames_enabled: settings.profiling.advanced.native_filenames_enabled,
108
+ show_classes: settings.profiling.advanced.experimental_show_classes_enabled,
108
109
  )
109
110
  end
110
111
 
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "rbconfig"
3
4
  require_relative "core"
4
5
  require_relative "core/environment/variable_helpers"
5
6
  require_relative "core/utils/only_once"
@@ -7,6 +8,9 @@ require_relative "core/utils/only_once"
7
8
  module Datadog
8
9
  # Datadog Continuous Profiler implementation: https://docs.datadoghq.com/profiler/
9
10
  module Profiling
11
+ STATIC_RUBY_PATH = (RbConfig::CONFIG["ENABLE_SHARED"] == "no") ? RbConfig.ruby.freeze : nil
12
+ private_constant :STATIC_RUBY_PATH
13
+
10
14
  def self.supported?
11
15
  unsupported_reason.nil?
12
16
  end
@@ -104,9 +104,30 @@ module Datadog
104
104
  return writer
105
105
  end
106
106
 
107
+ if settings.tracing.native_transport && (transport = build_native_transport(agent_settings))
108
+ options = options.merge(transport: transport)
109
+ end
110
+
107
111
  Tracing::Writer.new(agent_settings: agent_settings, **options)
108
112
  end
109
113
 
114
+ def build_native_transport(agent_settings)
115
+ require_relative "transport/native"
116
+
117
+ unless Transport::Native.supported?
118
+ Datadog.logger.warn(
119
+ "Native transport requested but not available: #{Transport::Native::UNSUPPORTED_REASON}. " \
120
+ "Falling back to default HTTP transport."
121
+ )
122
+ return nil
123
+ end
124
+
125
+ Transport::Native::Transport.new(
126
+ agent_settings: agent_settings,
127
+ logger: Datadog.logger
128
+ )
129
+ end
130
+
110
131
  def subscribe_to_writer_events!(writer, sampler_delegator, test_mode)
111
132
  return unless writer.respond_to?(:events) # Check if it's a custom, external writer
112
133
 
@@ -16,6 +16,7 @@ module Datadog
16
16
  ENV_RESOURCE_RENAMING_ENABLED = "DD_TRACE_RESOURCE_RENAMING_ENABLED"
17
17
  ENV_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT = "DD_TRACE_RESOURCE_RENAMING_ALWAYS_SIMPLIFIED_ENDPOINT"
18
18
  ENV_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED = "DD_EXPERIMENTAL_PROPAGATE_PROCESS_TAGS_ENABLED"
19
+ ENV_EXPERIMENTAL_NATIVE_TRANSPORT_ENABLED = "DD_EXPERIMENTAL_NATIVE_TRANSPORT_ENABLED"
19
20
 
20
21
  # @public_api
21
22
  module SpanAttributeSchema
@@ -482,6 +482,22 @@ module Datadog
482
482
  end
483
483
  end
484
484
 
485
+ # Use the native trace transport (Rust via C FFI) instead of
486
+ # the default pure-Ruby HTTP transport.
487
+ #
488
+ # The native transport delegates serialization, stats
489
+ # computation, and HTTP sending to the Rust data pipeline.
490
+ #
491
+ # This option is recommended for internal use only.
492
+ #
493
+ # @default `false`
494
+ # @return [Boolean]
495
+ option :native_transport do |o|
496
+ o.env Configuration::Ext::ENV_EXPERIMENTAL_NATIVE_TRANSPORT_ENABLED
497
+ o.default false
498
+ o.type :bool
499
+ end
500
+
485
501
  # A custom writer instance.
486
502
  # The object must respect the {Datadog::Tracing::Writer} interface.
487
503
  #
@@ -10,7 +10,8 @@ module Datadog
10
10
  # Hanami Instrumentation for `hanami.routing`
11
11
  module RouterTracing
12
12
  def call(env)
13
- return super if Tracing.active_span && Tracing.active_span.name == Ext::SPAN_ROUTING
13
+ active_span = Tracing.active_span
14
+ return super if active_span && active_span.name == Ext::SPAN_ROUTING
14
15
 
15
16
  Tracing.trace(
16
17
  Ext::SPAN_ROUTING,
@@ -21,6 +21,10 @@ module Datadog
21
21
  super
22
22
 
23
23
  span.resource = payload[:api]
24
+ end
25
+
26
+ def on_finish(span, _event, _id, payload)
27
+ super
24
28
 
25
29
  span.set_tag(Ext::TAG_REQUEST_SIZE, payload[:request_size]) if payload.key?(:request_size)
26
30
  span.set_tag(Ext::TAG_RESPONSE_SIZE, payload[:response_size]) if payload.key?(:response_size)
@@ -17,12 +17,17 @@ module Datadog
17
17
 
18
18
  module_function
19
19
 
20
- def on_start(span, _event, _id, payload)
20
+ def on_start(span, _event, _id, _payload)
21
+ super
22
+
23
+ span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PRODUCER)
24
+ end
25
+
26
+ def on_finish(span, _event, _id, payload)
21
27
  super
22
28
 
23
29
  span.set_tag(Ext::TAG_MESSAGE_COUNT, payload[:message_count]) if payload.key?(:message_count)
24
30
  span.set_tag(Ext::TAG_SENT_MESSAGE_COUNT, payload[:sent_message_count]) if payload.key?(:sent_message_count)
25
- span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PRODUCER)
26
31
  end
27
32
 
28
33
  def span_name
@@ -17,7 +17,13 @@ module Datadog
17
17
 
18
18
  module_function
19
19
 
20
- def on_start(span, _event, _id, payload)
20
+ def on_start(span, _event, _id, _payload)
21
+ super
22
+
23
+ span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PRODUCER)
24
+ end
25
+
26
+ def on_finish(span, _event, _id, payload)
21
27
  super
22
28
 
23
29
  span.set_tag(Ext::TAG_ATTEMPTS, payload[:attempts]) if payload.key?(:attempts)
@@ -25,7 +31,6 @@ module Datadog
25
31
  if payload.key?(:delivered_message_count)
26
32
  span.set_tag(Ext::TAG_DELIVERED_MESSAGE_COUNT, payload[:delivered_message_count])
27
33
  end
28
- span.set_tag(Tracing::Metadata::Ext::TAG_KIND, Tracing::Metadata::Ext::SpanKind::TAG_PRODUCER)
29
34
  end
30
35
 
31
36
  def span_name
@@ -46,11 +46,7 @@ module Datadog
46
46
  end
47
47
 
48
48
  def parse_opts(sql, opts)
49
- db_opts = if ::Sequel::VERSION < "3.41.0" && self.class.to_s !~ /Dataset$/
50
- @opts
51
- elsif instance_variable_defined?(:@pool) && @pool
52
- @pool.db.opts
53
- end
49
+ db_opts = @pool.db.opts if instance_variable_defined?(:@pool) && @pool
54
50
  sql = sql.is_a?(::Sequel::SQL::Expression) ? literal(sql) : sql.to_s
55
51
 
56
52
  Utils.parse_opts(sql, opts, db_opts)
@@ -28,6 +28,11 @@ module Datadog
28
28
  return true
29
29
  end
30
30
 
31
+ if ::Datadog.configuration.respond_to?(:ai_guard) && ::Datadog.configuration.ai_guard.enabled &&
32
+ (trace_source & ::Datadog::AIGuard::Ext::PRODUCT_BIT) != 0
33
+ return true
34
+ end
35
+
31
36
  return false
32
37
  end
33
38
 
@@ -46,10 +46,15 @@ module Datadog
46
46
  logger.debug(e)
47
47
  end
48
48
 
49
- # Does nothing.
50
- # The {SyncWriter} does not need to be stopped as it holds no state.
49
+ # Stops the {SyncWriter}.
50
+ # The {SyncWriter} holds no worker thread, but it owns its transport, so
51
+ # on teardown we deterministically release transports that hold native
52
+ # resources (e.g. the native trace exporter's Rust runtime and
53
+ # process-global fork hooks) rather than relying on the GC finalizer.
54
+ # The default HTTP transport has no `#close` and is left untouched, and
55
+ # `#close` is idempotent so repeated `#stop` calls are safe.
51
56
  def stop
52
- # No cleanup to do for the SyncWriter
57
+ @transport.close if @transport.respond_to?(:close)
53
58
  true
54
59
  end
55
60
 
@@ -628,6 +628,10 @@ module Datadog
628
628
  appsec_bit = upstream_tags[Tracing::Metadata::Ext::Distributed::TAG_TRACE_SOURCE].to_i(16) &
629
629
  Datadog::AppSec::Ext::PRODUCT_BIT
630
630
  return appsec_enabled if appsec_bit != 0
631
+
632
+ ai_guard_bit = upstream_tags[Tracing::Metadata::Ext::Distributed::TAG_TRACE_SOURCE].to_i(16) &
633
+ Datadog::AIGuard::Ext::PRODUCT_BIT
634
+ return ai_guard_enabled if ai_guard_bit != 0
631
635
  end
632
636
 
633
637
  false
@@ -642,6 +646,10 @@ module Datadog
642
646
  @appsec_enabled ||= Datadog.configuration.appsec.enabled
643
647
  end
644
648
 
649
+ def ai_guard_enabled
650
+ @ai_guard_enabled ||= Datadog.configuration.respond_to?(:ai_guard) && Datadog.configuration.ai_guard.enabled
651
+ end
652
+
645
653
  # Due to APM Tracing (the product) and Tracing (the transport) being intertwined, we cannot completely disabled APM
646
654
  # without also disabling the tracer. When setting `@apm_tracing_enabled` to `false`, it does not disable the tracer,
647
655
  # but rather only sends heartbeat traces (1 per minutes), so that the service is considered alive in the backend.
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Datadog
6
+ module Tracing
7
+ module Transport
8
+ module Native
9
+ # Response from the native trace exporter.
10
+ #
11
+ # Constructed by the C extension after a send completes (or fails).
12
+ # Implements the same predicate interface as the HTTP transport's
13
+ # response so callers can treat both uniformly.
14
+ class Response
15
+ SERVICE_RATE_KEY = "rate_by_service"
16
+
17
+ attr_reader :trace_count, :payload
18
+
19
+ def initialize(ok:, internal_error: false, server_error: false, client_error: false,
20
+ not_found: false, unsupported: false, trace_count: 0, payload: nil)
21
+ @ok = ok
22
+ @internal_error = internal_error
23
+ @server_error = server_error
24
+ @client_error = client_error
25
+ @not_found = not_found
26
+ @unsupported = unsupported
27
+ @trace_count = trace_count
28
+ @payload = payload
29
+ end
30
+
31
+ def ok?
32
+ @ok
33
+ end
34
+
35
+ def internal_error?
36
+ @internal_error
37
+ end
38
+
39
+ def server_error?
40
+ @server_error
41
+ end
42
+
43
+ def client_error?
44
+ @client_error
45
+ end
46
+
47
+ def not_found?
48
+ @not_found
49
+ end
50
+
51
+ def unsupported?
52
+ @unsupported
53
+ end
54
+
55
+ # Parse the agent's JSON response body and extract the
56
+ # +rate_by_service+ map. Returns +nil+ when the payload
57
+ # is absent or does not contain sampling rates.
58
+ def service_rates
59
+ payload = @payload
60
+ return nil if payload.nil? || payload.empty?
61
+
62
+ parsed = JSON.parse(payload)
63
+ parsed[SERVICE_RATE_KEY] if parsed.is_a?(Hash)
64
+ rescue JSON::ParserError
65
+ nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end