datadog 2.37.0 → 2.38.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/ext/datadog_profiling_native_extension/collectors_cpu_and_wall_time_worker.c +23 -15
  4. data/ext/datadog_profiling_native_extension/collectors_stack.c +19 -24
  5. data/ext/datadog_profiling_native_extension/collectors_stack.h +0 -1
  6. data/ext/datadog_profiling_native_extension/collectors_thread_context.c +155 -141
  7. data/ext/datadog_profiling_native_extension/collectors_thread_context.h +1 -0
  8. data/ext/datadog_profiling_native_extension/extconf.rb +0 -3
  9. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +5 -0
  10. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  11. data/ext/datadog_profiling_native_extension/time_helpers.h +12 -7
  12. data/lib/datadog/appsec/contrib/rack/gateway/request.rb +5 -0
  13. data/lib/datadog/appsec/contrib/rails/gateway/request.rb +5 -0
  14. data/lib/datadog/core/configuration/agentless_settings_resolver.rb +2 -2
  15. data/lib/datadog/core/configuration/settings.rb +4 -4
  16. data/lib/datadog/core/configuration/supported_configurations.rb +1 -0
  17. data/lib/datadog/core/knuth_sampler.rb +3 -1
  18. data/lib/datadog/core/metrics/client.rb +1 -1
  19. data/lib/datadog/core/transport/http/builder.rb +1 -1
  20. data/lib/datadog/core/utils/at_fork_monkey_patch.rb +1 -1
  21. data/lib/datadog/core/utils/time.rb +5 -1
  22. data/lib/datadog/di/boot.rb +3 -0
  23. data/lib/datadog/di/capture_expression.rb +21 -0
  24. data/lib/datadog/di/capture_expression_evaluator.rb +71 -0
  25. data/lib/datadog/di/capture_limits.rb +41 -0
  26. data/lib/datadog/di/component.rb +2 -2
  27. data/lib/datadog/di/configuration.rb +6 -0
  28. data/lib/datadog/di/context.rb +9 -3
  29. data/lib/datadog/di/el/compiler.rb +1 -1
  30. data/lib/datadog/di/instrumenter.rb +34 -3
  31. data/lib/datadog/di/probe.rb +47 -2
  32. data/lib/datadog/di/probe_builder.rb +83 -1
  33. data/lib/datadog/di/probe_notification_builder.rb +48 -7
  34. data/lib/datadog/di/remote.rb +1 -1
  35. data/lib/datadog/di/serializer.rb +22 -10
  36. data/lib/datadog/profiling/collectors/cpu_and_wall_time_worker.rb +5 -0
  37. data/lib/datadog/profiling/collectors/thread_context.rb +9 -1
  38. data/lib/datadog/profiling/component.rb +17 -0
  39. data/lib/datadog/tracing/contrib/active_job/data_streams.rb +62 -0
  40. data/lib/datadog/tracing/contrib/active_job/patcher.rb +12 -0
  41. data/lib/datadog/tracing/contrib/active_model_serializers/events/serialize.rb +2 -3
  42. data/lib/datadog/tracing/contrib/pg/instrumentation.rb +12 -12
  43. data/lib/datadog/tracing/contrib/rack/patcher.rb +1 -1
  44. data/lib/datadog/tracing/contrib/sequel/utils.rb +2 -2
  45. data/lib/datadog/tracing/contrib/sidekiq/server_tracer.rb +1 -1
  46. data/lib/datadog/tracing/contrib/sinatra/framework.rb +1 -1
  47. data/lib/datadog/tracing/transport/trace_formatter.rb +1 -1
  48. data/lib/datadog/version.rb +1 -1
  49. metadata +9 -5
@@ -32,7 +32,9 @@ module Datadog
32
32
  @knuth_factor = knuth_factor
33
33
 
34
34
  rate = rate.to_f
35
- unless rate >= 0.0 && rate <= 1.0
35
+ # Not using `between?` here: it raises on NaN instead of falling back to 1.0 below.
36
+ # No current caller passes NaN, but this guards against it defensively for future ones.
37
+ unless rate >= 0.0 && rate <= 1.0 # rubocop:disable Style/ComparableBetween
36
38
  Datadog.logger.warn("Sample rate #{rate} is not between 0.0 and 1.0, falling back to 1.0")
37
39
  rate = 1.0
38
40
  end
@@ -157,7 +157,7 @@ module Datadog
157
157
  begin
158
158
  if send_stats? && !start.nil?
159
159
  finished = Utils::Time.get_time
160
- distribution(stat, ((finished - start) * 1000), options)
160
+ distribution(stat, (finished - start) * 1000, options)
161
161
  end
162
162
  rescue => e
163
163
  # TODO: Likely to be redundant, since `distribution` handles its own errors.
@@ -108,7 +108,7 @@ module Datadog
108
108
 
109
109
  # Resolve fallback and merge headers
110
110
  fallback = api_options.delete(:fallback)
111
- api_options[:headers] = @default_headers.merge((api_options[:headers] || {}))
111
+ api_options[:headers] = @default_headers.merge(api_options[:headers] || {})
112
112
 
113
113
  # Add API::Instance with all settings
114
114
  instances[key] = API::Instance.new(
@@ -6,7 +6,7 @@ module Datadog
6
6
  # Monkey patches `Kernel#fork` and similar functions, adding an `at_fork` callback mechanism which
7
7
  # is used to restart observability after the VM forks (e.g. in multiprocess Ruby apps).
8
8
  module AtForkMonkeyPatch
9
- AT_FORK_CHILD_BLOCKS = [] # rubocop:disable Style/MutableConstant Used to store blocks to run, mutable by design.
9
+ AT_FORK_CHILD_BLOCKS = [] # rubocop:disable Style/MutableConstant -- Used to store blocks to run, mutable by design.
10
10
  private_constant :AT_FORK_CHILD_BLOCKS
11
11
 
12
12
  def self.supported?
@@ -8,11 +8,15 @@ module Datadog
8
8
  module_function
9
9
 
10
10
  # Current monotonic time
11
+ # On macOS, CLOCK_MONOTONIC only has microsecond precision,
12
+ # so we use CLOCK_MONOTONIC_RAW which has nanosecond precision instead.
11
13
  #
12
14
  # @param unit [Symbol] unit for the resulting value, same as ::Process#clock_gettime, defaults to :float_second
13
15
  # @return [Float|Integer] timestamp in the requested unit, since some unspecified starting point
16
+ MONOTONIC_CLOCK_ID = RUBY_PLATFORM.include?("darwin") ? Process::CLOCK_MONOTONIC_RAW : Process::CLOCK_MONOTONIC
17
+
14
18
  def get_time(unit = :float_second)
15
- Process.clock_gettime(Process::CLOCK_MONOTONIC, unit)
19
+ Process.clock_gettime(MONOTONIC_CLOCK_ID, unit)
16
20
  end
17
21
 
18
22
  # Current wall time.
@@ -6,6 +6,9 @@ require_relative 'error'
6
6
  require_relative 'code_tracker'
7
7
  require_relative 'component'
8
8
  require_relative 'context'
9
+ require_relative 'capture_limits'
10
+ require_relative 'capture_expression'
11
+ require_relative 'capture_expression_evaluator'
9
12
  require_relative 'instrumenter'
10
13
  require_relative 'probe'
11
14
  require_relative 'probe_builder'
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capture_limits"
4
+
5
+ module Datadog
6
+ module DI
7
+ class CaptureExpression
8
+ def initialize(name:, expr:, limits: nil)
9
+ @name = name
10
+ @expr = expr
11
+ @limits = limits
12
+ end
13
+
14
+ attr_reader :name
15
+
16
+ attr_reader :expr
17
+
18
+ attr_reader :limits
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "capture_expression"
4
+ require_relative "capture_limits"
5
+ require_relative "fatal_exceptions"
6
+
7
+ module Datadog
8
+ module DI
9
+ class CaptureExpressionEvaluator
10
+ TELEMETRY_NAMESPACE = "dynamic_instrumentation"
11
+
12
+ def initialize(settings:, serializer:, logger:, telemetry: nil)
13
+ @settings = settings
14
+ @serializer = serializer
15
+ @logger = logger
16
+ @telemetry = telemetry
17
+ end
18
+
19
+ attr_reader :settings
20
+
21
+ attr_reader :serializer
22
+
23
+ attr_reader :logger
24
+
25
+ attr_reader :telemetry
26
+
27
+ def evaluate(probe, context)
28
+ budget_ns = settings.dynamic_instrumentation.max_time_to_serialize_ms * 1_000_000
29
+ deadline_ns = ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) + budget_ns
30
+
31
+ output = {}
32
+ evaluation_errors = []
33
+
34
+ probe.capture_expressions.each do |capture_expression|
35
+ name = capture_expression.name
36
+
37
+ if ::Process.clock_gettime(::Process::CLOCK_MONOTONIC, :nanosecond) >= deadline_ns
38
+ output[name] = {notCapturedReason: "timeout"}
39
+ telemetry&.inc(TELEMETRY_NAMESPACE, "capture_expressions_skipped_by_timeout", 1)
40
+ next
41
+ end
42
+
43
+ begin
44
+ value = capture_expression.expr.evaluate(context)
45
+ limits = CaptureLimits.resolve(
46
+ expr_limits: capture_expression.limits,
47
+ probe: probe,
48
+ settings: settings,
49
+ )
50
+ output[name] = serializer.serialize_value(
51
+ value, name: name,
52
+ depth: limits[:depth],
53
+ attribute_count: limits[:attribute_count],
54
+ length: limits[:length],
55
+ collection_size: limits[:collection_size],
56
+ )
57
+ rescue Exception => exc # standard:disable Lint/RescueException
58
+ Datadog::DI.reraise_if_fatal(exc)
59
+ evaluation_errors << {expr: name, message: "#{exc.class}: #{exc.message}"}
60
+ logger.debug do
61
+ "di: probe #{probe.id}: capture expression #{name}: evaluation failed: #{exc.class}: #{exc.message}"
62
+ end
63
+ telemetry&.report(exc, description: "DI capture-expression evaluation failed")
64
+ end
65
+ end
66
+
67
+ [output, evaluation_errors]
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ class CaptureLimits
6
+ def initialize(max_reference_depth: nil, max_collection_size: nil,
7
+ max_length: nil, max_field_count: nil)
8
+ @max_reference_depth = max_reference_depth
9
+ @max_collection_size = max_collection_size
10
+ @max_length = max_length
11
+ @max_field_count = max_field_count
12
+ end
13
+
14
+ attr_reader :max_reference_depth
15
+
16
+ attr_reader :max_collection_size
17
+
18
+ attr_reader :max_length
19
+
20
+ attr_reader :max_field_count
21
+
22
+ def self.resolve(expr_limits:, probe:, settings:)
23
+ di = settings.dynamic_instrumentation
24
+ {
25
+ depth: expr_limits&.max_reference_depth ||
26
+ probe.max_capture_depth ||
27
+ di.max_capture_depth,
28
+ collection_size: expr_limits&.max_collection_size ||
29
+ probe.max_capture_collection_size ||
30
+ di.max_capture_collection_size,
31
+ length: expr_limits&.max_length ||
32
+ probe.max_capture_string_length ||
33
+ di.max_capture_string_length,
34
+ attribute_count: expr_limits&.max_field_count ||
35
+ probe.max_capture_attribute_count ||
36
+ di.max_capture_attribute_count,
37
+ }
38
+ end
39
+ end
40
+ end
41
+ end
@@ -76,7 +76,7 @@ module Datadog
76
76
  @serializer = Serializer.new(settings, redactor, telemetry: telemetry)
77
77
  @instrumenter = Instrumenter.new(settings, serializer, logger, code_tracker: code_tracker, telemetry: telemetry)
78
78
  @probe_repository = ProbeRepository.new
79
- @probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer)
79
+ @probe_notification_builder = ProbeNotificationBuilder.new(settings, serializer, logger, telemetry: telemetry)
80
80
  @probe_notifier_worker = ProbeNotifierWorker.new(
81
81
  settings, logger,
82
82
  agent_settings: agent_settings,
@@ -185,7 +185,7 @@ module Datadog
185
185
  end
186
186
 
187
187
  def parse_probe_spec_and_notify(probe_spec)
188
- probe = ProbeBuilder.build_from_remote_config(probe_spec)
188
+ probe = ProbeBuilder.build_from_remote_config(probe_spec, logger: logger)
189
189
  rescue Exception => exc # standard:disable Lint/RescueException
190
190
  Datadog::DI.reraise_if_fatal(exc)
191
191
  begin
@@ -134,6 +134,12 @@ module Datadog
134
134
  o.default 20
135
135
  end
136
136
 
137
+ option :max_time_to_serialize_ms do |o|
138
+ o.type :int
139
+ o.default 200
140
+ o.env 'DD_DYNAMIC_INSTRUMENTATION_MAX_TIME_TO_SERIALIZE'
141
+ end
142
+
137
143
  # Settings in the 'internal' group are for internal Datadog
138
144
  # use only, and are needed to test dynamic instrumentation or
139
145
  # experiment with features not released to customers.
@@ -14,6 +14,8 @@ module Datadog
14
14
  target_self: nil,
15
15
  path: nil, caller_locations: nil,
16
16
  serialized_entry_args: nil,
17
+ entry_capture_expressions: nil,
18
+ entry_capture_evaluation_errors: nil,
17
19
  return_value: nil, duration: nil, exception: nil)
18
20
  @probe = probe
19
21
  @settings = settings
@@ -23,6 +25,8 @@ module Datadog
23
25
  @path = path
24
26
  @caller_locations = caller_locations
25
27
  @serialized_entry_args = serialized_entry_args
28
+ @entry_capture_expressions = entry_capture_expressions
29
+ @entry_capture_evaluation_errors = entry_capture_evaluation_errors
26
30
  @return_value = return_value
27
31
  @duration = duration
28
32
  @exception = exception
@@ -44,6 +48,10 @@ module Datadog
44
48
 
45
49
  attr_reader :serialized_entry_args
46
50
 
51
+ attr_reader :entry_capture_expressions
52
+
53
+ attr_reader :entry_capture_evaluation_errors
54
+
47
55
  # Return value for the method, for a method probe
48
56
  attr_reader :return_value
49
57
 
@@ -55,9 +63,7 @@ module Datadog
55
63
 
56
64
  def serialized_locals
57
65
  # TODO cache?
58
- locals && serializer.serialize_vars(locals,
59
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
60
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
66
+ locals && serializer.serialize_vars(locals, **probe.snapshot_serializer_limits(settings))
61
67
  end
62
68
 
63
69
  def fetch(var_name)
@@ -78,7 +78,7 @@ module Datadog
78
78
  # We could format to a string here but what if customer
79
79
  # has @duration as part of an expression and wants
80
80
  # to retain it as a number?
81
- "(context.duration * 1000)"
81
+ "(context.duration && context.duration * 1000)"
82
82
  when '@exception'
83
83
  "context.exception"
84
84
  else
@@ -3,6 +3,7 @@
3
3
  require_relative '../core/utils/time'
4
4
  require_relative '../ruby_version'
5
5
  require_relative 'fatal_exceptions'
6
+ require_relative 'capture_expression_evaluator'
6
7
 
7
8
  # rubocop:disable Lint/AssignmentInCondition
8
9
  # rubocop:disable Style/AndOr
@@ -84,6 +85,12 @@ module Datadog
84
85
  attr_reader :telemetry
85
86
  attr_reader :code_tracker
86
87
 
88
+ def capture_expression_evaluator
89
+ @capture_expression_evaluator ||= CaptureExpressionEvaluator.new(
90
+ settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
91
+ )
92
+ end
93
+
87
94
  # This is a substitute for Thread::Backtrace::Location
88
95
  # which does not have a public constructor.
89
96
  # Used for the fabricated stack frame for the method itself
@@ -520,8 +527,27 @@ module Datadog
520
527
  # they need to be serialized prior to method invocation.
521
528
  serialized_entry_args = if probe.capture_snapshot?
522
529
  serializer.serialize_args(args, kwargs, target_self,
523
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
524
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
530
+ **probe.snapshot_serializer_limits(settings))
531
+ end
532
+
533
+ entry_capture_expressions = nil
534
+ entry_capture_evaluation_errors = nil
535
+ if probe.capture_entry_expressions?
536
+ begin
537
+ entry_context = Context.new(
538
+ probe: probe, settings: settings, serializer: serializer,
539
+ target_self: target_self,
540
+ locals: serializer.combine_args(args, kwargs, target_self),
541
+ )
542
+ entry_capture_expressions, entry_capture_evaluation_errors =
543
+ capture_expression_evaluator.evaluate(probe, entry_context)
544
+ rescue Exception => exc # standard:disable Lint/RescueException
545
+ Datadog::DI.reraise_if_fatal(exc)
546
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
547
+
548
+ logger.debug { "di: error evaluating entry-time capture expressions: #{exc.class}: #{exc.message}" }
549
+ telemetry&.report(exc, description: "Error evaluating entry-time capture expressions")
550
+ end
525
551
  end
526
552
  # We intentionally do not use Core::Utils::Time.get_time
527
553
  # here because the time provider may be overridden by the
@@ -584,9 +610,14 @@ module Datadog
584
610
  caller_locs = method_frame + (caller_locations(2) || [])
585
611
  # TODO capture arguments at exit
586
612
 
587
- context = Context.new(locals: nil, target_self: target_self,
613
+ capture_expression_locals = if probe.capture_expressions_only?
614
+ serializer.combine_args(args, kwargs, target_self)
615
+ end
616
+ context = Context.new(locals: capture_expression_locals, target_self: target_self,
588
617
  probe: probe, settings: settings, serializer: serializer,
589
618
  serialized_entry_args: serialized_entry_args,
619
+ entry_capture_expressions: entry_capture_expressions,
620
+ entry_capture_evaluation_errors: entry_capture_evaluation_errors,
590
621
  caller_locations: caller_locs,
591
622
  return_value: rv, duration: duration, exception: exc,)
592
623
 
@@ -34,11 +34,17 @@ module Datadog
34
34
  class Probe
35
35
  KNOWN_TYPES = %i[log].freeze
36
36
 
37
+ EVALUATE_AT_VALUES = %i[entry exit].freeze
38
+
37
39
  def initialize(id:, type:,
38
40
  file: nil, line_no: nil, type_name: nil, method_name: nil,
39
41
  template: nil, template_segments: nil,
40
42
  capture_snapshot: false, max_capture_depth: nil,
41
- max_capture_attribute_count: nil, condition: nil,
43
+ max_capture_attribute_count: nil,
44
+ max_capture_collection_size: nil, max_capture_string_length: nil,
45
+ capture_expressions: [],
46
+ evaluate_at: nil,
47
+ condition: nil,
42
48
  rate_limit: nil)
43
49
  # Perform some sanity checks here to detect unexpected attribute
44
50
  # combinations, in order to not do them in subsequent code.
@@ -81,9 +87,17 @@ module Datadog
81
87
  @capture_snapshot = !!capture_snapshot
82
88
  @max_capture_depth = max_capture_depth
83
89
  @max_capture_attribute_count = max_capture_attribute_count
90
+ @max_capture_collection_size = max_capture_collection_size
91
+ @max_capture_string_length = max_capture_string_length
92
+ @capture_expressions = capture_expressions || []
93
+ evaluate_at = :exit if evaluate_at.nil?
94
+ unless EVALUATE_AT_VALUES.include?(evaluate_at)
95
+ raise ArgumentError, "Unknown evaluate_at value: #{evaluate_at.inspect} (expected one of #{EVALUATE_AT_VALUES.inspect})"
96
+ end
97
+ @evaluate_at = evaluate_at
84
98
  @condition = condition
85
99
 
86
- @rate_limit = rate_limit || (@capture_snapshot ? 1 : 5000)
100
+ @rate_limit = rate_limit || ((@capture_snapshot || !@capture_expressions.empty?) ? 1 : 5000)
87
101
  @rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)
88
102
 
89
103
  # At most one report per second.
@@ -120,6 +134,14 @@ module Datadog
120
134
  # the global default will be used.
121
135
  attr_reader :max_capture_attribute_count
122
136
 
137
+ attr_reader :max_capture_collection_size
138
+
139
+ attr_reader :max_capture_string_length
140
+
141
+ attr_reader :capture_expressions
142
+
143
+ attr_reader :evaluate_at
144
+
123
145
  # Rate limit in effect, in invocations per second. Always present.
124
146
  attr_reader :rate_limit
125
147
 
@@ -140,6 +162,29 @@ module Datadog
140
162
  @capture_snapshot
141
163
  end
142
164
 
165
+ def capture_expressions?
166
+ !@capture_expressions.empty?
167
+ end
168
+
169
+ def evaluate_at_entry?
170
+ evaluate_at == :entry
171
+ end
172
+
173
+ # Capture-expression mode: the probe captures expressions but not a full
174
+ # snapshot. Snapshot capture serializes its own values, so expression
175
+ # capture only runs when a snapshot is not being taken.
176
+ def capture_expressions_only?
177
+ capture_expressions? && !capture_snapshot?
178
+ end
179
+
180
+ def capture_entry_expressions?
181
+ capture_expressions_only? && evaluate_at_entry?
182
+ end
183
+
184
+ def snapshot_serializer_limits(settings)
185
+ CaptureLimits.resolve(expr_limits: nil, probe: self, settings: settings)
186
+ end
187
+
143
188
  # Returns whether the probe is a line probe.
144
189
  #
145
190
  # Method probes may still specify a file name (to aid in locating the
@@ -3,6 +3,8 @@
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
5
  require_relative "probe"
6
+ require_relative "capture_expression"
7
+ require_relative "capture_limits"
6
8
  require_relative 'el'
7
9
 
8
10
  module Datadog
@@ -27,7 +29,15 @@ module Datadog
27
29
 
28
30
  module_function
29
31
 
30
- def build_from_remote_config(config)
32
+ CAPTURE_EXPRESSION_NAME_PATTERN = /\A[a-zA-Z0-9_?]+\z/
33
+
34
+ EVALUATE_AT_STRINGS = {
35
+ "ENTRY" => :entry,
36
+ "EXIT" => :exit,
37
+ "DEFAULT" => :exit,
38
+ }.freeze
39
+
40
+ def build_from_remote_config(config, logger:)
31
41
  # The validations here are not yet comprehensive.
32
42
  type = config.fetch('type')
33
43
  type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
@@ -38,6 +48,13 @@ module Datadog
38
48
  compiled = EL::Compiler.new.compile(cond_spec['json'])
39
49
  EL::Expression.new(cond_spec['dsl'], compiled)
40
50
  end
51
+ capture_expressions = build_capture_expressions(config['captureExpressions'])
52
+ capture_expressions = dedup_capture_expressions(capture_expressions, config["id"], logger)
53
+ if !!config["captureSnapshot"] && !capture_expressions.empty?
54
+ logger.debug do
55
+ "di: probe #{config["id"]}: captureSnapshot=true wins over captureExpressions (n=#{capture_expressions.size})"
56
+ end
57
+ end
41
58
  Probe.new(
42
59
  id: config.fetch("id"),
43
60
  type: type_symbol,
@@ -54,6 +71,10 @@ module Datadog
54
71
  capture_snapshot: !!config["captureSnapshot"],
55
72
  max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
56
73
  max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
74
+ max_capture_collection_size: config["capture"]&.[]("maxCollectionSize"),
75
+ max_capture_string_length: config["capture"]&.[]("maxLength"),
76
+ capture_expressions: capture_expressions,
77
+ evaluate_at: parse_evaluate_at(config["evaluateAt"], config["id"], logger),
57
78
  rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
58
79
  condition: cond,
59
80
  )
@@ -61,6 +82,67 @@ module Datadog
61
82
  raise ArgumentError, "Malformed remote configuration entry for probe: #{exc.class}: #{exc.message}: #{config}"
62
83
  end
63
84
 
85
+ def build_capture_expressions(raw)
86
+ return [] if raw.nil?
87
+ unless Array === raw
88
+ raise ArgumentError, "captureExpressions must be an array, got: #{raw.class}"
89
+ end
90
+ return [] if raw.empty?
91
+ raw.map do |entry|
92
+ unless Hash === entry
93
+ raise ArgumentError, "captureExpressions entry must be a hash, got: #{entry.class}"
94
+ end
95
+ name = entry['name']
96
+ unless String === name && CAPTURE_EXPRESSION_NAME_PATTERN.match?(name)
97
+ raise ArgumentError, "captureExpressions entry name missing or invalid (must match #{CAPTURE_EXPRESSION_NAME_PATTERN.inspect}): #{name.inspect}"
98
+ end
99
+ expr_spec = entry['expr']
100
+ unless Hash === expr_spec && expr_spec['dsl'] && expr_spec['json']
101
+ raise ArgumentError, "captureExpressions entry #{name}: missing or malformed expr"
102
+ end
103
+ compiled = EL::Compiler.new.compile(expr_spec['json'])
104
+ expr = EL::Expression.new(expr_spec['dsl'], compiled)
105
+ limits = build_capture_limits(entry['capture'])
106
+ CaptureExpression.new(name: name, expr: expr, limits: limits)
107
+ end
108
+ end
109
+
110
+ def dedup_capture_expressions(capture_expressions, probe_id, logger)
111
+ return capture_expressions if capture_expressions.size < 2
112
+ by_name = {}
113
+ capture_expressions.each do |capture_expression|
114
+ by_name[capture_expression.name] = capture_expression
115
+ end
116
+ return capture_expressions if by_name.size == capture_expressions.size
117
+ logger.debug do
118
+ "di: probe #{probe_id}: collapsed duplicate captureExpressions names, kept last per name (#{capture_expressions.size} -> #{by_name.size})"
119
+ end
120
+ by_name.values
121
+ end
122
+
123
+ def build_capture_limits(raw)
124
+ return nil if raw.nil?
125
+ unless Hash === raw
126
+ raise ArgumentError, "capture-expression entry capture must be a hash, got: #{raw.class}"
127
+ end
128
+ CaptureLimits.new(
129
+ max_reference_depth: raw['maxReferenceDepth'],
130
+ max_collection_size: raw['maxCollectionSize'],
131
+ max_length: raw['maxLength'],
132
+ max_field_count: raw['maxFieldCount'],
133
+ )
134
+ end
135
+
136
+ def parse_evaluate_at(raw, probe_id, logger)
137
+ return :exit if raw.nil?
138
+ EVALUATE_AT_STRINGS[raw] || begin
139
+ logger.debug do
140
+ "di: probe #{probe_id}: unrecognized evaluateAt value #{raw.inspect}, defaulting to :exit"
141
+ end
142
+ :exit
143
+ end
144
+ end
145
+
64
146
  def build_template_segments(segments)
65
147
  segments&.map do |segment|
66
148
  if Hash === segment
@@ -3,6 +3,7 @@
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
5
  require_relative 'fatal_exceptions'
6
+ require_relative 'capture_expression_evaluator'
6
7
 
7
8
  module Datadog
8
9
  module DI
@@ -10,14 +11,26 @@ module Datadog
10
11
  #
11
12
  # @api private
12
13
  class ProbeNotificationBuilder
13
- def initialize(settings, serializer)
14
+ def initialize(settings, serializer, logger, telemetry: nil)
14
15
  @settings = settings
15
16
  @serializer = serializer
17
+ @logger = logger
18
+ @telemetry = telemetry
19
+ @capture_expression_evaluator = CaptureExpressionEvaluator.new(
20
+ settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
21
+ )
16
22
  end
17
23
 
18
24
  attr_reader :settings
25
+
19
26
  attr_reader :serializer
20
27
 
28
+ attr_reader :logger
29
+
30
+ attr_reader :telemetry
31
+
32
+ attr_reader :capture_expression_evaluator
33
+
21
34
  def build_received(probe)
22
35
  build_status(probe,
23
36
  message: "Probe #{probe.id} has been received correctly",
@@ -67,13 +80,13 @@ module Datadog
67
80
 
68
81
  # TODO also verify that non-capturing probe does not pass
69
82
  # snapshot or vars/args into this method
83
+ capture_expression_evaluation_errors = []
70
84
  captures = if probe.capture_snapshot?
85
+ snapshot_limits = probe.snapshot_serializer_limits(settings)
71
86
  if probe.method?
72
87
  return_arguments = {
73
- "@return": serializer.serialize_value(context.return_value,
74
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
75
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count),
76
- self: serializer.serialize_value(context.target_self),
88
+ "@return": serializer.serialize_value(context.return_value, **snapshot_limits),
89
+ self: serializer.serialize_value(context.target_self, **snapshot_limits),
77
90
  }
78
91
  {
79
92
  entry: {
@@ -89,11 +102,38 @@ module Datadog
89
102
  lines: (locals = context.serialized_locals) && {
90
103
  probe.line_no => {
91
104
  locals: locals,
92
- arguments: {self: serializer.serialize_value(context.target_self)},
105
+ arguments: {self: serializer.serialize_value(context.target_self, **snapshot_limits)},
93
106
  },
94
107
  },
95
108
  }
96
109
  end
110
+ elsif probe.capture_expressions?
111
+ if probe.method?
112
+ if probe.evaluate_at_entry?
113
+ captured_block = context.entry_capture_expressions || {}
114
+ capture_expression_evaluation_errors = context.entry_capture_evaluation_errors || []
115
+ {
116
+ entry: {captureExpressions: captured_block},
117
+ }
118
+ else
119
+ captured_block, capture_expression_evaluation_errors =
120
+ capture_expression_evaluator.evaluate(probe, context)
121
+ {
122
+ return: {
123
+ captureExpressions: captured_block,
124
+ throwable: context.exception ? serialize_throwable(context.exception) : nil,
125
+ },
126
+ }
127
+ end
128
+ elsif probe.line?
129
+ captured_block, capture_expression_evaluation_errors =
130
+ capture_expression_evaluator.evaluate(probe, context)
131
+ {
132
+ lines: {
133
+ probe.line_no => {captureExpressions: captured_block},
134
+ },
135
+ }
136
+ end
97
137
  end
98
138
 
99
139
  message = nil
@@ -101,6 +141,7 @@ module Datadog
101
141
  if segments = probe.template_segments
102
142
  message, evaluation_errors = evaluate_template(segments, context)
103
143
  end
144
+ evaluation_errors.concat(capture_expression_evaluation_errors)
104
145
  build_snapshot_base(context,
105
146
  evaluation_errors: evaluation_errors, message: message,
106
147
  captures: captures)
@@ -305,7 +346,7 @@ module Datadog
305
346
  # except there is currently no consensus on said heuristics.
306
347
  # .NET always sends ld, other languages send nothing at the moment.
307
348
  # Don't send anything for the time being.
308
- #product: 'di/ld',
349
+ # product: 'di/ld',
309
350
  snapshot: {
310
351
  id: SecureRandom.uuid,
311
352
  timestamp: timestamp,
@@ -228,7 +228,7 @@ module Datadog
228
228
  # we need to note it as being current so that we do not
229
229
  # try to remove instrumentation that is still supposed to be
230
230
  # active.
231
- #current_probe_ids[probe_spec.fetch('id')] = true
231
+ # current_probe_ids[probe_spec.fetch('id')] = true
232
232
  rescue Exception => exc # standard:disable Lint/RescueException
233
233
  Datadog::DI.reraise_if_fatal(exc)
234
234
  raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions