instrument_all_the_things 1.0.4
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.
- checksums.yaml +7 -0
- data/.drone.yml +14 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +95 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +74 -0
- data/README.md +397 -0
- data/Rakefile +6 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/instrument_all_the_things.gemspec +42 -0
- data/lib/instrument_all_the_things.rb +70 -0
- data/lib/instrument_all_the_things/clients/stat_reporter/datadog.rb +12 -0
- data/lib/instrument_all_the_things/clients/tracer/blackhole.rb +22 -0
- data/lib/instrument_all_the_things/context.rb +23 -0
- data/lib/instrument_all_the_things/helpers.rb +55 -0
- data/lib/instrument_all_the_things/instrumentors/all.rb +6 -0
- data/lib/instrument_all_the_things/instrumentors/error_logging.rb +48 -0
- data/lib/instrument_all_the_things/instrumentors/execution_count_and_timing.rb +21 -0
- data/lib/instrument_all_the_things/instrumentors/gc_stats.rb +49 -0
- data/lib/instrument_all_the_things/instrumentors/tracing.rb +30 -0
- data/lib/instrument_all_the_things/method_instrumentor.rb +57 -0
- data/lib/instrument_all_the_things/method_proxy.rb +77 -0
- data/lib/instrument_all_the_things/testing/rspec_matchers.rb +97 -0
- data/lib/instrument_all_the_things/testing/stat_tracker.rb +43 -0
- data/lib/instrument_all_the_things/testing/trace_tracker.rb +25 -0
- data/lib/instrument_all_the_things/version.rb +5 -0
- data/logo.jpg +0 -0
- data/vendor/cache/ast-2.4.0.gem +0 -0
- data/vendor/cache/benchmark-ips-2.7.2.gem +0 -0
- data/vendor/cache/coderay-1.1.2.gem +0 -0
- data/vendor/cache/ddtrace-0.34.0.gem +0 -0
- data/vendor/cache/diff-lcs-1.3.gem +0 -0
- data/vendor/cache/docile-1.3.2.gem +0 -0
- data/vendor/cache/dogstatsd-ruby-4.7.0.gem +0 -0
- data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
- data/vendor/cache/method_source-0.9.2.gem +0 -0
- data/vendor/cache/msgpack-1.3.3.gem +0 -0
- data/vendor/cache/parallel-1.19.1.gem +0 -0
- data/vendor/cache/parser-2.7.0.2.gem +0 -0
- data/vendor/cache/pry-0.12.2.gem +0 -0
- data/vendor/cache/rainbow-3.0.0.gem +0 -0
- data/vendor/cache/rake-10.5.0.gem +0 -0
- data/vendor/cache/rexml-3.2.4.gem +0 -0
- data/vendor/cache/rspec-3.9.0.gem +0 -0
- data/vendor/cache/rspec-core-3.9.1.gem +0 -0
- data/vendor/cache/rspec-expectations-3.9.0.gem +0 -0
- data/vendor/cache/rspec-mocks-3.9.1.gem +0 -0
- data/vendor/cache/rspec-support-3.9.2.gem +0 -0
- data/vendor/cache/rubocop-0.80.0.gem +0 -0
- data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
- data/vendor/cache/simplecov-0.18.1.gem +0 -0
- data/vendor/cache/simplecov-html-0.11.0.gem +0 -0
- data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
- metadata +227 -0
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "instrument_all_the_things"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
require "pry"
|
11
|
+
Pry.start
|
data/bin/setup
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
4
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
5
|
+
require 'instrument_all_the_things/version'
|
6
|
+
|
7
|
+
Gem::Specification.new do |spec|
|
8
|
+
spec.name = 'instrument_all_the_things'
|
9
|
+
spec.version = InstrumentAllTheThings::VERSION
|
10
|
+
spec.authors = ['Brian Malinconico']
|
11
|
+
spec.email = ['bmalinconico@terminus.com']
|
12
|
+
|
13
|
+
spec.summary = 'Make instrumentation with DataDog easy peasy'
|
14
|
+
spec.description = 'Wrappers to make instrumentation of methods easy and pleasant to read'
|
15
|
+
spec.homepage = 'https://github.com/GetTerminus/instrument-all-the-things'
|
16
|
+
|
17
|
+
spec.metadata['allowed_push_host'] = "https://www.rubygems.org"
|
18
|
+
|
19
|
+
spec.metadata['homepage_uri'] = spec.homepage
|
20
|
+
spec.metadata['source_code_uri'] = 'https://github.com/GetTerminus/instrument-all-the-things'
|
21
|
+
# spec.metadata['changelog_uri'] = 'http://google.com'
|
22
|
+
|
23
|
+
# Specify which files should be added to the gem when it is released.
|
24
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
25
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
26
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
27
|
+
end
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.add_dependency 'ddtrace'
|
33
|
+
spec.add_dependency 'dogstatsd-ruby'
|
34
|
+
|
35
|
+
spec.add_development_dependency 'bundler', '~> 2.0'
|
36
|
+
spec.add_development_dependency 'pry'
|
37
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
38
|
+
spec.add_development_dependency 'rspec', '~> 3.0'
|
39
|
+
spec.add_development_dependency 'simplecov'
|
40
|
+
spec.add_development_dependency 'rubocop'
|
41
|
+
spec.add_development_dependency 'benchmark-ips'
|
42
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ddtrace'
|
4
|
+
|
5
|
+
require 'instrument_all_the_things/version'
|
6
|
+
|
7
|
+
require_relative './instrument_all_the_things/helpers'
|
8
|
+
require_relative './instrument_all_the_things/clients/stat_reporter/datadog'
|
9
|
+
|
10
|
+
module InstrumentAllTheThings
|
11
|
+
class Error < StandardError; end
|
12
|
+
|
13
|
+
class << self
|
14
|
+
attr_accessor :stat_namespace
|
15
|
+
attr_writer :logger, :stat_reporter, :tracer
|
16
|
+
|
17
|
+
def logger
|
18
|
+
return @logger if defined?(@logger)
|
19
|
+
|
20
|
+
@logger ||= if defined?(Rails)
|
21
|
+
Rails.logger
|
22
|
+
elsif defined?(App) && App.respond_to?(:logger)
|
23
|
+
App.logger
|
24
|
+
else
|
25
|
+
require 'logger'
|
26
|
+
Logger.new(STDOUT)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def stat_reporter
|
31
|
+
return @stat_reporter if defined?(@stat_reporter)
|
32
|
+
|
33
|
+
@stat_reporter ||= Clients::StatReporter::DataDog.new(
|
34
|
+
ENV.fetch('DATADOG_HOST', 'localhost'),
|
35
|
+
ENV.fetch('DATADOG_PORT', 8125),
|
36
|
+
namespace: stat_namespace,
|
37
|
+
)
|
38
|
+
end
|
39
|
+
|
40
|
+
def tracer
|
41
|
+
return @tracer if defined?(@tracer)
|
42
|
+
|
43
|
+
@tracer ||= Datadog.tracer
|
44
|
+
end
|
45
|
+
|
46
|
+
%i[
|
47
|
+
increment
|
48
|
+
decrement
|
49
|
+
count
|
50
|
+
gauge
|
51
|
+
set
|
52
|
+
histogram
|
53
|
+
distribution
|
54
|
+
timing
|
55
|
+
time
|
56
|
+
].each do |method_name|
|
57
|
+
define_method(method_name) do |*args, **kwargs, &blk|
|
58
|
+
return unless stat_reporter
|
59
|
+
|
60
|
+
stat_reporter.public_send(method_name, *args, **kwargs, &blk)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.included(other)
|
66
|
+
other.include(Helpers)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
IATT = InstrumentAllTheThings unless defined?(IATT)
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module InstrumentAllTheThings
|
2
|
+
module Clients
|
3
|
+
class Blackhole
|
4
|
+
class Span
|
5
|
+
def initialize
|
6
|
+
end
|
7
|
+
|
8
|
+
def set_tag(name, value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
reset!
|
15
|
+
end
|
16
|
+
|
17
|
+
def trace(name, options)
|
18
|
+
yield Span.new
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstrumentAllTheThings
|
4
|
+
Context = Struct.new(:method_name, :instance, :tags, keyword_init: true) do
|
5
|
+
def stats_name(klass_or_instance)
|
6
|
+
@stats_name ||= [
|
7
|
+
class_name(klass_or_instance),
|
8
|
+
(instance ? 'instance' : 'class') + '_methods',
|
9
|
+
method_name,
|
10
|
+
].join('.')
|
11
|
+
end
|
12
|
+
|
13
|
+
def trace_name(klass_or_instance)
|
14
|
+
@trace_name ||= "#{class_name(klass_or_instance)}#{instance ? '.' : '#'}#{method_name}"
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def class_name(klass_or_instance)
|
20
|
+
klass_or_instance.is_a?(Class) ? klass_or_instance.to_s : klass_or_instance
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './method_proxy'
|
4
|
+
require_relative './context'
|
5
|
+
|
6
|
+
module InstrumentAllTheThings
|
7
|
+
module Helpers
|
8
|
+
module ClassMethods
|
9
|
+
def instrument(**kwargs)
|
10
|
+
@last_settings = kwargs
|
11
|
+
end
|
12
|
+
|
13
|
+
def _conscript_last_iatt_settings
|
14
|
+
@last_settings.tap { @last_settings = nil }
|
15
|
+
end
|
16
|
+
|
17
|
+
def singleton_method_added(method_name)
|
18
|
+
settings = _conscript_last_iatt_settings
|
19
|
+
|
20
|
+
return unless settings
|
21
|
+
|
22
|
+
settings[:context] = Context.new(
|
23
|
+
method_name: method_name,
|
24
|
+
instance: false
|
25
|
+
)
|
26
|
+
|
27
|
+
InstrumentAllTheThings::MethodProxy
|
28
|
+
.for_class(singleton_class)
|
29
|
+
.wrap_implementation(method_name, settings)
|
30
|
+
super
|
31
|
+
end
|
32
|
+
|
33
|
+
def method_added(method_name)
|
34
|
+
settings = _conscript_last_iatt_settings
|
35
|
+
|
36
|
+
return unless settings
|
37
|
+
|
38
|
+
settings[:context] = Context.new(
|
39
|
+
method_name: method_name,
|
40
|
+
instance: true
|
41
|
+
)
|
42
|
+
|
43
|
+
InstrumentAllTheThings::MethodProxy
|
44
|
+
.for_class(self)
|
45
|
+
.wrap_implementation(method_name, settings)
|
46
|
+
|
47
|
+
super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.included(other_class)
|
52
|
+
other_class.extend(ClassMethods)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './tracing'
|
4
|
+
require_relative './error_logging'
|
5
|
+
|
6
|
+
module InstrumentAllTheThings
|
7
|
+
module Instrumentors
|
8
|
+
DEFAULT_ERROR_LOGGING_OPTIONS = {
|
9
|
+
exclude_bundle_path: true,
|
10
|
+
rescue_class: StandardError,
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
ERROR_LOGGER = lambda do |exception, backtrace_cleaner|
|
14
|
+
end
|
15
|
+
|
16
|
+
ERROR_LOGGING_WRAPPER = lambda do |opts, context|
|
17
|
+
opts = if opts == true
|
18
|
+
DEFAULT_ERROR_LOGGING_OPTIONS
|
19
|
+
else
|
20
|
+
DEFAULT_ERROR_LOGGING_OPTIONS.merge(opts)
|
21
|
+
end
|
22
|
+
|
23
|
+
backtrace_cleaner = if opts[:exclude_bundle_path] && defined?(Bundler)
|
24
|
+
bundle_path = Bundler.bundle_path.to_s
|
25
|
+
->(trace) { trace.reject { |p| p.start_with?(bundle_path) } }
|
26
|
+
else
|
27
|
+
->(trace) { trace }
|
28
|
+
end
|
29
|
+
|
30
|
+
lambda do |klass, next_blk, actual_code|
|
31
|
+
next_blk.call(klass, actual_code)
|
32
|
+
rescue opts[:rescue_class] => e
|
33
|
+
raise if e.instance_variable_get(:@_logged_by_iatt)
|
34
|
+
|
35
|
+
e.instance_variable_set(:@_logged_by_iatt, true)
|
36
|
+
|
37
|
+
InstrumentAllTheThings.logger&.error <<~ERROR
|
38
|
+
An error occurred in #{context.trace_name(klass)}
|
39
|
+
|
40
|
+
#{e.message}
|
41
|
+
#{backtrace_cleaner.call(e.backtrace || []).join("\n")}
|
42
|
+
ERROR
|
43
|
+
|
44
|
+
raise
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstrumentAllTheThings
|
4
|
+
module Instrumentors
|
5
|
+
DEFAULT_EXECUTION_COUNT_AND_TIMING_OPTIONS = {}.freeze
|
6
|
+
|
7
|
+
EXECUTION_COUNT_AND_TIMING_WRAPPER = proc do |_opts, context|
|
8
|
+
proc do |klass, next_blk, actual_code|
|
9
|
+
context.tags ||= []
|
10
|
+
|
11
|
+
InstrumentAllTheThings.increment("#{context.stats_name(klass)}.executed", { tags: context.tags })
|
12
|
+
InstrumentAllTheThings.time("#{context.stats_name(klass)}.duration", { tags: context.tags }) do
|
13
|
+
next_blk.call(klass, actual_code)
|
14
|
+
end
|
15
|
+
rescue StandardError
|
16
|
+
InstrumentAllTheThings.increment("#{context.stats_name(klass)}.errored", { tags: context.tags })
|
17
|
+
raise
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstrumentAllTheThings
|
4
|
+
module Instrumentors
|
5
|
+
DEFAULT_GC_STATS_OPTIONS = {
|
6
|
+
diffed_stats: %i[
|
7
|
+
total_allocated_pages
|
8
|
+
total_allocated_objects
|
9
|
+
count
|
10
|
+
].freeze
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
# This is to make it easier to spec since other
|
14
|
+
# gems may call this
|
15
|
+
GC_STAT_GETTER = -> { GC.stat }
|
16
|
+
|
17
|
+
GC_STATS_WRAPPER = lambda do |opts, context|
|
18
|
+
opts = if opts == true
|
19
|
+
DEFAULT_GC_STATS_OPTIONS
|
20
|
+
else
|
21
|
+
DEFAULT_GC_STATS_OPTIONS.merge(opts)
|
22
|
+
end
|
23
|
+
|
24
|
+
report_value = proc do |klass, stat_name, value|
|
25
|
+
InstrumentAllTheThings.stat_reporter.histogram(
|
26
|
+
context.stats_name(klass) + ".#{stat_name}_change",
|
27
|
+
value
|
28
|
+
)
|
29
|
+
end
|
30
|
+
|
31
|
+
lambda do |klass, next_blk, actual_code|
|
32
|
+
starting_values = GC_STAT_GETTER.call.slice(*opts[:diffed_stats])
|
33
|
+
next_blk.call(klass, actual_code).tap do
|
34
|
+
new_values = GC_STAT_GETTER.call.slice(*opts[:diffed_stats])
|
35
|
+
|
36
|
+
diff = new_values.merge(starting_values) do |_, new_value, starting_value|
|
37
|
+
new_value - starting_value
|
38
|
+
end
|
39
|
+
|
40
|
+
if (span = InstrumentAllTheThings.tracer.active_span)
|
41
|
+
span.set_tag('gc_stats', diff)
|
42
|
+
end
|
43
|
+
|
44
|
+
diff.each { |s, v| report_value.call(klass, s, v) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module InstrumentAllTheThings
|
4
|
+
module Instrumentors
|
5
|
+
DEFAULT_TRACE_OPTIONS = {
|
6
|
+
service: '',
|
7
|
+
span_type: '',
|
8
|
+
tags: {},
|
9
|
+
span_name: 'method.execution'
|
10
|
+
}.freeze
|
11
|
+
|
12
|
+
TRACE_WRAPPER = proc do |opts, context|
|
13
|
+
opts = if opts == true
|
14
|
+
DEFAULT_TRACE_OPTIONS
|
15
|
+
else
|
16
|
+
DEFAULT_TRACE_OPTIONS.merge(opts)
|
17
|
+
end
|
18
|
+
|
19
|
+
proc do |klass, next_blk, actual_code|
|
20
|
+
InstrumentAllTheThings.tracer.trace(
|
21
|
+
opts[:span_name],
|
22
|
+
tags: context[:tags] || {},
|
23
|
+
service: opts[:service],
|
24
|
+
resource: opts[:resource] || context.trace_name(klass),
|
25
|
+
span_type: opts[:span_type]
|
26
|
+
) { next_blk.call(klass, actual_code) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative './instrumentors/all'
|
4
|
+
|
5
|
+
module InstrumentAllTheThings
|
6
|
+
class MethodInstrumentor
|
7
|
+
WRAPPERS = {
|
8
|
+
# Note that the order of these hash keys are applied top to bottom, with the first inserted key
|
9
|
+
# being the inner most wrapper
|
10
|
+
gc_stats: Instrumentors::GC_STATS_WRAPPER,
|
11
|
+
error_logging: Instrumentors::ERROR_LOGGING_WRAPPER,
|
12
|
+
execution_counts_and_timing: Instrumentors::EXECUTION_COUNT_AND_TIMING_WRAPPER,
|
13
|
+
trace: Instrumentors::TRACE_WRAPPER,
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
DEFAULT_OPTIONS = {
|
17
|
+
trace: true,
|
18
|
+
gc_stats: true,
|
19
|
+
error_logging: true,
|
20
|
+
execution_counts_and_timing: true
|
21
|
+
}.freeze
|
22
|
+
|
23
|
+
attr_accessor :options, :instrumentor
|
24
|
+
|
25
|
+
def initialize(options)
|
26
|
+
self.options = DEFAULT_OPTIONS.merge(options)
|
27
|
+
|
28
|
+
build_instrumentor
|
29
|
+
|
30
|
+
freeze
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_instrumentor
|
34
|
+
procs = WRAPPERS.collect do |type, builder|
|
35
|
+
next unless options[type]
|
36
|
+
|
37
|
+
builder.call(options[type], options[:context])
|
38
|
+
end.compact
|
39
|
+
|
40
|
+
self.instrumentor = combine_procs(procs)
|
41
|
+
end
|
42
|
+
|
43
|
+
def invoke(klass:, &blk)
|
44
|
+
instrumentor.call(klass, blk)
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def combine_procs(procs)
|
50
|
+
# I know it's crazy, but this wraps procs which take "Next Block"
|
51
|
+
# and "Final Block"
|
52
|
+
procs.inject(->(_, f) { f.call }) do |next_blk, current_blk|
|
53
|
+
proc { |k, final| current_blk.call(k, next_blk, final) }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|