instrument_all_the_things 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.drone.yml +14 -0
  3. data/.gitignore +13 -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 +397 -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 +21 -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 +77 -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.34.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, :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,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,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