Class: Datadog::DI::ProbeManager Private

Inherits:
Object
  • Object
show all
Defined in:
lib/datadog/di/probe_manager.rb

Overview

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

Stores probes received from remote config (that we can parse, in other words, whose type/attributes we support), requests needed instrumentation for the probes via Instrumenter, and stores pending probes (those which haven’t yet been instrumented successfully due to their targets not existing) and failed probes (where we are certain the target will not ever be loaded, or otherwise become valid).

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(settings, instrumenter, probe_notification_builder, probe_notifier_worker, logger, telemetry: nil) ⇒ ProbeManager

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 new instance of ProbeManager.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# File 'lib/datadog/di/probe_manager.rb', line 18

def initialize(settings, instrumenter, probe_notification_builder,
  probe_notifier_worker, logger, telemetry: nil)
  @settings = settings
  @instrumenter = instrumenter
  @probe_notification_builder = probe_notification_builder
  @probe_notifier_worker = probe_notifier_worker
  @logger = logger
  @telemetry = telemetry
  @installed_probes = {}
  @pending_probes = {}
  @failed_probes = {}
  @lock = Monitor.new

  @definition_trace_point = TracePoint.trace(:end) do |tp|
    install_pending_method_probes(tp.self)
  rescue => exc
    raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
    logger.debug { "di: unhandled exception in definition trace point: #{exc.class}: #{exc}" }
    telemetry&.report(exc, description: "Unhandled exception in definition trace point")
    # TODO test this path
  end
end

Instance Attribute Details

#definition_trace_pointObject (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.

Class/module definition trace point (:end type). Used to install hooks when the target classes/modules aren’t yet defined when the hook request is received.



262
263
264
# File 'lib/datadog/di/probe_manager.rb', line 262

def definition_trace_point
  @definition_trace_point
end

#instrumenterObject (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.



62
63
64
# File 'lib/datadog/di/probe_manager.rb', line 62

def instrumenter
  @instrumenter
end

#loggerObject (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.



41
42
43
# File 'lib/datadog/di/probe_manager.rb', line 41

def logger
  @logger
end

#probe_notification_builderObject (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.



63
64
65
# File 'lib/datadog/di/probe_manager.rb', line 63

def probe_notification_builder
  @probe_notification_builder
end

#probe_notifier_workerObject (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.



64
65
66
# File 'lib/datadog/di/probe_manager.rb', line 64

def probe_notifier_worker
  @probe_notifier_worker
end

#settingsObject (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.



61
62
63
# File 'lib/datadog/di/probe_manager.rb', line 61

def settings
  @settings
end

#telemetryObject (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.



42
43
44
# File 'lib/datadog/di/probe_manager.rb', line 42

def telemetry
  @telemetry
end

Instance Method Details

#add_probe(probe) ⇒ 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.

Requests to install the specified probe.

If the target of the probe does not exist, assume the relevant code is not loaded yet (rather than that it will never be loaded), and store the probe in a pending probe list. When classes are defined, or files loaded, the probe will be checked against the newly defined classes/loaded files, and will be installed if it matches.



95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/datadog/di/probe_manager.rb', line 95

def add_probe(probe)
  @lock.synchronize do
    if @installed_probes[probe.id]
      # Either this probe was already installed, or another probe was
      # installed with the same id (previous version perhaps?).
      # Since our state tracking is keyed by probe id, we cannot
      # install this probe since we won't have a way of removing the
      # instrumentation for the probe with the same id which is already
      # installed.
      #
      # The exception raised here will be caught below and logged and
      # reported to telemetry.
      raise Error::AlreadyInstrumented, "Probe with id #{probe.id} is already in installed probes"
    end

    # Probe failed to install previously, do not try to install it again.
    if msg = @failed_probes[probe.id]
      # TODO test this path
      raise Error::ProbePreviouslyFailed, msg
    end

    begin
      instrumenter.hook(probe, self)

      @installed_probes[probe.id] = probe
      payload = probe_notification_builder.build_installed(probe)
      probe_notifier_worker.add_status(payload)
      # The probe would only be in the pending probes list if it was
      # previously attempted to be installed and the target was not loaded.
      # Always remove from pending list here because it makes the
      # API smaller and shouldn't cause any actual problems.
      @pending_probes.delete(probe.id)
      logger.trace { "di: installed #{probe.type} probe at #{probe.location} (#{probe.id})" }
      true
    rescue Error::DITargetNotDefined
      @pending_probes[probe.id] = probe
      logger.trace { "di: could not install #{probe.type} probe at #{probe.location} (#{probe.id}) because its target is not defined, adding it to pending list" }
      false
    end
  rescue => exc
    # In "propagate all exceptions" mode we will try to instrument again.
    raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions

    logger.debug { "di: error processing probe configuration: #{exc.class}: #{exc}" }
    telemetry&.report(exc, description: "Error processing probe configuration")
    # TODO report probe as failed to agent since we won't attempt to
    # install it again.

    # TODO add top stack frame to message
    @failed_probes[probe.id] = "#{exc.class}: #{exc}"

    raise
  end
end

#clear_hooksObject

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.



51
52
53
54
55
56
57
58
59
# File 'lib/datadog/di/probe_manager.rb', line 51

def clear_hooks
  @lock.synchronize do
    @pending_probes.clear
    @installed_probes.each do |probe_id, probe|
      instrumenter.unhook(probe)
    end
    @installed_probes.clear
  end
end

#closeObject

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.

TODO test that close is called during component teardown and the trace point is cleared



46
47
48
49
# File 'lib/datadog/di/probe_manager.rb', line 46

def close
  definition_trace_point.disable
  clear_hooks
end

#failed_probesObject

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.

Probes that failed to instrument for reasons other than the target is not yet loaded are added to this collection, so that we do not try to instrument them every time remote configuration is processed.



81
82
83
84
85
# File 'lib/datadog/di/probe_manager.rb', line 81

def failed_probes
  @lock.synchronize do
    @failed_probes
  end
end

#install_pending_line_probes(path) ⇒ 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.

Installs pending line probes, if any, for the file of the specified absolute path.

This method is meant to be called from the script_compiled trace point, which is invoked for each required or loaded file (and also for eval’d code, but those invocations are filtered out).



216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/datadog/di/probe_manager.rb', line 216

def install_pending_line_probes(path)
  if path.nil?
    raise ArgumentError, "path must not be nil"
  end
  @lock.synchronize do
    @pending_probes.values.each do |probe|
      if probe.line?
        if probe.file_matches?(path)
          add_probe(probe)
        end
      end
    end
  end
end

#installed_probesObject

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.



66
67
68
69
70
# File 'lib/datadog/di/probe_manager.rb', line 66

def installed_probes
  @lock.synchronize do
    @installed_probes
  end
end

#pending_probesObject

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.



72
73
74
75
76
# File 'lib/datadog/di/probe_manager.rb', line 72

def pending_probes
  @lock.synchronize do
    @pending_probes
  end
end

#probe_condition_evaluation_failed_callback(context, expr, exc) ⇒ 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.



251
252
253
254
255
256
257
# File 'lib/datadog/di/probe_manager.rb', line 251

def probe_condition_evaluation_failed_callback(context, expr, exc)
  probe = context.probe
  if probe.condition_evaluation_failed_rate_limiter&.allow?
    payload = probe_notification_builder.build_condition_evaluation_failed(context, expr, exc)
    probe_notifier_worker.add_snapshot(payload)
  end
end

#probe_executed_callback(context) ⇒ 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.

Entry point invoked from the instrumentation when the specfied probe is invoked (that is, either its target method is invoked, or execution reached its target file/line).

This method is responsible for queueing probe status to be sent to the backend (once per the probe’s lifetime) and a snapshot corresponding to the current invocation.



238
239
240
241
242
243
244
245
246
247
248
249
# File 'lib/datadog/di/probe_manager.rb', line 238

def probe_executed_callback(context)
  probe = context.probe
  logger.trace { "di: executed #{probe.type} probe at #{probe.location} (#{probe.id})" }
  unless probe.emitting_notified?
    payload = probe_notification_builder.build_emitting(probe)
    probe_notifier_worker.add_status(payload)
    probe.emitting_notified = true
  end

  payload = probe_notification_builder.build_executed(context)
  probe_notifier_worker.add_snapshot(payload)
end

#remove_probe(probe_id) ⇒ 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.

Removes probe with specified id. The probe could be pending or installed. Does nothing if there is no probe with the specified id.



152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/datadog/di/probe_manager.rb', line 152

def remove_probe(probe_id)
  @lock.synchronize do
    @pending_probes.delete(probe_id)
  end

  # Do not delete the probe from the registry here in case
  # deinstrumentation fails - though I don't know why deinstrumentation
  # would fail and how we could recover if it does.
  # I plan on tracking the number of outstanding (instrumented) probes
  # in the future, and if deinstrumentation fails I would want to
  # keep that probe as "installed" for the count, so that we can
  # investigate the situation.
  if probe = @installed_probes[probe_id]
    begin
      instrumenter.unhook(probe)
      @installed_probes.delete(probe_id)
    rescue => exc
      raise if settings.dynamic_instrumentation.internal.propagate_all_exceptions
      # Silence all exceptions?
      # TODO should we propagate here and rescue upstream?
      logger.debug { "di: error removing #{probe.type} probe at #{probe.location} (#{probe.id}): #{exc.class}: #{exc}" }
      telemetry&.report(exc, description: "Error removing probe")
    end
  end
end