datadog 2.21.0 → 2.22.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 (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +48 -1
  3. data/ext/LIBDATADOG_DEVELOPMENT.md +60 -0
  4. data/ext/datadog_profiling_native_extension/collectors_discrete_dynamic_sampler.c +1 -1
  5. data/ext/libdatadog_api/ddsketch.c +106 -0
  6. data/ext/libdatadog_api/init.c +3 -0
  7. data/ext/libdatadog_api/library_config.c +35 -27
  8. data/ext/libdatadog_api/process_discovery.c +19 -13
  9. data/ext/libdatadog_extconf_helpers.rb +1 -1
  10. data/lib/datadog/appsec/api_security/endpoint_collection/grape_route_serializer.rb +26 -0
  11. data/lib/datadog/appsec/api_security/endpoint_collection/rails_collector.rb +59 -0
  12. data/lib/datadog/appsec/api_security/endpoint_collection/rails_route_serializer.rb +29 -0
  13. data/lib/datadog/appsec/api_security/endpoint_collection/sinatra_route_serializer.rb +26 -0
  14. data/lib/datadog/appsec/api_security/endpoint_collection.rb +10 -0
  15. data/lib/datadog/appsec/assets/waf_rules/README.md +30 -36
  16. data/lib/datadog/appsec/assets/waf_rules/recommended.json +359 -4
  17. data/lib/datadog/appsec/assets/waf_rules/strict.json +43 -2
  18. data/lib/datadog/appsec/compressed_json.rb +1 -1
  19. data/lib/datadog/appsec/configuration/settings.rb +9 -0
  20. data/lib/datadog/appsec/contrib/active_record/instrumentation.rb +3 -1
  21. data/lib/datadog/appsec/contrib/excon/ssrf_detection_middleware.rb +3 -2
  22. data/lib/datadog/appsec/contrib/faraday/ssrf_detection_middleware.rb +3 -1
  23. data/lib/datadog/appsec/contrib/graphql/gateway/watcher.rb +3 -1
  24. data/lib/datadog/appsec/contrib/rack/gateway/watcher.rb +9 -4
  25. data/lib/datadog/appsec/contrib/rack/request_middleware.rb +5 -1
  26. data/lib/datadog/appsec/contrib/rails/gateway/watcher.rb +7 -2
  27. data/lib/datadog/appsec/contrib/rails/patcher.rb +30 -0
  28. data/lib/datadog/appsec/contrib/rest_client/request_ssrf_detection_patch.rb +3 -1
  29. data/lib/datadog/appsec/contrib/sinatra/gateway/watcher.rb +10 -4
  30. data/lib/datadog/appsec/event.rb +12 -14
  31. data/lib/datadog/appsec/metrics/collector.rb +19 -3
  32. data/lib/datadog/appsec/metrics/telemetry_exporter.rb +2 -1
  33. data/lib/datadog/appsec/monitor/gateway/watcher.rb +4 -4
  34. data/lib/datadog/appsec/remote.rb +25 -13
  35. data/lib/datadog/appsec/security_engine/result.rb +28 -9
  36. data/lib/datadog/appsec/security_engine/runner.rb +17 -7
  37. data/lib/datadog/appsec/security_event.rb +5 -7
  38. data/lib/datadog/core/configuration/components.rb +14 -6
  39. data/lib/datadog/core/configuration/stable_config.rb +10 -0
  40. data/lib/datadog/core/configuration/supported_configurations.rb +2 -0
  41. data/lib/datadog/core/configuration.rb +1 -1
  42. data/lib/datadog/core/ddsketch.rb +21 -0
  43. data/lib/datadog/core/environment/yjit.rb +2 -1
  44. data/lib/datadog/core/pin.rb +4 -8
  45. data/lib/datadog/core/process_discovery.rb +4 -2
  46. data/lib/datadog/core/remote/component.rb +4 -6
  47. data/lib/datadog/core/telemetry/component.rb +11 -0
  48. data/lib/datadog/core/telemetry/emitter.rb +6 -6
  49. data/lib/datadog/core/telemetry/event/app_endpoints_loaded.rb +30 -0
  50. data/lib/datadog/core/telemetry/event.rb +1 -0
  51. data/lib/datadog/core/transport/response.rb +4 -1
  52. data/lib/datadog/core/utils/network.rb +19 -0
  53. data/lib/datadog/di/boot.rb +1 -0
  54. data/lib/datadog/di/component.rb +14 -0
  55. data/lib/datadog/di/context.rb +70 -0
  56. data/lib/datadog/di/el/compiler.rb +164 -0
  57. data/lib/datadog/di/el/evaluator.rb +159 -0
  58. data/lib/datadog/di/el/expression.rb +42 -0
  59. data/lib/datadog/di/el.rb +5 -0
  60. data/lib/datadog/di/error.rb +25 -0
  61. data/lib/datadog/di/instrumenter.rb +101 -32
  62. data/lib/datadog/di/probe.rb +35 -15
  63. data/lib/datadog/di/probe_builder.rb +39 -1
  64. data/lib/datadog/di/probe_manager.rb +3 -2
  65. data/lib/datadog/di/probe_notification_builder.rb +50 -51
  66. data/lib/datadog/di/serializer.rb +151 -7
  67. data/lib/datadog/tracing/component.rb +6 -17
  68. data/lib/datadog/tracing/configuration/dynamic.rb +2 -2
  69. data/lib/datadog/tracing/configuration/settings.rb +3 -3
  70. data/lib/datadog/tracing/contrib/component.rb +2 -2
  71. data/lib/datadog/tracing/contrib/graphql/configuration/settings.rb +7 -0
  72. data/lib/datadog/tracing/contrib/graphql/ext.rb +1 -0
  73. data/lib/datadog/tracing/contrib/graphql/unified_trace.rb +53 -28
  74. data/lib/datadog/tracing/metadata/ext.rb +8 -0
  75. data/lib/datadog/version.rb +1 -1
  76. metadata +22 -9
  77. data/ext/libdatadog_api/macos_development.md +0 -26
@@ -0,0 +1,159 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Evaluator for expression language.
7
+ #
8
+ # @api private
9
+ class Evaluator
10
+ def ref(var)
11
+ @context.fetch(var)
12
+ end
13
+
14
+ def iref(var)
15
+ @context.fetch_ivar(var)
16
+ end
17
+
18
+ def len(var, var_name)
19
+ case var
20
+ when Array, String
21
+ var.length
22
+ else
23
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for length: #{var.class}: #{var_name}"
24
+ end
25
+ end
26
+
27
+ def is_empty(var, var_name)
28
+ case var
29
+ when nil, Numeric
30
+ false
31
+ when Array, String
32
+ var.empty?
33
+ else
34
+ raise DI::Error::ExpressionEvaluationError, "Unsupported type for isEmpty: #{var.class}: #{var_name}"
35
+ end
36
+ end
37
+
38
+ def is_undefined(var, var_name)
39
+ var.nil?
40
+ end
41
+
42
+ def contains(haystack, needle)
43
+ if String === haystack && String === needle or # standard:disable Style/AndOr
44
+ Array === haystack
45
+ haystack.include?(needle)
46
+ else
47
+ raise DI::Error::ExpressionEvaluationError, "Invalid arguments for contains: #{haystack}, #{needle}"
48
+ end
49
+ end
50
+
51
+ def matches(haystack, needle)
52
+ re = Regexp.compile(needle)
53
+ !!(haystack =~ re)
54
+ end
55
+
56
+ def getmember(object, field)
57
+ object.instance_variable_get("@#{field}")
58
+ end
59
+
60
+ def index(array_or_hash, index_or_key)
61
+ case array_or_hash
62
+ when Array
63
+ case index_or_key
64
+ when Integer
65
+ array_or_hash[index_or_key]
66
+ else
67
+ raise DI::Error::ExpressionEvaluationError, "Invalid index value: #{index_or_key}"
68
+ end
69
+ when Hash
70
+ array_or_hash[index_or_key]
71
+ else
72
+ raise DI::Error::ExpressionEvaluationError, "Invalid argument for index: #{array_or_hash}"
73
+ end
74
+ end
75
+
76
+ def substring(object, from, to)
77
+ unless String === object
78
+ raise DI::Error::ExpressionEvaluationError, "Invalid type for substring: #{object}"
79
+ end
80
+ object[from...to]
81
+ end
82
+
83
+ def starts_with(haystack, needle)
84
+ # To guard against running arbitrary customer code, check that
85
+ # the haystack is a string. This does not help if customer
86
+ # overrode String#start_with? but at least it's better than nothing.
87
+ String === haystack && haystack.start_with?(needle)
88
+ end
89
+
90
+ def ends_with(haystack, needle)
91
+ String === haystack && haystack.end_with?(needle)
92
+ end
93
+
94
+ def all(collection, &block)
95
+ case collection
96
+ when Array
97
+ collection.all? do |item|
98
+ block.call(item)
99
+ end
100
+ when Hash
101
+ # For hashes, the expression language has both @it and
102
+ # @key/@value. Manufacture @it from the key and value.
103
+ collection.all? do |key, value|
104
+ block.call([key, value], key, value)
105
+ end
106
+ else
107
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for all: #{collection.class}"
108
+ end
109
+ end
110
+
111
+ def any(collection, &block)
112
+ case collection
113
+ when Array
114
+ collection.any? do |item|
115
+ block.call(item)
116
+ end
117
+ when Hash
118
+ collection.any? do |key, value|
119
+ # For hashes, the expression language has both @it and
120
+ # @key/@value. Manufacture @it from the key and value.
121
+ block.call([key, value], key, value)
122
+ end
123
+ else
124
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for any: #{collection.class}"
125
+ end
126
+ end
127
+
128
+ def filter(collection, &block)
129
+ case collection
130
+ when Array
131
+ collection.select do |item|
132
+ block.call(item)
133
+ end
134
+ when Hash
135
+ collection.select do |key, value|
136
+ block.call([key, value], key, value)
137
+ end.to_h
138
+ else
139
+ raise DI::Error::ExpressionEvaluationError, "Bad collection type for filter: #{collection.class}"
140
+ end
141
+ end
142
+
143
+ def instanceof(object, cls_name)
144
+ cls = object.class
145
+ loop do
146
+ if cls.name == cls_name
147
+ return true
148
+ end
149
+ if supercls = cls.superclass # standard:disable Lint/AssignmentInCondition
150
+ cls = supercls
151
+ else
152
+ return false
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ module EL
6
+ # Represents an Expression Language expression.
7
+ #
8
+ # @api private
9
+ class Expression
10
+ def initialize(dsl_expr, compiled_expr)
11
+ unless String === compiled_expr
12
+ raise ArgumentError, "compiled_expr must be a string"
13
+ end
14
+
15
+ @dsl_expr = dsl_expr
16
+
17
+ cls = Class.new(Evaluator)
18
+ cls.class_exec do
19
+ eval(<<-RUBY, Object.new.send(:binding), __FILE__, __LINE__ + 1) # standard:disable Security/Eval
20
+ def evaluate(context)
21
+ @context = context
22
+ #{compiled_expr}
23
+ end
24
+ RUBY
25
+ end
26
+ @evaluator = cls.new
27
+ end
28
+
29
+ attr_reader :dsl_expr
30
+ attr_reader :evaluator
31
+
32
+ def evaluate(context)
33
+ @evaluator.evaluate(context)
34
+ end
35
+
36
+ def satisfied?(context)
37
+ !!evaluate(context)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'el/expression'
4
+ require_relative 'el/compiler'
5
+ require_relative 'el/evaluator'
@@ -48,6 +48,31 @@ module Datadog
48
48
  # and the user will need to make their suffix more precise.
49
49
  class MultiplePathsMatch < Error
50
50
  end
51
+
52
+ # Base class for exceptions arising during expression language AST
53
+ # compilation into Ruby code.
54
+ #
55
+ # Expression language does not specify behavior in all cases,
56
+ # leaving some choices to the language implementation in the tracers.
57
+ # It is therefore possible that some technically valid expressions are
58
+ # prohibited by our implementation.
59
+ #
60
+ # It is also possible that the sanitizers/validators prohibit some
61
+ # esoteric constructs that are technically valid in Ruby,
62
+ # for example if instance variable name rules are relaxed to allow
63
+ # arbitrary characters in them as permitted in method names.
64
+ class InvalidExpression < Error
65
+ end
66
+
67
+ # Variable name with invalid characters in an expression language
68
+ # expression.
69
+ class BadVariableName < InvalidExpression
70
+ end
71
+
72
+ # Base class for exceptions arising when evaluating expression language
73
+ # expressions.
74
+ class ExpressionEvaluationError < Error
75
+ end
51
76
  end
52
77
  end
53
78
  end
@@ -3,6 +3,7 @@
3
3
  require_relative '../core/utils/time'
4
4
 
5
5
  # rubocop:disable Lint/AssignmentInCondition
6
+ # rubocop:disable Style/AndOr
6
7
 
7
8
  module Datadog
8
9
  module DI
@@ -118,7 +119,26 @@ module Datadog
118
119
 
119
120
  mod = Module.new do
120
121
  define_method(method_name) do |*args, **kwargs, &target_block| # steep:ignore
121
- if rate_limiter.nil? || rate_limiter.allow?
122
+ continue = true
123
+ if condition = probe.condition
124
+ begin
125
+ # This context will be recreated later, unlike for line probes.
126
+ context = Context.new(
127
+ locals: serializer.combine_args(args, kwargs, self),
128
+ target_self: self,
129
+ probe: probe, settings: settings, serializer: serializer,
130
+ caller_locations: caller_locations,
131
+ )
132
+ continue = condition.satisfied?(context)
133
+ rescue
134
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
135
+
136
+ # TODO log / report via telemetry?
137
+ continue = false
138
+ end
139
+ end
140
+
141
+ if continue and rate_limiter.nil? || rate_limiter.allow?
122
142
  # Arguments may be mutated by the method, therefore
123
143
  # they need to be serialized prior to method invocation.
124
144
  serialized_entry_args = if probe.capture_snapshot?
@@ -127,19 +147,29 @@ module Datadog
127
147
  attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count)
128
148
  end
129
149
  start_time = Core::Utils::Time.get_time
130
- # Under Ruby 2.6 we cannot just call super(*args, **kwargs)
131
- # for methods defined via method_missing.
132
- rv = if args.any?
133
- if kwargs.any?
134
- super(*args, **kwargs, &target_block)
150
+
151
+ rv = nil
152
+ begin
153
+ # Under Ruby 2.6 we cannot just call super(*args, **kwargs)
154
+ # for methods defined via method_missing.
155
+ rv = if args.any?
156
+ if kwargs.any?
157
+ super(*args, **kwargs, &target_block)
158
+ else
159
+ super(*args, &target_block)
160
+ end
161
+ elsif kwargs.any?
162
+ super(**kwargs, &target_block)
135
163
  else
136
- super(*args, &target_block)
164
+ super(&target_block)
137
165
  end
138
- elsif kwargs.any?
139
- super(**kwargs, &target_block)
140
- else
141
- super(&target_block)
166
+ rescue NoMemoryError, Interrupt, SystemExit
167
+ raise
168
+ rescue Exception => exc # standard:disable Lint/RescueException
169
+ # We will raise the exception captured here later, after
170
+ # the instrumentation callback runs.
142
171
  end
172
+
143
173
  duration = Core::Utils::Time.get_time - start_time
144
174
  # The method itself is not part of the stack trace because
145
175
  # we are getting the stack trace from outside of the method.
@@ -158,12 +188,20 @@ module Datadog
158
188
  end
159
189
  caller_locs = method_frame + caller_locations # steep:ignore
160
190
  # TODO capture arguments at exit
191
+
192
+ context = Context.new(locals: nil, target_self: self,
193
+ probe: probe, settings: settings, serializer: serializer,
194
+ serialized_entry_args: serialized_entry_args,
195
+ caller_locations: caller_locs,
196
+ return_value: rv, duration: duration, exception: exc,)
197
+
161
198
  # & is to stop steep complaints, block is always present here.
162
- block&.call(probe: probe, rv: rv,
163
- duration: duration, caller_locations: caller_locs,
164
- target_self: self,
165
- serialized_entry_args: serialized_entry_args)
166
- rv
199
+ block&.call(context)
200
+ if exc
201
+ raise exc
202
+ else
203
+ rv
204
+ end
167
205
  else
168
206
  # stop standard from trying to mess up my code
169
207
  _ = 42
@@ -307,27 +345,57 @@ module Datadog
307
345
  # are invoked for *each* line of Ruby executed.
308
346
  # TODO find out exactly when the path in trace point is relative.
309
347
  # Looks like this is the case when line trace point is not targeted?
310
- if iseq || tp.lineno == probe.line_no && (
348
+ continue = iseq || tp.lineno == probe.line_no && (
311
349
  probe.file == tp.path || probe.file_matches?(tp.path)
312
350
  )
313
- if rate_limiter.nil? || rate_limiter.allow?
314
- serialized_locals = if probe.capture_snapshot?
315
- serializer.serialize_vars(Instrumenter.get_local_variables(tp),
316
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
317
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
318
- end
319
- if probe.capture_snapshot?
320
- serializer.serialize_value(tp.self,
321
- depth: probe.max_capture_depth || settings.dynamic_instrumentation.max_capture_depth,
322
- attribute_count: probe.max_capture_attribute_count || settings.dynamic_instrumentation.max_capture_attribute_count,)
323
- end
324
- # & is to stop steep complaints, block is always present here.
325
- block&.call(probe: probe,
326
- serialized_locals: serialized_locals,
351
+
352
+ # We set the trace point on :return to be able to instrument
353
+ # 'end' lines. This also causes the trace point to be invoked on
354
+ # non-'end' lines when a line raises an exception, since the
355
+ # exception causes the method to stop executing and stack unwends.
356
+ # We do not want two invocations of the trace point.
357
+ # Therefore, if a trace point is invoked with a :line event,
358
+ # mark it as such and ignore subsequent :return events.
359
+ continue &&= if probe.executed_on_line?
360
+ tp.event == :line
361
+ else
362
+ if tp.event == :line
363
+ probe.executed_on_line!
364
+ end
365
+ true
366
+ end
367
+
368
+ if continue
369
+ if condition = probe.condition
370
+ context = Context.new(
371
+ locals: Instrumenter.get_local_variables(tp),
327
372
  target_self: tp.self,
328
- path: tp.path, caller_locations: caller_locations)
373
+ probe: probe, settings: settings, serializer: serializer,
374
+ path: tp.path,
375
+ caller_locations: caller_locations,
376
+ )
377
+ continue = condition.satisfied?(context)
329
378
  end
330
379
  end
380
+
381
+ continue &&= rate_limiter.nil? || rate_limiter.allow? # standard:disable Style/AndOr
382
+
383
+ if continue
384
+ # The context creation is relatively expensive and we don't
385
+ # want to run it if the callback won't be executed due to the
386
+ # rate limit.
387
+ # Thus the copy-paste of the creation call here.
388
+ context ||= Context.new(
389
+ locals: Instrumenter.get_local_variables(tp),
390
+ target_self: tp.self,
391
+ probe: probe, settings: settings, serializer: serializer,
392
+ path: tp.path,
393
+ caller_locations: caller_locations,
394
+ )
395
+
396
+ # & is to stop steep complaints, block is always present here.
397
+ block&.call(context)
398
+ end
331
399
  rescue => exc
332
400
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
333
401
  logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc}" }
@@ -449,3 +517,4 @@ module Datadog
449
517
  end
450
518
 
451
519
  # rubocop:enable Lint/AssignmentInCondition
520
+ # rubocop:enable Style/AndOr
@@ -17,7 +17,7 @@ module Datadog
17
17
  # and remote config code must be prepared to deal with exceptions
18
18
  # raised by Probe constructor in particular. Therefore, Probe constructor
19
19
  # will raise an exception if it determines that there is not enough
20
- # information (or confilcting information) in the arguments to create a
20
+ # information (or conflicting information) in the arguments to create a
21
21
  # functional probe, and upstream code is tasked with not spamming logs
22
22
  # with notifications of such errors (and potentially limiting the
23
23
  # attempts to construct probe from a given payload).
@@ -36,8 +36,9 @@ module Datadog
36
36
 
37
37
  def initialize(id:, type:,
38
38
  file: nil, line_no: nil, type_name: nil, method_name: nil,
39
- template: nil, capture_snapshot: false, max_capture_depth: nil,
40
- max_capture_attribute_count: nil,
39
+ template: nil, template_segments: nil,
40
+ capture_snapshot: false, max_capture_depth: nil,
41
+ max_capture_attribute_count: nil, condition: nil,
41
42
  rate_limit: nil)
42
43
  # Perform some sanity checks here to detect unexpected attribute
43
44
  # combinations, in order to not do them in subsequent code.
@@ -45,9 +46,17 @@ module Datadog
45
46
  raise ArgumentError, "Unknown probe type: #{type}"
46
47
  end
47
48
 
48
- if line_no && method_name
49
- raise ArgumentError, "Probe contains both line number and method name: #{id}"
50
- end
49
+ # Probe should be inferred to be a line probe if the specification
50
+ # contains a line number. This how Java tracer works and Go tracer
51
+ # is implementing the same behavior, and Go will have all 3 fields
52
+ # (file path, line number and method name) for line probes.
53
+ # Do not raise if line number and method name both exist - instead
54
+ # treat the probe as a line probe.
55
+ #
56
+ # In the future we want to provide type name and method name to line
57
+ # probes, so that the library can verify that the instrumented line
58
+ # is in the method that the frontend showed to the user when the
59
+ # user created the probe.
51
60
 
52
61
  if line_no && !file
53
62
  raise ArgumentError, "Probe contains line number but not file: #{id}"
@@ -57,6 +66,10 @@ module Datadog
57
66
  raise ArgumentError, "Partial method probe definition: #{id}"
58
67
  end
59
68
 
69
+ if line_no.nil? && method_name.nil?
70
+ raise ArgumentError, "Unhandled probe type: neither method nor line probe: #{id}"
71
+ end
72
+
60
73
  @id = id
61
74
  @type = type
62
75
  @file = file
@@ -64,17 +77,11 @@ module Datadog
64
77
  @type_name = type_name
65
78
  @method_name = method_name
66
79
  @template = template
80
+ @template_segments = template_segments
67
81
  @capture_snapshot = !!capture_snapshot
68
82
  @max_capture_depth = max_capture_depth
69
83
  @max_capture_attribute_count = max_capture_attribute_count
70
-
71
- # These checks use instance methods that have more complex logic
72
- # than checking a single argument value. To avoid duplicating
73
- # the logic here, use the methods and perform these checks after
74
- # instance variable assignment.
75
- unless method? || line?
76
- raise ArgumentError, "Unhandled probe type: neither method nor line probe: #{id}"
77
- end
84
+ @condition = condition
78
85
 
79
86
  @rate_limit = rate_limit || (@capture_snapshot ? 1 : 5000)
80
87
  @rate_limiter = Datadog::Core::TokenBucket.new(@rate_limit)
@@ -89,6 +96,10 @@ module Datadog
89
96
  attr_reader :type_name
90
97
  attr_reader :method_name
91
98
  attr_reader :template
99
+ attr_reader :template_segments
100
+
101
+ # The compiled condition for the probe, as a String.
102
+ attr_reader :condition
92
103
 
93
104
  # Configured maximum capture depth. Can be nil in which case
94
105
  # the global default will be used.
@@ -122,7 +133,7 @@ module Datadog
122
133
 
123
134
  # Returns whether the probe is a method probe.
124
135
  def method?
125
- !!(type_name && method_name)
136
+ line_no.nil?
126
137
  end
127
138
 
128
139
  # Returns the line number associated with the probe, raising
@@ -186,6 +197,15 @@ module Datadog
186
197
  def emitting_notified?
187
198
  !!@emitting_notified
188
199
  end
200
+
201
+ def executed_on_line?
202
+ !!@executed_on_line
203
+ end
204
+
205
+ def executed_on_line!
206
+ # TODO lock?
207
+ @executed_on_line = true
208
+ end
189
209
  end
190
210
  end
191
211
  end
@@ -1,6 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # rubocop:disable Lint/AssignmentInCondition
4
+
3
5
  require_relative "probe"
6
+ require_relative 'el'
4
7
 
5
8
  module Datadog
6
9
  module DI
@@ -21,10 +24,19 @@ module Datadog
21
24
  'LOG_PROBE' => :log,
22
25
  }.freeze
23
26
 
24
- module_function def build_from_remote_config(config)
27
+ module_function
28
+
29
+ def build_from_remote_config(config)
25
30
  # The validations here are not yet comprehensive.
26
31
  type = config.fetch('type')
27
32
  type_symbol = PROBE_TYPES[type] or raise ArgumentError, "Unrecognized probe type: #{type}"
33
+ cond = if cond_spec = config['when']
34
+ unless cond_spec['dsl'] && cond_spec['json']
35
+ raise ArgumentError, "Malformed condition specification for probe: #{config}"
36
+ end
37
+ compiled = EL::Compiler.new.compile(cond_spec['json'])
38
+ EL::Expression.new(cond_spec['dsl'], compiled)
39
+ end
28
40
  Probe.new(
29
41
  id: config.fetch("id"),
30
42
  type: type_symbol,
@@ -34,15 +46,41 @@ module Datadog
34
46
  line_no: config["where"]&.[]("lines")&.compact&.map(&:to_i)&.first,
35
47
  type_name: config["where"]&.[]("typeName"),
36
48
  method_name: config["where"]&.[]("methodName"),
49
+ # We should not be using the template for anything - we instead
50
+ # use +segments+ - but keep the template for debugging.
37
51
  template: config["template"],
52
+ template_segments: build_template_segments(config['segments']),
38
53
  capture_snapshot: !!config["captureSnapshot"],
39
54
  max_capture_depth: config["capture"]&.[]("maxReferenceDepth"),
40
55
  max_capture_attribute_count: config["capture"]&.[]("maxFieldCount"),
41
56
  rate_limit: config["sampling"]&.[]("snapshotsPerSecond"),
57
+ condition: cond,
42
58
  )
43
59
  rescue KeyError => exc
44
60
  raise ArgumentError, "Malformed remote configuration entry for probe: #{exc.class}: #{exc}: #{config}"
45
61
  end
62
+
63
+ def build_template_segments(segments)
64
+ segments&.map do |segment|
65
+ if Hash === segment
66
+ if str = segment['str']
67
+ str
68
+ elsif ast = segment['json']
69
+ unless dsl = segment['dsl']
70
+ raise ArgumentError, "Missing dsl for json in segment: #{segment}"
71
+ end
72
+ compiled = EL::Compiler.new.compile(ast)
73
+ EL::Expression.new(dsl, compiled)
74
+ else
75
+ # TODO report to telemetry?
76
+ end
77
+ else
78
+ # TODO report to telemetry?
79
+ end
80
+ end&.compact
81
+ end
46
82
  end
47
83
  end
48
84
  end
85
+
86
+ # rubocop:enable Lint/AssignmentInCondition
@@ -229,7 +229,8 @@ module Datadog
229
229
  # This method is responsible for queueing probe status to be sent to the
230
230
  # backend (once per the probe's lifetime) and a snapshot corresponding
231
231
  # to the current invocation.
232
- def probe_executed_callback(probe:, **opts)
232
+ def probe_executed_callback(context)
233
+ probe = context.probe
233
234
  logger.trace { "di: executed #{probe.type} probe at #{probe.location} (#{probe.id})" }
234
235
  unless probe.emitting_notified?
235
236
  payload = probe_notification_builder.build_emitting(probe)
@@ -237,7 +238,7 @@ module Datadog
237
238
  probe.emitting_notified = true
238
239
  end
239
240
 
240
- payload = probe_notification_builder.build_executed(probe, **opts)
241
+ payload = probe_notification_builder.build_executed(context)
241
242
  probe_notifier_worker.add_snapshot(payload)
242
243
  end
243
244