instrument_all_the_things 0.9.0.alpha

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 +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 +369 -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/clients/stat_reporter/datadog.rb +12 -0
  16. data/lib/instrument_all_the_things/clients/tracer/blackhole.rb +22 -0
  17. data/lib/instrument_all_the_things/context.rb +23 -0
  18. data/lib/instrument_all_the_things/helpers.rb +55 -0
  19. data/lib/instrument_all_the_things/instrumentors/all.rb +6 -0
  20. data/lib/instrument_all_the_things/instrumentors/error_logging.rb +48 -0
  21. data/lib/instrument_all_the_things/instrumentors/execution_count_and_timing.rb +20 -0
  22. data/lib/instrument_all_the_things/instrumentors/gc_stats.rb +49 -0
  23. data/lib/instrument_all_the_things/instrumentors/tracing.rb +30 -0
  24. data/lib/instrument_all_the_things/method_instrumentor.rb +54 -0
  25. data/lib/instrument_all_the_things/method_proxy.rb +54 -0
  26. data/lib/instrument_all_the_things/testing/rspec_matchers.rb +97 -0
  27. data/lib/instrument_all_the_things/testing/stat_tracker.rb +43 -0
  28. data/lib/instrument_all_the_things/testing/trace_tracker.rb +25 -0
  29. data/lib/instrument_all_the_things/version.rb +3 -0
  30. data/lib/instrument_all_the_things.rb +70 -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.32.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.6.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,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
+ IATT.logger&.error("An error occurred in #{context.trace_name(klass)}")
38
+ IATT.logger&.error(e.message)
39
+
40
+ callstack = backtrace_cleaner.call(e.backtrace || [])
41
+
42
+ IATT.logger&.error(callstack.join("\n"))
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
+ IATT.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 = IATT.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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './instrumentors/all'
4
+
5
+ module InstrumentAllTheThings
6
+ class MethodInstrumentor
7
+ WAPPERS = {
8
+ trace: Instrumentors::TRACE_WRAPPER,
9
+ error_logging: Instrumentors::ERROR_LOGGING_WRAPPER,
10
+ gc_stats: Instrumentors::GC_STATS_WRAPPER,
11
+ execution_counts_and_timing: Instrumentors::EXECUTION_COUNT_AND_TIMING_WRAPPER,
12
+ }.freeze
13
+
14
+ DEFAULT_OPTIONS = {
15
+ trace: true,
16
+ gc_stats: true,
17
+ error_logging: true,
18
+ }.freeze
19
+
20
+ attr_accessor :options, :instrumentor
21
+
22
+ def initialize(options)
23
+ self.options = DEFAULT_OPTIONS.merge(options)
24
+
25
+ build_instrumentor
26
+
27
+ freeze
28
+ end
29
+
30
+ def build_instrumentor
31
+ procs = WAPPERS.collect do |type, builder|
32
+ next unless options[type]
33
+
34
+ builder.call(options[type], options[:context])
35
+ end.compact
36
+
37
+ self.instrumentor = combine_procs(procs)
38
+ end
39
+
40
+ def invoke(klass:, &blk)
41
+ instrumentor.call(klass, blk)
42
+ end
43
+
44
+ private
45
+
46
+ def combine_procs(procs)
47
+ # I know it's crazy, but this wraps procs which take "Next Block"
48
+ # and "Final Block"
49
+ procs.inject(->(_, f) { f.call }) do |next_blk, current_blk|
50
+ proc { |k, final| current_blk.call(k, next_blk, final) }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative './method_instrumentor'
4
+
5
+ module InstrumentAllTheThings
6
+ module MethodProxy
7
+ def self.for_class(klass)
8
+ find_for_class(klass) || install_on_class(klass)
9
+ end
10
+
11
+ def self.find_for_class(klass)
12
+ klass.ancestors.detect do |a|
13
+ a.is_a?(Instrumentor) &&
14
+ a._iatt_built_for == klass
15
+ end
16
+ end
17
+
18
+ def self.install_on_class(klass)
19
+ construct_for_class(klass).tap do |m|
20
+ klass.prepend(m)
21
+ end
22
+ end
23
+
24
+ def self.construct_for_class(klass)
25
+ Module.new do
26
+ extend Instrumentor
27
+ end.tap { |m| m._iatt_built_for = klass }
28
+ end
29
+
30
+ module Instrumentor
31
+ def inspect
32
+ "InstrumentAllTheThings::#{@_iatt_built_for}Proxy"
33
+ end
34
+
35
+ def _iatt_built_for
36
+ @_iatt_built_for
37
+ end
38
+
39
+ def _iatt_built_for=(val)
40
+ @_iatt_built_for = val
41
+ end
42
+
43
+ def wrap_implementation(method_name, settings)
44
+ wrap = MethodInstrumentor.new(**settings)
45
+
46
+ define_method(method_name) do |*args, **kwargs, &blk|
47
+ wrap.invoke(klass: is_a?(Class) ? self : self.class) do
48
+ super(*args, **kwargs, &blk)
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstrumentAllTheThings
4
+ module Testing
5
+ module RSpecMatchers
6
+ def histogram_value(counter_name)
7
+ stats = IATT.stat_reporter.emitted_values[:histogram][counter_name]
8
+ stats.inject(0){|l, n| l + n[:args][0] }
9
+ end
10
+
11
+ def distribution_values(distribution_name, with_tags: nil)
12
+ stats = IATT.stat_reporter.emitted_values[:distribution][distribution_name]
13
+
14
+ if with_tags && !stats.empty?
15
+ stats = stats.select do |s|
16
+ with_tags.all?{|t| s[:tags].include?(t) }
17
+ end
18
+ end
19
+
20
+ stats&.map{|i| i[:args] }&.map(&:first) || []
21
+ end
22
+
23
+ def histogram_values(histogram_name, with_tags: nil)
24
+ stats = IATT.stat_reporter.emitted_values[:histogram][histogram_name]
25
+
26
+ if with_tags && !stats.empty?
27
+ stats = stats.select do |s|
28
+ with_tags.all?{|t| s[:tags].include?(t) }
29
+ end
30
+ end
31
+
32
+ stats&.map{|i| i[:args] }&.map(&:first) || []
33
+ end
34
+
35
+ def timing_values(timing_name, with_tags: nil)
36
+ stats = IATT.stat_reporter.emitted_values[:timing][timing_name]
37
+
38
+ if with_tags && !stats.empty?
39
+ stats = stats.select do |s|
40
+ with_tags.all?{|t| s[:tags].include?(t) }
41
+ end
42
+ end
43
+
44
+ stats&.map{|i| i[:args] }&.map(&:first) || []
45
+ end
46
+
47
+ def set_value(counter_name, with_tags: nil)
48
+ stats = IATT.stat_reporter.emitted_values[:set][counter_name]
49
+
50
+ if with_tags && !stats.empty?
51
+ stats = stats.select do |s|
52
+ with_tags.all?{|t| s[:tags].include?(t) }
53
+ end
54
+ end
55
+
56
+ data = stats&.map{|i| i[:args] }&.map(&:first)
57
+ data ? data.uniq.length : 0
58
+ end
59
+
60
+ def gauge_value(counter_name, with_tags: nil)
61
+ stats = IATT.stat_reporter.emitted_values[:gauge][counter_name]
62
+
63
+ if with_tags && !stats.empty?
64
+ stats = stats.select do |s|
65
+ with_tags.all?{|t| s[:tags].include?(t) }
66
+ end
67
+ end
68
+ stats.last&.fetch(:args)&.first
69
+ end
70
+
71
+ def counter_value(counter_name, with_tags: nil)
72
+ stats = IATT.stat_reporter.emitted_values[:count][counter_name]
73
+ if with_tags && !stats.empty?
74
+ stats = stats.select do |s|
75
+ with_tags.all?{|t| s[:tags].include?(t) }
76
+ end
77
+ end
78
+ stats.inject(0){|l, n| l + n[:args][0] }
79
+ end
80
+
81
+ def flush_traces
82
+ Datadog.tracer&.writer&.worker&.flush_data
83
+ end
84
+
85
+ def emitted_spans(filtered_by: nil)
86
+ sleep 0.01
87
+ traces = IATT::Testing::TraceTracker.tracker.traces.map(&:dup)
88
+ if filtered_by
89
+ filtered_by.transform_keys!(&:to_s)
90
+ traces.select! { |t| filtered_by < t }
91
+ end
92
+
93
+ traces
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'datadog/statsd'
4
+
5
+ module InstrumentAllTheThings
6
+ module Testing
7
+ class StatTracker < Clients::StatReporter::DataDog
8
+ attr_reader :emitted_values
9
+ %i[
10
+ count
11
+ distribution
12
+ gauge
13
+ histogram
14
+ set
15
+ time
16
+ timing
17
+ ].each do |meth|
18
+ define_method(meth) do |*args, **kwargs, &blk|
19
+ @emitted_values[meth][args[0]] << {
20
+ args: args[1..-1],
21
+ tags: kwargs.fetch(:tags, []),
22
+ kwargs: kwargs,
23
+ }
24
+
25
+ super(*args, **kwargs, &blk)
26
+ end
27
+ end
28
+
29
+ def initialize(*args, **kwargs, &blk)
30
+ super
31
+ reset!
32
+ end
33
+
34
+ def reset!
35
+ @emitted_values = Hash.new do |h, k|
36
+ h[k] = Hash.new do |h2, k2|
37
+ h2[k2] = []
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InstrumentAllTheThings
4
+ module Testing
5
+ class TraceTracker
6
+ attr_reader :traces
7
+
8
+ def self.tracker
9
+ @tracker ||= new
10
+ end
11
+
12
+ def initialize
13
+ reset!
14
+ end
15
+
16
+ def reset!
17
+ @traces = []
18
+ end
19
+
20
+ def <<(val)
21
+ @traces = @traces.concat(MessagePack.load(val[:body]).flatten)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,3 @@
1
+ module InstrumentAllTheThings
2
+ VERSION = "0.9.0.alpha"
3
+ 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
data/logo.jpg ADDED
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file