instrument_all_the_things 1.0.2

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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +14 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +95 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +7 -0
  8. data/Gemfile +4 -0
  9. data/Gemfile.lock +74 -0
  10. data/README.md +371 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +11 -0
  13. data/bin/setup +8 -0
  14. data/instrument_all_the_things.gemspec +42 -0
  15. data/lib/instrument_all_the_things.rb +70 -0
  16. data/lib/instrument_all_the_things/clients/stat_reporter/datadog.rb +12 -0
  17. data/lib/instrument_all_the_things/clients/tracer/blackhole.rb +22 -0
  18. data/lib/instrument_all_the_things/context.rb +23 -0
  19. data/lib/instrument_all_the_things/helpers.rb +55 -0
  20. data/lib/instrument_all_the_things/instrumentors/all.rb +6 -0
  21. data/lib/instrument_all_the_things/instrumentors/error_logging.rb +48 -0
  22. data/lib/instrument_all_the_things/instrumentors/execution_count_and_timing.rb +20 -0
  23. data/lib/instrument_all_the_things/instrumentors/gc_stats.rb +49 -0
  24. data/lib/instrument_all_the_things/instrumentors/tracing.rb +30 -0
  25. data/lib/instrument_all_the_things/method_instrumentor.rb +57 -0
  26. data/lib/instrument_all_the_things/method_proxy.rb +54 -0
  27. data/lib/instrument_all_the_things/testing/rspec_matchers.rb +97 -0
  28. data/lib/instrument_all_the_things/testing/stat_tracker.rb +43 -0
  29. data/lib/instrument_all_the_things/testing/trace_tracker.rb +25 -0
  30. data/lib/instrument_all_the_things/version.rb +5 -0
  31. data/logo.jpg +0 -0
  32. data/vendor/cache/ast-2.4.0.gem +0 -0
  33. data/vendor/cache/benchmark-ips-2.7.2.gem +0 -0
  34. data/vendor/cache/coderay-1.1.2.gem +0 -0
  35. data/vendor/cache/ddtrace-0.33.0.gem +0 -0
  36. data/vendor/cache/diff-lcs-1.3.gem +0 -0
  37. data/vendor/cache/docile-1.3.2.gem +0 -0
  38. data/vendor/cache/dogstatsd-ruby-4.7.0.gem +0 -0
  39. data/vendor/cache/jaro_winkler-1.5.4.gem +0 -0
  40. data/vendor/cache/method_source-0.9.2.gem +0 -0
  41. data/vendor/cache/msgpack-1.3.3.gem +0 -0
  42. data/vendor/cache/parallel-1.19.1.gem +0 -0
  43. data/vendor/cache/parser-2.7.0.2.gem +0 -0
  44. data/vendor/cache/pry-0.12.2.gem +0 -0
  45. data/vendor/cache/rainbow-3.0.0.gem +0 -0
  46. data/vendor/cache/rake-10.5.0.gem +0 -0
  47. data/vendor/cache/rexml-3.2.4.gem +0 -0
  48. data/vendor/cache/rspec-3.9.0.gem +0 -0
  49. data/vendor/cache/rspec-core-3.9.1.gem +0 -0
  50. data/vendor/cache/rspec-expectations-3.9.0.gem +0 -0
  51. data/vendor/cache/rspec-mocks-3.9.1.gem +0 -0
  52. data/vendor/cache/rspec-support-3.9.2.gem +0 -0
  53. data/vendor/cache/rubocop-0.80.0.gem +0 -0
  54. data/vendor/cache/ruby-progressbar-1.10.1.gem +0 -0
  55. data/vendor/cache/simplecov-0.18.1.gem +0 -0
  56. data/vendor/cache/simplecov-html-0.11.0.gem +0 -0
  57. data/vendor/cache/unicode-display_width-1.6.1.gem +0 -0
  58. metadata +227 -0
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/statsd'
4
+
5
+ module InstrumentAllTheThings
6
+ module Clients
7
+ module StatReporter
8
+ class DataDog < Datadog::Statsd
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './tracing'
4
+ require_relative './error_logging'
5
+ require_relative './gc_stats'
6
+ require_relative './execution_count_and_timing'
@@ -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