datadog 2.45.0 → 2.46.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -2
  3. data/ext/datadog_profiling_native_extension/private_vm_api_access.c +4 -0
  4. data/ext/datadog_profiling_native_extension/private_vm_api_access.h +2 -0
  5. data/ext/datadog_profiling_native_extension/profiling.c +6 -0
  6. data/ext/libdatadog_api/di.c +30 -5
  7. data/ext/libdatadog_api/extconf.rb +2 -0
  8. data/ext/libdatadog_api/otel_thread_context.c +20 -14
  9. data/ext/libdatadog_api/process_discovery.c +15 -1
  10. data/ext/libdatadog_api/trace_exporter.c +105 -25
  11. data/ext/libdatadog_extconf_helpers.rb +1 -1
  12. data/lib/datadog/core/configuration/components.rb +29 -8
  13. data/lib/datadog/core/configuration/supported_configurations.rb +5 -2
  14. data/lib/datadog/core/process_discovery.rb +5 -1
  15. data/lib/datadog/core/remote/client/capabilities.rb +34 -29
  16. data/lib/datadog/core/remote/component.rb +28 -12
  17. data/lib/datadog/core/remote/dispatcher.rb +13 -3
  18. data/lib/datadog/di/boot.rb +1 -0
  19. data/lib/datadog/di/capture_expression_evaluator/result.rb +30 -0
  20. data/lib/datadog/di/capture_expression_evaluator.rb +39 -9
  21. data/lib/datadog/di/component.rb +38 -3
  22. data/lib/datadog/di/configuration.rb +53 -2
  23. data/lib/datadog/di/context.rb +7 -2
  24. data/lib/datadog/di/contrib/active_record.rb +8 -2
  25. data/lib/datadog/di/evaluation_failure.rb +28 -0
  26. data/lib/datadog/di/guardrails_telemetry/reason.rb +87 -0
  27. data/lib/datadog/di/guardrails_telemetry.rb +203 -0
  28. data/lib/datadog/di/instrumenter.rb +65 -15
  29. data/lib/datadog/di/logger.rb +30 -8
  30. data/lib/datadog/di/probe_manager.rb +48 -5
  31. data/lib/datadog/di/probe_notification_builder.rb +28 -26
  32. data/lib/datadog/di/probe_notifier_worker.rb +56 -6
  33. data/lib/datadog/di/serializer.rb +130 -54
  34. data/lib/datadog/di/snapshot_event.rb +38 -0
  35. data/lib/datadog/di/transport/http.rb +13 -3
  36. data/lib/datadog/di/transport/input.rb +32 -7
  37. data/lib/datadog/open_feature/activation.rb +268 -0
  38. data/lib/datadog/open_feature/component.rb +31 -15
  39. data/lib/datadog/open_feature/provider.rb +282 -5
  40. data/lib/datadog/open_feature.rb +31 -1
  41. data/lib/datadog/tracing/component.rb +14 -0
  42. data/lib/datadog/tracing/configuration/settings.rb +3 -1
  43. data/lib/datadog/tracing/otel_thread_context.rb +3 -5
  44. data/lib/datadog/tracing/transport/http/telemetry.rb +94 -0
  45. data/lib/datadog/tracing/transport/http/traces.rb +2 -0
  46. data/lib/datadog/tracing/transport/native/telemetry.rb +17 -0
  47. data/lib/datadog/tracing/transport/native.rb +16 -1
  48. data/lib/datadog/tracing/transport/telemetry.rb +62 -0
  49. data/lib/datadog/tracing/transport/traces.rb +82 -15
  50. data/lib/datadog/version.rb +1 -1
  51. metadata +16 -7
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ module DI
5
+ class GuardrailsTelemetry
6
+ # Canonical guardrails reason codes. Codes
7
+ # shared with the other Datadog tracers keep the exact shared
8
+ # spelling; codes with no shared counterpart use the Ruby tracer's
9
+ # own name for the same decision.
10
+ module Reason
11
+ # Skip reason emitted when a probe firing is rejected by the
12
+ # probe's own rate limiter.
13
+ RATE_LIMIT_PROBE = "rateLimitProbe"
14
+
15
+ # Skip reason emitted when a probe firing is rejected by the
16
+ # process-wide rate limiter.
17
+ RATE_LIMIT_GLOBAL = "rateLimitGlobal"
18
+
19
+ # Skip reason emitted when a capture expression evaluation exceeds
20
+ # the capture time budget.
21
+ EVALUATION_TIMEOUT = "evaluationTimeout"
22
+
23
+ # Skip reason named in the +di: debug log when a condition-evaluation
24
+ # failure notification is rejected by the per-probe limiter.
25
+ EVALUATION_ERROR_THROTTLED = "evaluationErrorThrottled"
26
+
27
+ # Skip reason naming the per-invocation DI time budget guardrail
28
+ # decision.
29
+ BUDGET_EXCEEDED_INVOCATION = "budgetExceededInvocation"
30
+
31
+ # Skip reason naming the process-wide DI time budget guardrail
32
+ # decision.
33
+ BUDGET_EXCEEDED_GLOBAL = "budgetExceededGlobal"
34
+
35
+ # Skip and drop reason emitted when an event is discarded at a
36
+ # full worker queue.
37
+ QUEUE_FULL = "queueFull"
38
+
39
+ # Skip and drop reason naming the queue high-watermark guardrail
40
+ # decision.
41
+ QUEUE_HIGH_WATERMARK = "queueHighWatermark"
42
+
43
+ # Drop reason emitted when a snapshot exceeds the serialized
44
+ # payload limit.
45
+ PAYLOAD_TOO_LARGE = "payloadTooLarge"
46
+
47
+ # Drop reason naming the batch byte-cap guardrail decision.
48
+ BATCH_BYTES_EXCEEDED = "batchBytesExceeded"
49
+
50
+ # Capture-incomplete reason emitted when a capture or
51
+ # serialization runtime exception ends the capture of a value
52
+ # early.
53
+ RUNTIME_ERROR = "runtimeError"
54
+
55
+ # Capture-incomplete reason emitted when a value is not captured
56
+ # because the capture time budget expired while it was being
57
+ # serialized.
58
+ CAPTURE_TIMEOUT = "timeout"
59
+
60
+ # Capture-incomplete reason emitted when a value at the maximum
61
+ # capture depth is not captured.
62
+ DEPTH = "depth"
63
+
64
+ # Capture-incomplete reason emitted when an object with more
65
+ # fields than the capture limit is partially captured.
66
+ FIELD_COUNT = "fieldCount"
67
+
68
+ # Capture-incomplete reason emitted when a collection with more
69
+ # items than the capture limit is partially captured.
70
+ COLLECTION_SIZE = "collectionSize"
71
+
72
+ # Capture-incomplete reason emitted when a captured string is
73
+ # trimmed to the capture limit.
74
+ STRING_LENGTH = "stringLength"
75
+
76
+ # Capture-incomplete reason for an early-capture cause with no
77
+ # more specific code.
78
+ OTHER = "other"
79
+
80
+ # The skip reasons the events.skipped metric tags with the firing
81
+ # probe's id; every other skip reason names a process-wide
82
+ # decision and carries no probe id.
83
+ PROBE_SCOPED_REASONS = [RATE_LIMIT_PROBE, EVALUATION_TIMEOUT].freeze
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "fatal_exceptions"
4
+ require_relative "guardrails_telemetry/reason"
5
+
6
+ module Datadog
7
+ module DI
8
+ # Telemetry namespace for DI metrics, shared with the other tracers.
9
+ # The telemetry wire name is
10
+ # +dd.instrumentation_telemetry_data.debugger.*+.
11
+ TELEMETRY_NAMESPACE = "debugger"
12
+
13
+ # Telemetry reporter for the DI guardrails: the reason codes and the
14
+ # metric emitters.
15
+ class GuardrailsTelemetry
16
+ # Skip count metric name, emitted under +DI::TELEMETRY_NAMESPACE+.
17
+ EVENTS_SKIPPED = "events.skipped"
18
+ # Drop count metric name, emitted under +DI::TELEMETRY_NAMESPACE+.
19
+ EVENTS_DROPPED = "events.dropped"
20
+ # Incomplete-capture count metric name, emitted under +DI::TELEMETRY_NAMESPACE+.
21
+ CAPTURE_INCOMPLETE = "capture.incomplete"
22
+
23
+ # Queue byte-count metric, emitted under +DI::TELEMETRY_NAMESPACE+.
24
+ QUEUE_DROPPED_BYTES = "guardrails.queue.dropped_bytes"
25
+
26
+ # Tag value for probes that capture a full snapshot.
27
+ EVENT_TYPE_SNAPSHOT = "snapshot"
28
+ # Tag value for probes that capture evaluated log output.
29
+ EVENT_TYPE_LOG = "log"
30
+
31
+ # Initializes the reporter.
32
+ #
33
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings the propagation escape hatch is read from
34
+ # @param logger [DI::Logger] logger for contained emission failures
35
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component the guardrails metrics are emitted through
36
+ def initialize(settings:, logger:, telemetry:)
37
+ @settings = settings
38
+ @logger = logger
39
+ @telemetry = telemetry
40
+ @skipped_tags_memo = {}
41
+ @dropped_tags_memo = {}
42
+ @capture_incomplete_tags_memo = {}
43
+ end
44
+
45
+ # The tracer settings.
46
+ # @return [Datadog::Core::Configuration::Settings]
47
+ attr_reader :settings
48
+ # The logger.
49
+ # @return [DI::Logger]
50
+ attr_reader :logger
51
+ # The component the guardrails metrics are emitted through.
52
+ # @return [Datadog::Core::Telemetry::Component, nil]
53
+ attr_reader :telemetry
54
+ # The memo of frozen tag hashes for emitted skip metrics without
55
+ # +probe_id+, keyed by reason and event type.
56
+ # @return [Hash{String => Hash{String => Hash{Symbol => String}}}]
57
+ attr_reader :skipped_tags_memo
58
+ # The memo of frozen tag hashes for emitted drop metrics, keyed by
59
+ # reason and event type.
60
+ # @return [Hash{String => Hash{String => Hash{Symbol => String}}}]
61
+ attr_reader :dropped_tags_memo
62
+ # The memo of frozen tag hashes for emitted incomplete-capture
63
+ # metrics, keyed by reason and event type.
64
+ # @return [Hash{String => Hash{String => Hash{Symbol => String}}}]
65
+ attr_reader :capture_incomplete_tags_memo
66
+
67
+ # Returns the canonical event_type tag for a probe, derived from
68
+ # whether the probe captures a full snapshot.
69
+ #
70
+ # @param probe [Probe] the probe whose firing is being tagged
71
+ # @return [String] EVENT_TYPE_SNAPSHOT or EVENT_TYPE_LOG
72
+ def self.probe_event_type_tag(probe)
73
+ probe.capture_snapshot? ? EVENT_TYPE_SNAPSHOT : EVENT_TYPE_LOG
74
+ end
75
+
76
+ # Emits the canonical +debugger.events.skipped+ count metric for a
77
+ # probe firing a guardrail rejected.
78
+ #
79
+ # @param reason [String] a GuardrailsTelemetry::Reason constant naming the skip cause
80
+ # @param probe [Probe] the probe whose firing was rejected
81
+ # @return [void]
82
+ def skipped(reason:, probe:)
83
+ probe_id = probe.id if Reason::PROBE_SCOPED_REASONS.include?(reason)
84
+ inc_metric(
85
+ DI::TELEMETRY_NAMESPACE, EVENTS_SKIPPED, 1,
86
+ tags: skipped_tags(reason, self.class.probe_event_type_tag(probe), probe_id),
87
+ )
88
+ nil
89
+ end
90
+
91
+ # Emits the canonical +debugger.events.dropped+ count metric, and,
92
+ # when +bytes+ is provided, the +guardrails.queue.dropped_bytes+ count
93
+ # metric.
94
+ #
95
+ # @param reason [String] a GuardrailsTelemetry::Reason constant naming the drop cause
96
+ # @param event_type [String] the canonical event_type tag value
97
+ # @param bytes [Integer, nil] encoded bytes discarded, when the drop site knows the count
98
+ # @return [void]
99
+ def dropped(reason:, event_type:, bytes: nil)
100
+ tags = dropped_tags(reason, event_type)
101
+ inc_metric(DI::TELEMETRY_NAMESPACE, EVENTS_DROPPED, 1, tags: tags)
102
+ inc_metric(DI::TELEMETRY_NAMESPACE, QUEUE_DROPPED_BYTES, bytes, tags: tags) if bytes
103
+ nil
104
+ end
105
+
106
+ # Emits the canonical +debugger.capture.incomplete+ count metric for
107
+ # a capture limit enforced on a snapshot that is still sent.
108
+ #
109
+ # @param reason [String] a GuardrailsTelemetry::Reason constant naming the enforced capture limit
110
+ # @param event_type [String] the canonical event_type tag value
111
+ # @return [void]
112
+ def capture_incomplete(reason:, event_type:)
113
+ inc_metric(
114
+ DI::TELEMETRY_NAMESPACE, CAPTURE_INCOMPLETE, 1,
115
+ tags: capture_incomplete_tags(reason, event_type),
116
+ )
117
+ nil
118
+ end
119
+
120
+ private
121
+
122
+ # Emits a guardrails count metric.
123
+ #
124
+ # @param namespace [String] telemetry namespace the metric is emitted under
125
+ # @param metric_name [String] guardrails metric name
126
+ # @param value [Integer] increment count, or the discarded byte count
127
+ # @param tags [Hash{Symbol => String}] frozen metric tags for the emission
128
+ # @return [void]
129
+ def inc_metric(namespace, metric_name, value, tags:)
130
+ telemetry&.inc(namespace, metric_name, value, tags: tags)
131
+ rescue Exception => exc # standard:disable Lint/RescueException
132
+ Datadog::DI.reraise_if_fatal(exc)
133
+ raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
134
+ report_emission_failure(namespace, metric_name, exc)
135
+ nil
136
+ end
137
+
138
+ def report_emission_failure(namespace, metric_name, exc)
139
+ logger.debug { "di: error emitting #{namespace}.#{metric_name} metric: #{exc.class}: #{exc.message}" }
140
+ begin
141
+ telemetry&.report(exc, description: "Error emitting #{namespace}.#{metric_name} metric")
142
+ rescue Exception => nested_exc # standard:disable Lint/RescueException
143
+ Datadog::DI.reraise_if_fatal(nested_exc)
144
+ logger.debug do
145
+ "di: error reporting #{namespace}.#{metric_name} metric emission failure: " \
146
+ "#{nested_exc.class}: #{nested_exc.message}"
147
+ end
148
+ end
149
+ nil
150
+ end
151
+
152
+ # Returns the frozen tags for a skip emission. Emissions without
153
+ # +probe_id+ are memoized; probe-scoped emissions allocate one
154
+ # frozen hash per emission: remote configuration adds and removes
155
+ # probes frequently, and a per-probe-id memo would grow without
156
+ # bound over the process lifetime.
157
+ #
158
+ # @param reason [String] the skip reason
159
+ # @param event_type [String] the canonical event_type tag value
160
+ # @param probe_id [String, nil] the rejected probe's id
161
+ # @return [Hash{Symbol => String}] the frozen skip-metric tags
162
+ def skipped_tags(reason, event_type, probe_id)
163
+ return {reason: reason, event_type: event_type, probe_id: probe_id}.freeze if probe_id
164
+
165
+ memoized_tags(skipped_tags_memo, reason, event_type)
166
+ end
167
+
168
+ # Returns the frozen tags for a drop emission, memoized per
169
+ # (reason, event_type) pair.
170
+ #
171
+ # @param reason [String] the drop reason
172
+ # @param event_type [String] the canonical event_type tag value
173
+ # @return [Hash{Symbol => String}] the frozen drop-metric tags
174
+ def dropped_tags(reason, event_type)
175
+ memoized_tags(dropped_tags_memo, reason, event_type)
176
+ end
177
+
178
+ # Returns the frozen tags for an incomplete-capture emission,
179
+ # memoized per (reason, event_type) pair.
180
+ #
181
+ # @param reason [String] the enforced capture limit
182
+ # @param event_type [String] the canonical event_type tag value
183
+ # @return [Hash{Symbol => String}] the frozen incomplete-capture metric tags
184
+ def capture_incomplete_tags(reason, event_type)
185
+ memoized_tags(capture_incomplete_tags_memo, reason, event_type)
186
+ end
187
+
188
+ # Returns the frozen tags memoized per (reason, event_type) pair,
189
+ # so a repeated emission allocates no tag hash. The memo is
190
+ # lock-free: a racy first write can only discard a duplicate
191
+ # frozen hash.
192
+ #
193
+ # @param memo [Hash{String => Hash{String => Hash{Symbol => String}}}] the emission-kind memo to index
194
+ # @param reason [String] the emission reason
195
+ # @param event_type [String] the canonical event_type tag value
196
+ # @return [Hash{Symbol => String}] the frozen metric tags
197
+ def memoized_tags(memo, reason, event_type)
198
+ reason_memos = memo[reason] ||= {}
199
+ reason_memos[event_type] ||= {reason: reason, event_type: event_type}.freeze
200
+ end
201
+ end
202
+ end
203
+ end
@@ -4,7 +4,7 @@ require_relative "../core/rate_limiter"
4
4
  require_relative "../core/utils/time"
5
5
  require_relative "../ruby_version"
6
6
  require_relative "fatal_exceptions"
7
- require_relative "capture_expression_evaluator"
7
+ require_relative "guardrails_telemetry"
8
8
 
9
9
  # rubocop:disable Lint/AssignmentInCondition
10
10
  # rubocop:disable Style/AndOr
@@ -78,11 +78,24 @@ module Datadog
78
78
  # the whole process.
79
79
  GLOBAL_LOG_RATE_LIMIT = 5000
80
80
 
81
- def initialize(settings, serializer, logger, code_tracker: nil, telemetry: nil)
81
+ # Initializes the instrumenter.
82
+ #
83
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings
84
+ # @param serializer [Serializer] serializer for captured values
85
+ # @param logger [DI::Logger] logger for instrumenter diagnostics
86
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical rate-limit skip metric
87
+ # @param capture_expression_evaluator [CaptureExpressionEvaluator] evaluator for the probe's capture expressions
88
+ # @param code_tracker [CodeTracker, nil] code tracker for line probe installation
89
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component instrumenter errors are reported through
90
+ # @return [void]
91
+ def initialize(settings, serializer, logger, guardrails_telemetry:, capture_expression_evaluator:,
92
+ code_tracker: nil, telemetry: nil)
82
93
  @settings = settings
83
94
  @serializer = serializer
84
95
  @logger = logger
85
96
  @telemetry = telemetry
97
+ @guardrails_telemetry = guardrails_telemetry
98
+ @capture_expression_evaluator = capture_expression_evaluator
86
99
  @code_tracker = code_tracker
87
100
  @global_snapshot_rate_limiter = Datadog::Core::TokenBucket.new(GLOBAL_SNAPSHOT_RATE_LIMIT)
88
101
  @global_log_rate_limiter = Datadog::Core::TokenBucket.new(GLOBAL_LOG_RATE_LIMIT)
@@ -94,6 +107,12 @@ module Datadog
94
107
  attr_reader :serializer
95
108
  attr_reader :logger
96
109
  attr_reader :telemetry
110
+ # The guardrails metric emitter.
111
+ # @return [GuardrailsTelemetry]
112
+ attr_reader :guardrails_telemetry
113
+ # The capture expression evaluator.
114
+ # @return [CaptureExpressionEvaluator]
115
+ attr_reader :capture_expression_evaluator
97
116
  attr_reader :code_tracker
98
117
 
99
118
  # The code tracker is a global singleton created lazily by
@@ -109,12 +128,6 @@ module Datadog
109
128
  # @return [Datadog::Core::TokenBucket]
110
129
  attr_reader :global_log_rate_limiter
111
130
 
112
- def capture_expression_evaluator
113
- @capture_expression_evaluator ||= CaptureExpressionEvaluator.new(
114
- settings: settings, serializer: serializer, logger: logger, telemetry: telemetry,
115
- )
116
- end
117
-
118
131
  # Returns the process-wide rate limiter for the probe.
119
132
  #
120
133
  # @param probe [Probe] the probe whose invocation is being rate limited
@@ -127,6 +140,24 @@ module Datadog
127
140
  end
128
141
  end
129
142
 
143
+ # Emits the canonical rate-limit skip metric for a probe.
144
+ #
145
+ # @param probe [Probe] the probe being skipped
146
+ # @param reason [String] a GuardrailsTelemetry::Reason constant for the rejecting limit
147
+ # @return [void]
148
+ def record_rate_limit_skip(reason, probe)
149
+ logger.trace do
150
+ "di: #{probe.type} probe #{probe.id}: skipping due to " \
151
+ "#{(reason == GuardrailsTelemetry::Reason::RATE_LIMIT_PROBE) ? "per-probe" : "global"} rate limit" \
152
+ " (#{reason})"
153
+ end
154
+ guardrails_telemetry.skipped(
155
+ reason: reason,
156
+ probe: probe,
157
+ )
158
+ nil
159
+ end
160
+
130
161
  # This is a substitute for Thread::Backtrace::Location
131
162
  # which does not have a public constructor.
132
163
  # Used for the fabricated stack frame for the method itself
@@ -559,17 +590,22 @@ module Datadog
559
590
  end
560
591
 
561
592
  rate_limiter = probe.rate_limiter
562
- admitted = continue && (rate_limiter.nil? || rate_limiter.allow?)
593
+ admitted = continue
594
+ if continue && rate_limiter && !rate_limiter.allow?
595
+ admitted = false
596
+ record_rate_limit_skip(GuardrailsTelemetry::Reason::RATE_LIMIT_PROBE, probe)
597
+ end
563
598
  if admitted && !probe_global_rate_limiter(probe).allow?
564
599
  admitted = false
565
- logger.trace { "di: #{probe.type} probe #{probe.id}: skipping due to global rate limit" }
600
+ record_rate_limit_skip(GuardrailsTelemetry::Reason::RATE_LIMIT_GLOBAL, probe)
566
601
  end
567
602
  if admitted
568
603
  # Arguments may be mutated by the method, therefore
569
604
  # they need to be serialized prior to method invocation.
570
605
  serialized_entry_args = if probe.capture_snapshot?
571
606
  serializer.serialize_args(args, kwargs, target_self,
572
- **probe.snapshot_serializer_limits(settings))
607
+ deadline: serializer.serialization_deadline,
608
+ **probe.snapshot_serializer_limits(settings),)
573
609
  end
574
610
 
575
611
  entry_capture_expressions = nil
@@ -581,8 +617,9 @@ module Datadog
581
617
  target_self: target_self,
582
618
  locals: serializer.combine_args(args, kwargs, target_self),
583
619
  )
584
- entry_capture_expressions, entry_capture_evaluation_errors =
585
- capture_expression_evaluator.evaluate(probe, entry_context)
620
+ entry_capture_result = capture_expression_evaluator.evaluate(probe, entry_context)
621
+ entry_capture_expressions = entry_capture_result.serialized_values
622
+ entry_capture_evaluation_errors = entry_capture_result.errors
586
623
  rescue Exception => exc # standard:disable Lint/RescueException
587
624
  Datadog::DI.reraise_if_fatal(exc)
588
625
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
@@ -751,6 +788,13 @@ module Datadog
751
788
  public :kwargs_from_splat
752
789
  end
753
790
 
791
+ # Trace point callback for a line probe.
792
+ #
793
+ # @param probe [Probe] the probe installed on this line
794
+ # @param iseq [RubyVM::InstructionSequence, nil] instruction sequence the trace point is targeted at, or nil when untargeted
795
+ # @param responder [#probe_executed_callback, #probe_condition_evaluation_failed_callback] callback target invoked with the built Context
796
+ # @param tp [TracePoint] the firing trace point
797
+ # @return [void]
754
798
  def line_trace_point_callback(probe, iseq, responder, tp)
755
799
  di_start_time = Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID)
756
800
 
@@ -827,10 +871,13 @@ module Datadog
827
871
 
828
872
  # In practice we should always have a rate limiter, but be safe
829
873
  # and check that it is in fact set.
830
- return if probe.rate_limiter && !probe.rate_limiter.allow?
874
+ if probe.rate_limiter && !probe.rate_limiter.allow?
875
+ record_rate_limit_skip(GuardrailsTelemetry::Reason::RATE_LIMIT_PROBE, probe)
876
+ return
877
+ end
831
878
 
832
879
  unless probe_global_rate_limiter(probe).allow?
833
- logger.trace { "di: #{probe.type} probe #{probe.id}: skipping due to global rate limit" }
880
+ record_rate_limit_skip(GuardrailsTelemetry::Reason::RATE_LIMIT_GLOBAL, probe)
834
881
  return
835
882
  end
836
883
 
@@ -843,12 +890,14 @@ module Datadog
843
890
  responder.probe_executed_callback(context)
844
891
 
845
892
  check_and_disable_if_exceeded(probe, responder, di_start_time)
893
+ nil
846
894
  rescue Exception => exc # standard:disable Lint/RescueException
847
895
  Datadog::DI.reraise_if_fatal(exc)
848
896
  raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
849
897
  logger.debug { "di: unhandled exception in line trace point: #{exc.class}: #{exc.message}" }
850
898
  telemetry&.report(exc, description: "Unhandled exception in line trace point")
851
899
  # TODO test this path
900
+ nil
852
901
  end
853
902
 
854
903
  def build_trace_point_context(probe, tp)
@@ -881,6 +930,7 @@ module Datadog
881
930
  # avoid a dependency on ProbeManager from Instrumenter.
882
931
  probe.disable!
883
932
  responder.probe_disabled_callback(probe, di_duration)
933
+ nil
884
934
  end
885
935
  end
886
936
 
@@ -1,29 +1,51 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "forwardable"
4
-
5
3
  module Datadog
6
4
  module DI
7
5
  # Logger facade to add the +trace+ method.
8
6
  #
9
7
  # @api private
10
8
  class Logger
11
- extend Forwardable
12
-
9
+ # Initializes the logger facade with the trace-logging gate resolved
10
+ # once.
11
+ # Resolving the gate at construction keeps the per-call settings
12
+ # traversal off the probe hot path. Components are rebuilt on
13
+ # reconfiguration, so the cached value tracks configuration
14
+ # changes.
15
+ #
16
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings the gate is resolved from
17
+ # @param target [::Logger] logger the facade delegates to
13
18
  def initialize(settings, target)
14
- @settings = settings
19
+ @trace_logging = settings.dynamic_instrumentation.internal.trace_logging
15
20
  @target = target
16
21
  end
17
22
 
18
- attr_reader :settings
23
+ # Whether trace logging is enabled.
24
+ # @return [Boolean]
25
+ attr_reader :trace_logging
26
+ # The target logger.
27
+ # @return [::Logger]
19
28
  attr_reader :target
20
29
 
21
- def_delegators :target, :debug
30
+ # Writes the given message or the block's message to the target
31
+ # logger.
32
+ #
33
+ # @param message [String, nil] message written when no block is given
34
+ # @return [void]
35
+ def debug(message = nil, &block)
36
+ target.debug(message, &block)
37
+ nil
38
+ end
22
39
 
40
+ # Writes the block's message to the target logger at debug level when
41
+ # the trace-logging gate resolved at construction is enabled.
42
+ #
43
+ # @return [void]
23
44
  def trace
24
- if settings.dynamic_instrumentation.internal.trace_logging
45
+ if trace_logging
25
46
  debug { yield }
26
47
  end
48
+ nil
27
49
  end
28
50
  end
29
51
  end
@@ -3,6 +3,7 @@
3
3
  # rubocop:disable Lint/AssignmentInCondition
4
4
 
5
5
  require_relative "fatal_exceptions"
6
+ require_relative "guardrails_telemetry"
6
7
 
7
8
  module Datadog
8
9
  module DI
@@ -18,14 +19,26 @@ module Datadog
18
19
  #
19
20
  # @api private
20
21
  class ProbeManager
22
+ # Initializes the probe manager.
23
+ #
24
+ # @param settings [Datadog::Core::Configuration::Settings] tracer settings
25
+ # @param instrumenter [Instrumenter] instrumenter that executes installed probes
26
+ # @param probe_notification_builder [ProbeNotificationBuilder] builder for status and snapshot payloads
27
+ # @param probe_notifier_worker [ProbeNotifierWorker] worker that sends the built payloads
28
+ # @param logger [DI::Logger] logger for probe manager diagnostics
29
+ # @param probe_repository [ProbeRepository] repository of installed probes
30
+ # @param guardrails_telemetry [GuardrailsTelemetry] emitter for the canonical condition-error skip metric
31
+ # @param telemetry [Datadog::Core::Telemetry::Component, nil] component probe manager errors are reported through
32
+ # @return [void]
21
33
  def initialize(settings, instrumenter, probe_notification_builder,
22
- probe_notifier_worker, logger, probe_repository, telemetry: nil)
34
+ probe_notifier_worker, logger, probe_repository, guardrails_telemetry:, telemetry: nil)
23
35
  @settings = settings
24
36
  @instrumenter = instrumenter
25
37
  @probe_notification_builder = probe_notification_builder
26
38
  @probe_notifier_worker = probe_notifier_worker
27
39
  @logger = logger
28
40
  @telemetry = telemetry
41
+ @guardrails_telemetry = guardrails_telemetry
29
42
  @probe_repository = probe_repository
30
43
 
31
44
  @definition_trace_point = TracePoint.new(:end) do |tp|
@@ -40,6 +53,9 @@ module Datadog
40
53
 
41
54
  attr_reader :logger
42
55
  attr_reader :telemetry
56
+ # The guardrails metric emitter.
57
+ # @return [GuardrailsTelemetry]
58
+ attr_reader :guardrails_telemetry
43
59
  attr_reader :probe_repository
44
60
 
45
61
  # Stops the probe manager without permanently releasing resources.
@@ -303,7 +319,7 @@ module Datadog
303
319
  end
304
320
 
305
321
  payload = probe_notification_builder.build_executed(context)
306
- probe_notifier_worker.add_snapshot(payload)
322
+ probe_notifier_worker.add_snapshot(payload, probe: probe)
307
323
  end
308
324
 
309
325
  # Callback invoked when a probe's condition expression fails to evaluate.
@@ -317,16 +333,29 @@ module Datadog
317
333
  # @param context [Context] The execution context containing probe and captured data
318
334
  # @param expr [EL::Expression] The condition expression that failed
319
335
  # @param exc [Exception] The exception raised during condition evaluation
336
+ # @return [void]
320
337
  def probe_condition_evaluation_failed_callback(context, expr, exc)
321
338
  probe = context.probe
322
- if probe.condition_evaluation_failed_rate_limiter&.allow?
339
+ rate_limiter = probe.condition_evaluation_failed_rate_limiter
340
+ if rate_limiter&.allow?
323
341
  if instrumenter.global_snapshot_rate_limiter.allow?
324
342
  payload = probe_notification_builder.build_condition_evaluation_failed(context, expr, exc)
325
- probe_notifier_worker.add_snapshot(payload)
343
+ probe_notifier_worker.add_snapshot(payload, probe: probe)
326
344
  else
327
- logger.trace { "di: #{probe.type} probe #{probe.id}: skipping condition error snapshot due to global rate limit" }
345
+ logger.trace do
346
+ "di: #{probe.type} probe #{probe.id}: skipping condition error snapshot due to global rate limit" \
347
+ " (#{GuardrailsTelemetry::Reason::RATE_LIMIT_GLOBAL})"
348
+ end
349
+ record_condition_error_skip(GuardrailsTelemetry::Reason::RATE_LIMIT_GLOBAL, probe)
350
+ end
351
+ elsif rate_limiter
352
+ logger.trace do
353
+ "di: #{probe.type} probe #{probe.id}: skipping condition evaluation failure" \
354
+ " notification due to per-probe rate limit" \
355
+ " (#{GuardrailsTelemetry::Reason::EVALUATION_ERROR_THROTTLED})"
328
356
  end
329
357
  end
358
+ nil
330
359
  end
331
360
 
332
361
  # Callback invoked when a probe is disabled, for example due to
@@ -341,6 +370,20 @@ module Datadog
341
370
  probe_notifier_worker.add_status(payload, probe: probe)
342
371
  end
343
372
 
373
+ # Emits the canonical skip metric for a condition evaluation failure
374
+ # notification rejected by a rate limiter.
375
+ #
376
+ # @param probe [Probe] the probe whose condition evaluation failed
377
+ # @param reason [String] the GuardrailsTelemetry::Reason constant for the rejecting limit
378
+ # @return [void]
379
+ def record_condition_error_skip(reason, probe)
380
+ guardrails_telemetry.skipped(
381
+ reason: reason,
382
+ probe: probe,
383
+ )
384
+ nil
385
+ end
386
+
344
387
  # Class/module definition trace point (:end type).
345
388
  # Used to install hooks when the target classes/modules aren't yet
346
389
  # defined when the hook request is received.