instrument_all_the_things 1.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.drone.yml +14 -0
- data/.gitignore +12 -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 +371 -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 +20 -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 +54 -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.33.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, 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,20 @@
|
|
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
|
+
InstrumentAllTheThings.increment("#{context.stats_name(klass)}.executed")
|
10
|
+
|
11
|
+
InstrumentAllTheThings.time("#{context.stats_name(klass)}.duration") do
|
12
|
+
next_blk.call(klass, actual_code)
|
13
|
+
end
|
14
|
+
rescue
|
15
|
+
InstrumentAllTheThings.increment("#{context.stats_name(klass)}.errored")
|
16
|
+
raise
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
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: opts[: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
|