Module: Datadog::DI Private

Defined in:
lib/datadog/di.rb,
lib/datadog/di/base.rb,
lib/datadog/di/error.rb,
lib/datadog/di/probe.rb,
lib/datadog/di/utils.rb,
lib/datadog/di/logger.rb,
lib/datadog/di/remote.rb,
lib/datadog/di/context.rb,
lib/datadog/di/contrib.rb,
lib/datadog/di/redactor.rb,
lib/datadog/di/component.rb,
lib/datadog/di/extensions.rb,
lib/datadog/di/serializer.rb,
lib/datadog/di/el/compiler.rb,
lib/datadog/di/code_tracker.rb,
lib/datadog/di/el/evaluator.rb,
lib/datadog/di/instrumenter.rb,
lib/datadog/di/configuration.rb,
lib/datadog/di/el/expression.rb,
lib/datadog/di/probe_builder.rb,
lib/datadog/di/probe_manager.rb,
lib/datadog/di/capture_limits.rb,
lib/datadog/di/proc_responder.rb,
lib/datadog/di/transport/http.rb,
lib/datadog/di/contrib/railtie.rb,
lib/datadog/di/transport/input.rb,
lib/datadog/di/fatal_exceptions.rb,
lib/datadog/di/probe_repository.rb,
lib/datadog/di/probe_file_loader.rb,
lib/datadog/di/capture_expression.rb,
lib/datadog/di/transport/http/input.rb,
lib/datadog/di/probe_notifier_worker.rb,
lib/datadog/di/transport/diagnostics.rb,
lib/datadog/di/probe_file_loader/railtie.rb,
lib/datadog/di/probe_notification_builder.rb,
lib/datadog/di/transport/http/diagnostics.rb,
lib/datadog/di/capture_expression_evaluator.rb

Overview

This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.

Namespace for Datadog dynamic instrumentation.

Defined Under Namespace

Modules: Configuration, Contrib, EL, Extensions, ProbeBuilder, ProbeFileLoader, Remote, Transport, Utils Classes: CaptureExpression, CaptureExpressionEvaluator, CaptureLimits, CodeTracker, Component, Context, Error, Instrumenter, Logger, Probe, ProbeManager, ProbeNotificationBuilder, ProbeNotifierWorker, ProbeRepository, ProcResponder, Redactor, Serializer

Constant Summary collapse

INSTRUMENTED_COUNTERS_LOCK =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Mutex.new
EXCEPTION_BACKTRACE_LOCATIONS =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Captured at load time from Exception itself (not a subclass). Used to bypass subclass overrides of backtrace_locations.

This does NOT protect against monkeypatching Exception#backtrace_locations before dd-trace-rb loads — in that case we'd capture the monkeypatch. The practical threat model is customer subclasses overriding the method:

class MyError < StandardError
def backtrace_locations; []; end
end

The UnboundMethod bypasses subclass overrides: bind(exception).call always dispatches to the original Exception implementation.

Note: if the subclass overrides #backtrace (not #backtrace_locations), MRI's setup_exception skips storing the VM backtrace entirely — both returns nil. See EXCEPTION_BACKTRACE comment for details.

Exception.instance_method(:backtrace_locations)
EXCEPTION_BACKTRACE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Same UnboundMethod trick for Exception#backtrace (Array). Used as a fallback when backtrace_locations returns nil — which happens when someone calls Exception#set_backtrace with an Array.

set_backtrace accepts Array or nil. When called with strings, it replaces the VM-level backtrace: backtrace returns the new strings, but backtrace_locations returns nil because the VM cannot reconstruct Location objects from formatted strings. This occurs in exception wrapping patterns where a library catches an exception, creates a new one, and copies the original's string backtrace onto it via set_backtrace before re-raising.

Ruby 3.4+ also allows set_backtrace(Array), which preserves backtrace_locations — but older Rubies and most existing code use the string form.

Like EXCEPTION_BACKTRACE_LOCATIONS, this UnboundMethod bypasses subclass overrides of #backtrace: bind(exception).call dispatches to Exception#backtrace regardless of what the subclass defines.

However, when a subclass overrides #backtrace, MRI's setup_exception (eval.c) calls the override via rb_get_backtrace during raise. If it gets a non-nil result, it skips storing the VM backtrace in @bt and at dispatch but reads nil from @bt because the data was never stored.

This constant is used as a fallback when backtrace_locations returns nil. In the common set_backtrace-with-strings case, no subclass override is involved and the fallback works. The only unrecoverable case: a subclass overrides #backtrace, the exception is raised normally, and set_backtrace is never called. Both @bt and message are still reported).

Exception.instance_method(:backtrace)
LOCK =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Mutex.new
FATAL_EXCEPTION_CLASSES =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Exception classes that catch-all rescues must never swallow: they signal that the process is being torn down or has run out of memory and have to propagate. SignalException covers Interrupt (Ctrl+C) and every other signal delivered as an exception.

[SystemExit, SignalException, NoMemoryError].freeze

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.code_trackerObject (readonly)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



29
30
31
# File 'lib/datadog/di/base.rb', line 29

def code_tracker
  @code_tracker
end

Class Method Details

.activate_trackingObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Activates code tracking if possible.

This method does nothing if invoked in an environment that does not implement required trace points for code tracking (MRI Ruby < 2.6, JRuby) and rescues any exceptions that may be raised by downstream DI code.



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/datadog/di/base.rb', line 49

def activate_tracking
  # :script_compiled trace point was added in Ruby 2.6.
  return unless RubyVersion.is?(">= 2.6")

  begin
    # Activate code tracking by default because line trace points will not work
    # without it.
    Datadog::DI.activate_tracking!
  rescue Exception => exc # standard:disable Lint/RescueException
    Datadog::DI.reraise_if_fatal(exc)
    if defined?(Datadog.logger)
      Datadog.logger.warn { "di: Failed to activate code tracking for DI: #{exc.class}: #{exc.message}" }
    else
      # We do not have Datadog logger potentially because DI code tracker is
      # being loaded early in application boot process and the rest of datadog
      # wasn't loaded yet. Output to standard error.
      warn("datadog: di: Failed to activate code tracking for DI: #{exc.class}: #{exc.message}")
    end
  end
end

.activate_tracking!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Activates code tracking. Normally this method should be called when the application starts. If instrumenting third-party code, code tracking needs to be enabled before the third-party libraries are loaded. Any third-party code loaded before code tracking is activated will NOT be instrumentable using dynamic instrumentation.

TODO test that activating tracker multiple times preserves existing mappings in the registry



39
40
41
# File 'lib/datadog/di/base.rb', line 39

def activate_tracking!
  (@code_tracker ||= CodeTracker.new).start
end

.add_current_component(component) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

To avoid potential races with DI::Component being added and removed, we maintain a list of the components. Normally the list should contain either zero or one component depending on whether DI is enabled in Datadog configuration. However, if a new instance of DI::Component is created while the previous instance is still running, we are guaranteed to not end up with no component when one is running.



110
111
112
113
114
# File 'lib/datadog/di/base.rb', line 110

def add_current_component(component)
  LOCK.synchronize do
    @current_components << component
  end
end

.code_tracking_active?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns whether code tracking is available. This method should be used instead of querying #code_tracker because the latter one may be nil.

Returns:

  • (Boolean)


84
85
86
# File 'lib/datadog/di/base.rb', line 84

def code_tracking_active?
  code_tracker&.active? || false
end

.componentObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

This method is called from DI Remote handler to issue DI operations to the probe manager (add or remove probes).

When DI Remote is executing, Datadog.components should be initialized and we should be able to reference it to get to the DI component.

Given that we need the current_component anyway for code tracker, perhaps we should delete the component method and just use current_component in all cases.



172
173
174
# File 'lib/datadog/di.rb', line 172

def component
  Datadog.send(:components).dynamic_instrumentation
end

.current_componentObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

DI code tracker is instantiated globally before the regular set of components is created, but the code tracker needs to call out to the "current" DI component to perform instrumentation when application code is loaded. Because this call may happen prior to Datadog components having been initialized, we maintain the "current component" which contains a reference to the most recently instantiated DI::Component. This way, if a DI component hasn't been instantiated, we do not try to reference Datadog.components. In other words, this method exists so that we never attempt to call Datadog.components from the code tracker.



98
99
100
101
102
# File 'lib/datadog/di/base.rb', line 98

def current_component
  LOCK.synchronize do
    @current_components.last
  end
end

.deactivate_tracking!Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Deactivates code tracking. In normal usage of DI this method should never be called, however it is used by DI's test suite to reset state for individual tests.

Note that deactivating tracking clears out the registry, losing the ability to look up files that have been loaded into the process already.



77
78
79
# File 'lib/datadog/di/base.rb', line 77

def deactivate_tracking!
  code_tracker&.stop
end

.enabled?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns:

  • (Boolean)


70
71
72
# File 'lib/datadog/di.rb', line 70

def enabled?
  Datadog.configuration.dynamic_instrumentation.enabled
end

.file_iseqsArray<RubyVM::InstructionSequence>

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns iseqs that correspond to loaded files (filtering out eval'd code).

There are several types of iseqs returned by all_iseqs:

  1. Eval'd code — these have a nil absolute_path and are filtered out here.
  2. Whole-file iseqs — have absolute_path set and first_lineno of 0. Only available for a subset of loaded files (the full-file iseq may be garbage collected after loading completes). Easiest to work with since we just match the file path to the probe specification.
  3. Per-method iseqs — have absolute_path set and first_lineno > 0. Often the only iseqs available for third-party code. Require identifying the correct iseq containing the target line, which may involve examining the iseq's trace_points since define_method can create nested, non-contiguous line ranges.

Note: the same line of code can appear in multiple iseqs (e.g. when define_method is used inside a method). DI treats this as an error since a probe must resolve to exactly one code location.

Returns:

  • (Array<RubyVM::InstructionSequence>)

    iseqs with non-nil absolute_path



157
158
159
160
161
# File 'lib/datadog/di.rb', line 157

def file_iseqs
  all_iseqs.select do |iseq|
    iseq.absolute_path
  end
end

.instrumented_count(kind = nil) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Track how many outstanding instrumentations are in DI.

It is hard to find the actual instrumentations - there is no method provided by Ruby to list all trace points, and we would need to manually track our instrumentation modules for method probes. Plus, tracking the modules could create active references to instrumentation, which is not desired.

A simpler solution is to maintain a counter which is increased whenever a probe is installed and decreased when a probe is removed.

This counter does not include pending probes - being not installed, those pose no concerns to customer applications.



189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/datadog/di.rb', line 189

def instrumented_count(kind = nil)
  INSTRUMENTED_COUNTERS_LOCK.synchronize do
    if defined?(@instrumented_count)
      if kind
        validate_kind!(kind)
        @instrumented_count[kind] || 0
      else
        @instrumented_count.inject(0) do |sum, (_kind, count)|
          sum + count
        end
      end
    else
      0
    end
  end
end

.instrumented_count_dec(kind) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



214
215
216
217
218
219
220
221
222
223
224
# File 'lib/datadog/di.rb', line 214

def instrumented_count_dec(kind)
  validate_kind!(kind)
  INSTRUMENTED_COUNTERS_LOCK.synchronize do
    @instrumented_count = Hash.new(0) unless defined?(@instrumented_count)
    if @instrumented_count[kind] <= 0
      Datadog.logger.debug { "di: attempting to decrease instrumented count below zero for #{kind}" }
      return
    end
    @instrumented_count[kind] -= 1
  end
end

.instrumented_count_inc(kind) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



206
207
208
209
210
211
212
# File 'lib/datadog/di.rb', line 206

def instrumented_count_inc(kind)
  validate_kind!(kind)
  INSTRUMENTED_COUNTERS_LOCK.synchronize do
    @instrumented_count = Hash.new(0) unless defined?(@instrumented_count)
    @instrumented_count[kind] += 1
  end
end

.remove_current_component(component) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



116
117
118
119
120
# File 'lib/datadog/di/base.rb', line 116

def remove_current_component(component)
  LOCK.synchronize do
    @current_components.delete(component)
  end
end

.reraise_if_fatal(exc) ⇒ nil

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Re-raise exc when it is fatal (see FATAL_EXCEPTION_CLASSES). Call this as the first statement of a rescue Exception handler so that fatal conditions are not accidentally swallowed by a broad rescue.

Parameters:

  • exc (Exception)

    the currently-handled exception

Returns:

  • (nil)

    when exc is not fatal; otherwise re-raises exc



22
23
24
# File 'lib/datadog/di/fatal_exceptions.rb', line 22

def self.reraise_if_fatal(exc)
  raise exc if FATAL_EXCEPTION_CLASSES.any? { |klass| exc.is_a?(klass) }
end

.supported_runtime?Boolean

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Whether the current Ruby runtime can run dynamic instrumentation: MRI (CRuby) on Ruby 2.6 or later.

Returns:

  • (Boolean)


119
120
121
# File 'lib/datadog/di.rb', line 119

def supported_runtime?
  unsupported_platform_reason.nil?
end

.unsupported_reason(settings = Datadog.configuration) ⇒ String?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Returns a human-readable reason why dynamic instrumentation cannot run under the given settings, or nil if all build-time preconditions are met.

Single source of truth for the preconditions checked in Datadog::DI::Component.build and reported back by Datadog::DI::Remote.handle_rc_enablement when an implicit enablement signal arrives but the component was not built at startup. Checks are ordered from most-actionable to platform-constraint so the most useful reason wins.

The settings argument is optional so the helper can be called from contexts that don't have settings in scope (e.g. the RC handler).

Parameters:

Returns:

  • (String, nil)

    reason string or nil when supported



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/datadog/di.rb', line 88

def unsupported_reason(settings = Datadog.configuration)
  # Symmetric to the respond_to?(:remote) guard below: in unusual
  # configurations (test doubles, partial Settings) the DI namespace
  # may be absent. Returning a reason here lets callers — most
  # importantly Remote.handle_rc_enablement — emit the customer-facing
  # warn instead of raising NoMethodError on the unguarded access at
  # line 92 (`settings.dynamic_instrumentation.internal.development`).
  unless settings.respond_to?(:dynamic_instrumentation)
    return "dynamic instrumentation settings are not available"
  end
  unless settings.respond_to?(:remote) && settings.remote.enabled
    return "Remote Configuration is not enabled. See https://docs.datadoghq.com/agent/remote_config"
  end
  unless settings.dynamic_instrumentation.internal.development
    if Datadog::Core::Environment::Execution.development?
      return "development environment detected"
    end
  end
  if (reason = unsupported_platform_reason)
    return reason
  end
  unless respond_to?(:exception_message)
    return "C extension is not available"
  end
  nil
end