-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DEBUG-2334 dynamic instrumentation Probe class
- Loading branch information
Showing
5 changed files
with
377 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
# frozen_string_literal: true | ||
|
||
module Datadog | ||
module DI | ||
# Base class for Dynamic Instrumentation exceptions. | ||
# | ||
# @api public | ||
class Error < StandardError | ||
# Probe does not contain a line number (i.e., is not a line probe). | ||
class MissingLineNumber < Error | ||
end | ||
|
||
# Failed to communicate to the local Datadog agent (e.g. to send | ||
# probe status or a snapshot). | ||
class AgentCommunicationError < Error | ||
end | ||
|
||
# Attempting to instrument a method or file which does not exist. | ||
# | ||
# This could be due to the code that is referenced in the probe | ||
# having not been loaded yet, or due to the probe referencing code | ||
# that does not in fact exist anywhere (e.g. due to a misspelling). | ||
class DITargetNotDefined < Error | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
# frozen_string_literal: true | ||
|
||
require_relative "error" | ||
require_relative "../core/rate_limiter" | ||
|
||
module Datadog | ||
module DI | ||
# Encapsulates probe information (as received via remote config) | ||
# and state (e.g. whether the probe was installed, or executed). | ||
# | ||
# It is possible that remote configuration will specify an unsupported | ||
# probe type or attribute, due to new DI functionality being added | ||
# over time. We want to have predictable behavior in such cases, and | ||
# since we can't guarantee that there will be enough information in | ||
# a remote config payload to construct a functional probe, ProbeBuilder | ||
# and remote config code must be prepared to deal with exceptions | ||
# raised by Probe constructor in particular. Therefore, Probe constructor | ||
# will raise an exception if it determines that there is not enough | ||
# information (or confilcting information) in the arguments to create a | ||
# functional probe, and upstream code is tasked with not spamming logs | ||
# with notifications of such errors (and potentially limiting the | ||
# attempts to construct probe from a given payload). | ||
# | ||
# Note that, while remote configuration provides line numbers as an | ||
# array, the only supported line number configuration is a single line | ||
# (this is the case for all languages currently). Therefore Probe | ||
# only supports one line number, and ProbeBuilder is responsible for | ||
# extracting that one line number out of the array received from RC. | ||
# | ||
# Note: only some of the parameter/attribute values are currently validated. | ||
# | ||
# @api private | ||
class Probe | ||
def initialize(id:, type:, | ||
file: nil, line_no: nil, type_name: nil, method_name: nil, | ||
template: nil, capture_snapshot: false) | ||
# Perform some sanity checks here to detect unexpected attribute | ||
# combinations, in order to not do them in subsequent code. | ||
if line_no && method_name | ||
raise ArgumentError, "Probe contains both line number and method name: #{id}" | ||
end | ||
|
||
if type_name && !method_name || method_name && !type_name | ||
raise ArgumentError, "Partial method probe definition: #{id}" | ||
end | ||
|
||
@id = id | ||
@type = type | ||
@file = file | ||
@line_no = line_no | ||
@type_name = type_name | ||
@method_name = method_name | ||
@template = template | ||
@capture_snapshot = !!capture_snapshot | ||
|
||
# These checks use instance methods that have more complex logic | ||
# than checking a single argument value. To avoid duplicating | ||
# the logic here, use the methods and perform these checks after | ||
# instance variable assignment. | ||
unless method? || line? | ||
raise ArgumentError, "Unhandled probe type: neither method nor line probe: #{id}" | ||
end | ||
|
||
@rate_limiter = if capture_snapshot | ||
Datadog::Core::TokenBucket.new(1) | ||
else | ||
Datadog::Core::TokenBucket.new(5000) | ||
end | ||
end | ||
|
||
attr_reader :id | ||
attr_reader :type | ||
attr_reader :file | ||
attr_reader :line_no | ||
attr_reader :type_name | ||
attr_reader :method_name | ||
attr_reader :template | ||
|
||
# For internal DI use only | ||
attr_reader :rate_limiter | ||
|
||
def capture_snapshot? | ||
@capture_snapshot | ||
end | ||
|
||
# Returns whether the probe is a line probe. | ||
# | ||
# Method probes may still specify a file name (to aid in locating the | ||
# method or for stack traversal purposes?), therefore we do not check | ||
# for file name/path presence here and just consider the line number. | ||
def line? | ||
!line_no.nil? | ||
end | ||
|
||
# Returns whether the probe is a method probe. | ||
def method? | ||
!!(type_name && method_name) | ||
end | ||
|
||
# Returns the line number associated with the probe, raising | ||
# Error::MissingLineNumber if the probe does not have a line number | ||
# associated with it. | ||
# | ||
# This method is used by instrumentation driver to ensure a line number | ||
# that is passed into the instrumentation logic is actually a line number | ||
# and not nil. | ||
def line_no! | ||
if line_no.nil? | ||
raise Error::MissingLineNumber, "Probe #{id} does not have a line number associated with it" | ||
end | ||
line_no | ||
end | ||
|
||
# Source code location of the probe, for diagnostic reporting. | ||
def location | ||
if method? | ||
"#{type_name}.#{method_name}" | ||
elsif line? | ||
"#{file}:#{line_no}" | ||
else | ||
# This case should not be possible because constructor verifies that | ||
# the probe is a method or a line probe. | ||
raise NotImplementedError | ||
end | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
module Datadog | ||
module DI | ||
class Error < StandardError | ||
class MissingLineNumber < Error | ||
end | ||
class AgentCommunicationError < Error | ||
end | ||
class DITargetNotDefined < Error | ||
end | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
module Datadog | ||
module DI | ||
class Probe | ||
@id: String | ||
|
||
@type: String | ||
|
||
@file: String? | ||
|
||
@line_no: Integer? | ||
|
||
@type_name: String? | ||
|
||
@method_name: String? | ||
|
||
@template: String | ||
|
||
@capture_snapshot: bool | ||
|
||
@rate_limiter: Datadog::Core::RateLimiter | ||
|
||
def initialize: (id: String, type: String, ?file: String?, ?line_no: Integer?, ?type_name: String?, ?method_name: String?, ?template: String?, ?capture_snapshot: bool) -> void | ||
|
||
attr_reader id: String | ||
|
||
attr_reader type: String | ||
|
||
attr_reader file: String? | ||
|
||
attr_reader line_no: Integer? | ||
|
||
attr_reader type_name: String? | ||
|
||
attr_reader method_name: String? | ||
|
||
attr_reader template: String | ||
attr_reader rate_limiter: Datadog::Core::RateLimiter | ||
|
||
def capture_snapshot?: () -> bool | ||
def line?: () -> bool | ||
def method?: () -> bool | ||
def line_no!: () -> Integer | ||
def location: () -> ::String | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,164 @@ | ||
require "datadog/di/probe" | ||
|
||
RSpec.describe Datadog::DI::Probe do | ||
describe ".new" do | ||
context "method probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", type_name: "foo", method_name: "bar") | ||
end | ||
|
||
it "creates an instance" do | ||
expect(probe).to be_a(described_class) | ||
expect(probe.id).to eq "42" | ||
expect(probe.type).to eq "foo" | ||
expect(probe.type_name).to eq "foo" | ||
expect(probe.method_name).to eq "bar" | ||
expect(probe.file).to be nil | ||
expect(probe.line_no).to be nil | ||
end | ||
end | ||
|
||
context "line probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", file: "foo", line_no: 4) | ||
end | ||
|
||
it "creates an instance" do | ||
expect(probe).to be_a(described_class) | ||
expect(probe.id).to eq "42" | ||
expect(probe.type).to eq "foo" | ||
expect(probe.type_name).to be nil | ||
expect(probe.method_name).to be nil | ||
expect(probe.file).to eq "foo" | ||
expect(probe.line_no).to eq 4 | ||
end | ||
end | ||
|
||
context "neither method nor line" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo") | ||
end | ||
|
||
it "raises ArgumentError" do | ||
expect do | ||
probe | ||
end.to raise_error(ArgumentError, /neither method nor line/) | ||
end | ||
end | ||
|
||
context "both method and line" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", | ||
type_name: "foo", method_name: "bar", file: "baz", line_no: 4) | ||
end | ||
|
||
it "raises ArgumentError" do | ||
expect do | ||
probe | ||
end.to raise_error(ArgumentError, /both line number and method name/) | ||
end | ||
end | ||
end | ||
|
||
describe "#line?" do | ||
context "line probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", file: "bar.rb", line_no: 5) | ||
end | ||
|
||
it "is true" do | ||
expect(probe.line?).to be true | ||
end | ||
end | ||
|
||
context "method probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", type_name: "FooClass", method_name: "bar") | ||
end | ||
|
||
it "is false" do | ||
expect(probe.line?).to be false | ||
end | ||
end | ||
|
||
context "method probe with file name" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", type_name: "FooClass", method_name: "bar", file: "quux.rb") | ||
end | ||
|
||
it "is false" do | ||
expect(probe.line?).to be false | ||
end | ||
end | ||
end | ||
|
||
describe "#method?" do | ||
context "line probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", file: "bar.rb", line_no: 5) | ||
end | ||
|
||
it "is false" do | ||
expect(probe.method?).to be false | ||
end | ||
end | ||
|
||
context "method probe" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", type_name: "FooClass", method_name: "bar") | ||
end | ||
|
||
it "is true" do | ||
expect(probe.method?).to be true | ||
end | ||
end | ||
|
||
context "method probe with file name" do | ||
let(:probe) do | ||
described_class.new(id: "42", type: "foo", type_name: "FooClass", method_name: "bar", file: "quux.rb") | ||
end | ||
|
||
it "is true" do | ||
expect(probe.method?).to be true | ||
end | ||
end | ||
end | ||
|
||
describe "#line_no" do | ||
context "one line number" do | ||
let(:probe) { described_class.new(id: "x", type: "log", line_no: 5) } | ||
|
||
it "returns the line number" do | ||
expect(probe.line_no).to eq 5 | ||
end | ||
end | ||
|
||
context "nil line number" do | ||
let(:probe) { described_class.new(id: "id", type: "LOG", type_name: "x", method_name: "y", line_no: nil) } | ||
|
||
it "returns nil" do | ||
expect(probe.line_no).to be nil | ||
end | ||
end | ||
end | ||
|
||
describe "#line_no!" do | ||
context "one line number" do | ||
let(:probe) { described_class.new(id: "x", type: "log", line_no: 5) } | ||
|
||
it "returns the line number" do | ||
expect(probe.line_no!).to eq 5 | ||
end | ||
end | ||
|
||
context "nil line number" do | ||
let(:probe) { described_class.new(id: "id", type: "LOG", type_name: "x", method_name: "y", line_no: nil) } | ||
|
||
it "raises MissingLineNumber" do | ||
expect do | ||
probe.line_no! | ||
end.to raise_error(Datadog::DI::Error::MissingLineNumber, /does not have a line number/) | ||
end | ||
end | ||
end | ||
end |