Class: Datadog::DI::CodeTracker Private

Inherits:
Object
  • Object
show all
Defined in:
lib/datadog/di/code_tracker.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.

Tracks loaded Ruby code by source file and maintains a map from source file to the loaded code (instruction sequences). Also arranges for code in the loaded files to be instrumented by line probes that have already been received by the library.

The loaded code is used to target line trace points when installing line probes which dramatically improves efficiency of line trace points.

Note that, since most files will only be loaded one time (via the “require” mechanism), the code tracker needs to be global and not be recreated when the DI component is created.

Instance Method Summary collapse

Constructor Details

#initializeCodeTracker

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 CodeTracker.



21
22
23
24
25
26
# File 'lib/datadog/di/code_tracker.rb', line 21

def initialize
  @registry = {}
  @trace_point_lock = Mutex.new
  @registry_lock = Mutex.new
  @compiled_trace_point = nil
end

Instance Method Details

#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 this code tracker has been activated and is tracking.

Returns:

  • (Boolean)


109
110
111
112
113
# File 'lib/datadog/di/code_tracker.rb', line 109

def active?
  trace_point_lock.synchronize do
    !!@compiled_trace_point
  end
end

#clearObject

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.

Clears the stored mapping from paths to compiled code.

This method should normally never be called. It is meant to be used only by the test suite.



176
177
178
179
180
# File 'lib/datadog/di/code_tracker.rb', line 176

def clear
  registry_lock.synchronize do
    registry.clear
  end
end

#iseqs_for_path_suffix(suffix) ⇒ 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.

Returns an array of RubVM::InstructionSequence (i.e. the compiled code) for the provided path.

The argument can be a full path to a Ruby source code file or a suffix (basename + one or more directories preceding the basename). The idea with suffix matches is that file paths are likely to be different between development and production environments and the source control system uses relative paths and doesn’t have absolute paths at all.

Suffix matches are not guaranteed to be correct, meaning there may be multiple files with the same basename and they may all match a given suffix. In such cases, this method will return all matching paths (and all of these paths will be attempted to be instrumented by upstream code).

If the suffix matches one of the paths completely (which requires it to be an absolute path), only the exactly matching path is returned. Otherwise all known paths that end in the suffix are returned. If no paths match, an empty array is returned.



135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/datadog/di/code_tracker.rb', line 135

def iseqs_for_path_suffix(suffix)
  registry_lock.synchronize do
    exact = registry[suffix]
    return [suffix, exact] if exact

    inexact = []
    registry.each do |path, iseq|
      if Utils.path_matches_suffix?(path, suffix)
        inexact << [path, iseq]
      end
    end
    if inexact.length > 1
      raise Error::MultiplePathsMatch, "Multiple paths matched requested suffix"
    end
    inexact.first
  end
end

#startObject

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.

Starts tracking loaded code.

This method should generally be called early in application boot process, because any code loaded before code tracking is enabled will not be instrumentable via line probes.

Normally tracking should remain active for the lifetime of the process and would not be ever stopped.



36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
# File 'lib/datadog/di/code_tracker.rb', line 36

def start
  trace_point_lock.synchronize do
    # If this code tracker is already running, we can do nothing or
    # restart it (by disabling the trace point and recreating it).
    # It is likely that some applications will attempt to activate
    # DI more than once where the intention is to just activate DI;
    # do not break such applications by clearing out the registry.
    # For now, until there is a use case for recreating the trace point,
    # do nothing if the code tracker has already started.
    return if @compiled_trace_point

    # Note: .trace enables the trace point.
    @compiled_trace_point = TracePoint.trace(:script_compiled) do |tp|
      # Useful attributes of the trace point object here:
      # .instruction_sequence
      # .instruction_sequence.path (either absolute file path for
      #   loaded or required code, or for eval'd code, if filename
      #   is specified as argument to eval, then this is the provided
      #   filename, otherwise this is a synthesized
      #   "(eval at <definition-file>:<line>)" string)
      # .instruction_sequence.absolute_path (absolute file path when
      #   load or require are used to load code, nil for eval'd code
      #   regardless of whether filename was specified as an argument
      #   to eval on ruby 3.1+, same as path for eval'd code on ruby 3.0
      #   and lower)
      # .method_id
      # .path (refers to the code location that called the require/eval/etc.,
      #   not where the loaded code is; use .path on the instruction sequence
      #   to obtain the location of the compiled code)
      # .eval_script
      #
      # For now just map the path to the instruction sequence.
      path = tp.instruction_sequence.absolute_path
      # Do not store mapping for eval'd code, since there is no way
      # to target such code from dynamic instrumentation UI.
      # eval'd code always sets tp.eval_script.
      # When tp.eval_script is nil, code is either 'load'ed or 'require'd.
      # steep, of course, complains about indexing with +path+
      # without checking that it is not nil, so here, maybe there is
      # some situation where path would in fact be nil and
      # steep would end up saving the day.
      if path && !tp.eval_script
        registry_lock.synchronize do
          registry[path] = tp.instruction_sequence
        end

        # Also, pending line probes should only be installed for
        # non-eval'd code.
        DI.current_component&.probe_manager&.install_pending_line_probes(path)
      end
    # Since this method normally is called from customer applications,
    # rescue any exceptions that might not be handled to not break said
    # customer applications.
    rescue => exc
      # TODO we do not have DI.component defined yet, remove steep:ignore
      # before release.
      if component = DI.current_component # steep:ignore
        raise if component.settings.dynamic_instrumentation.internal.propagate_all_exceptions
        component.logger.warn("Unhandled exception in script_compiled trace point: #{exc.class}: #{exc}")
        component.telemetry&.report(exc, description: "Unhandled exception in script_compiled trace point")
        # TODO test this path
      else
        # If we don't have a component, we cannot log anything properly.
        # Do not just print a warning to avoid spamming customer logs.
        # Don't reraise the exception either.
        # TODO test this path
      end
    end
  end
end

#stopObject

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.

Stops tracking code that is being loaded.

This method should ordinarily never be called - if a file is loaded when code tracking is not active, this file will not be instrumentable by line probes.

This method is intended for test suite use only, where multiple code tracker instances are created, to fully clean up the old instances.



161
162
163
164
165
166
167
168
169
170
# File 'lib/datadog/di/code_tracker.rb', line 161

def stop
  # Permit multiple stop calls.
  trace_point_lock.synchronize do
    @compiled_trace_point&.disable
    # Clear the instance variable so that the trace point may be
    # reinstated in the future.
    @compiled_trace_point = nil
  end
  clear
end