metrician 0.0.1

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +65 -0
  5. data/.rubocop_todo.yml +24 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +36 -0
  8. data/Gemfile +1 -0
  9. data/METRICS.MD +48 -0
  10. data/README.md +77 -0
  11. data/Rakefile +12 -0
  12. data/config/metrician.yaml +136 -0
  13. data/gemfiles/Gemfile.4.2.sidekiq-4 +24 -0
  14. data/gemfiles/Gemfile.4.2.sidekiq-4.lock +173 -0
  15. data/gemfiles/Gemfile.5.0.pg +24 -0
  16. data/gemfiles/Gemfile.5.0.pg.lock +182 -0
  17. data/lib/metrician.rb +80 -0
  18. data/lib/metrician/configuration.rb +33 -0
  19. data/lib/metrician/jobs.rb +32 -0
  20. data/lib/metrician/jobs/delayed_job_callbacks.rb +33 -0
  21. data/lib/metrician/jobs/resque_plugin.rb +36 -0
  22. data/lib/metrician/jobs/sidekiq_middleware.rb +28 -0
  23. data/lib/metrician/middleware.rb +64 -0
  24. data/lib/metrician/middleware/application_timing.rb +29 -0
  25. data/lib/metrician/middleware/request_timing.rb +152 -0
  26. data/lib/metrician/reporter.rb +41 -0
  27. data/lib/metrician/reporters/active_record.rb +63 -0
  28. data/lib/metrician/reporters/delayed_job.rb +17 -0
  29. data/lib/metrician/reporters/honeybadger.rb +26 -0
  30. data/lib/metrician/reporters/memcache.rb +49 -0
  31. data/lib/metrician/reporters/method_tracer.rb +70 -0
  32. data/lib/metrician/reporters/middleware.rb +22 -0
  33. data/lib/metrician/reporters/net_http.rb +28 -0
  34. data/lib/metrician/reporters/redis.rb +31 -0
  35. data/lib/metrician/reporters/resque.rb +17 -0
  36. data/lib/metrician/reporters/sidekiq.rb +19 -0
  37. data/lib/metrician/version.rb +5 -0
  38. data/metrician.gemspec +25 -0
  39. data/script/setup +72 -0
  40. data/script/test +36 -0
  41. data/spec/metrician_spec.rb +372 -0
  42. data/spec/spec_helper.rb +23 -0
  43. data/spec/support/database.rb +33 -0
  44. data/spec/support/database.sample.yml +10 -0
  45. data/spec/support/database.travis.yml +9 -0
  46. data/spec/support/models.rb +2 -0
  47. data/spec/support/test_delayed_job.rb +12 -0
  48. data/spec/support/test_resque_job.rb +8 -0
  49. data/spec/support/test_sidekiq_worker.rb +8 -0
  50. metadata +188 -0
@@ -0,0 +1,33 @@
1
+ require "yaml"
2
+
3
+ module Metrician
4
+ class Configuration
5
+ FileMissing = Class.new(StandardError)
6
+
7
+ def self.load
8
+ if env_location
9
+ # this should never raise unless a bad ENV setting has been set
10
+ raise(FileMissing.new(env_location)) unless File.exist?(env_location)
11
+ return YAML.load(env_location)
12
+ end
13
+
14
+ if File.exist?(app_location)
15
+ return YAML.load_file(app_location)
16
+ end
17
+
18
+ YAML.load_file(gem_location)
19
+ end
20
+
21
+ def self.env_location
22
+ ENV["METRICIAN_CONFIG"]
23
+ end
24
+
25
+ def self.app_location
26
+ File.join(Dir.pwd, "config", "metrician.yaml")
27
+ end
28
+
29
+ def self.gem_location
30
+ File.expand_path("../../../config/metrician.yaml", __FILE__)
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,32 @@
1
+ module Metrician
2
+ module Jobs
3
+
4
+ RUN_METRIC = "jobs.run".freeze
5
+ ERROR_METRIC = "jobs.error".freeze
6
+
7
+ def self.configuration
8
+ @configuration ||= Metrician.configuration[:jobs]
9
+ end
10
+
11
+ def self.enabled?
12
+ @enabled ||= configuration[:enabled]
13
+ end
14
+
15
+ def self.run?
16
+ @run ||= configuration[:run][:enabled]
17
+ end
18
+
19
+ def self.error?
20
+ @error ||= configuration[:error][:enabled]
21
+ end
22
+
23
+ def self.job_specific?
24
+ @job_specific ||= configuration[:job_specific][:enabled]
25
+ end
26
+
27
+ def self.instrumentation_name(job_name)
28
+ job_name.gsub(/[^\w]+/, ".").gsub(/\.+$/, "")
29
+ end
30
+
31
+ end
32
+ end
@@ -0,0 +1,33 @@
1
+ module Metrician
2
+ module Jobs
3
+ class DelayedJobCallbacks < ::Delayed::Plugin
4
+
5
+ callbacks do |lifecycle|
6
+ lifecycle.around(:invoke_job) do |job, &block|
7
+ begin
8
+ start = Time.now
9
+ block.call(job)
10
+ ensure
11
+ if Jobs.run?
12
+ duration = Time.now - start
13
+ Metrician.gauge(Jobs::RUN_METRIC, duration)
14
+ if Jobs.job_specific?
15
+ Metrician.gauge("#{Jobs::RUN_METRIC}.job.#{Jobs.instrumentation_name(job.name)}", duration)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ lifecycle.after(:error) do |_worker, job|
22
+ if Jobs.error?
23
+ Metrician.increment(Jobs::ERROR_METRIC)
24
+ if Jobs.job_specific?
25
+ Metrician.increment("#{Jobs::ERROR_METRIC}.job.#{Jobs.instrumentation_name(job.name)}")
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,36 @@
1
+ # Reference materials:
2
+ # https://github.com/resque/resque/blob/master/docs/HOOKS.md
3
+ module Metrician
4
+ module Jobs
5
+ module ResquePlugin
6
+
7
+ def around_perform_with_metrician(*_args)
8
+ start = Time.now
9
+ yield
10
+ ensure
11
+ if Jobs.run?
12
+ duration = Time.now - start
13
+ Metrician.gauge(Jobs::RUN_METRIC, duration)
14
+ if Jobs.job_specific?
15
+ Metrician.gauge("#{Jobs::RUN_METRIC}.job.#{Jobs.instrumentation_name(self.to_s)}", duration)
16
+ end
17
+ Metrician.agent.cleanup
18
+ end
19
+ end
20
+
21
+ def on_failure_with_metrician(_e, *_args)
22
+ if Jobs.error?
23
+ Metrician.increment(Jobs::ERROR_METRIC)
24
+ if Jobs.job_specific?
25
+ Metrician.increment("#{Jobs::ERROR_METRIC}.job.#{Jobs.instrumentation_name(self.to_s)}")
26
+ end
27
+ Metrician.agent.cleanup
28
+ end
29
+ end
30
+
31
+ ::Resque.before_fork = proc { Metrician.agent.cleanup }
32
+
33
+ end
34
+
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module Metrician
2
+ module Jobs
3
+ class SidekiqMiddleware
4
+
5
+ def call(worker, _msg, _queue)
6
+ start = Time.now
7
+ yield
8
+ rescue
9
+ if Jobs.error?
10
+ Metrician.increment(Jobs::ERROR_METRIC)
11
+ if Jobs.job_specific?
12
+ Metrician.increment("#{Jobs::ERROR_METRIC}.job.#{Jobs.instrumentation_name(worker.class.name)}")
13
+ end
14
+ end
15
+ raise
16
+ ensure
17
+ if Jobs.run?
18
+ duration = Time.now - start
19
+ Metrician.gauge(Jobs::RUN_METRIC, duration)
20
+ if Jobs.job_specific?
21
+ Metrician.gauge("#{Jobs::RUN_METRIC}.job.#{Jobs.instrumentation_name(worker.class.name)}", duration)
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,64 @@
1
+ module Metrician
2
+ module Middleware
3
+ ENV_REQUEST_TOTAL_TIME = "METRICIAN_REQUEST_TOTAL_TIME".freeze
4
+ ENV_QUEUE_START_KEYS = ["X-Request-Start".freeze,
5
+ "X-Queue-Start".freeze,
6
+ "X-REQUEST-START".freeze,
7
+ "X_REQUEST_START".freeze,
8
+ "HTTP_X_QUEUE_START".freeze]
9
+ ENV_CONTROLLER_PATH = "action_controller.instance".freeze
10
+ ENV_REQUEST_PATH = "REQUEST_PATH".freeze
11
+ HEADER_CONTENT_LENGTH = "Content-Length".freeze
12
+ ASSET_CONTROLLER_ROUTE = "assets".freeze
13
+ UNKNOWN_CONTROLLER_ROUTE = "unknown_endpoint".freeze
14
+ UNKNOWN_ACTION = "unknown_action".freeze
15
+ ASSET_PATH_MATCHER = %r|\A/{0,2}/assets|.freeze
16
+ APDEX_SATISFIED_METRIC = "web.apdex.satisfied".freeze
17
+ APDEX_TOLERATED_METRIC = "web.apdex.tolerated".freeze
18
+ APDEX_FRUSTRATED_METRIC = "web.apdex.frustrated".freeze
19
+
20
+ def self.configuration
21
+ @configuration ||= Metrician.configuration[:request_timing]
22
+ end
23
+
24
+ def self.enabled?
25
+ @enabled ||= configuration[:enabled]
26
+ end
27
+
28
+ def self.request_timing_required?
29
+ request? || apdex?
30
+ end
31
+
32
+ def self.request?
33
+ @request ||= configuration[:request][:enabled]
34
+ end
35
+
36
+ def self.error?
37
+ @request ||= configuration[:error][:enabled]
38
+ end
39
+
40
+ def self.idle?
41
+ @idle ||= configuration[:idle][:enabled]
42
+ end
43
+
44
+ def self.response_size?
45
+ @response_size ||= configuration[:response_size][:enabled]
46
+ end
47
+
48
+ def self.middleware?
49
+ @middleware ||= configuration[:middleware][:enabled]
50
+ end
51
+
52
+ def self.queue_time?
53
+ @queue_time ||= configuration[:queue_time][:enabled]
54
+ end
55
+
56
+ def self.route_tracking?
57
+ @route_tracking ||= configuration[:route_tracking][:enabled]
58
+ end
59
+
60
+ def self.apdex?
61
+ @apdex ||= configuration[:apdex][:enabled]
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,29 @@
1
+ module Metrician
2
+ module Middleware
3
+ # RequestTiming and ApplicationTiming work in concert to time the middleware
4
+ # separate from the request processing. RequestTiming should be the first
5
+ # or near first middleware loaded since it will be timing from the moment
6
+ # the the app server is hit and setting up the env for tracking the
7
+ # middleware execution time. RequestTiming should be the last or near
8
+ # last middleware loaded as it times the application execution (separate from
9
+ # middleware).
10
+ class ApplicationTiming
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ if Middleware.request_timing_required?
18
+ start_time = Time.now.to_f
19
+ end
20
+ @app.call(env)
21
+ ensure
22
+ if Middleware.request_timing_required?
23
+ env[ENV_REQUEST_TOTAL_TIME] ||= (Time.now.to_f - start_time)
24
+ end
25
+ end
26
+
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,152 @@
1
+ module Metrician
2
+ module Middleware
3
+ # RequestTiming and ApplicationTiming work in concert to time the middleware
4
+ # separate from the request processing. RequestTiming should be the first
5
+ # or near first middleware loaded since it will be timing from the moment
6
+ # the the app server is hit and setting up the env for tracking the
7
+ # middleware execution time. RequestTiming should be the last or near
8
+ # last middleware loaded as it times the application execution (separate from
9
+ # middleware).
10
+ class RequestTiming
11
+
12
+ def initialize(app)
13
+ @app = app
14
+ end
15
+
16
+ def call(env)
17
+ process_start_time = Time.now.to_f
18
+ response_size = 0
19
+
20
+ if Middleware.queue_time?
21
+ queue_start_time = self.class.extract_request_start_time(env)
22
+ gauge(:queue_time, process_start_time - queue_start_time) if queue_start_time
23
+ end
24
+
25
+ if Middleware.idle?
26
+ if @request_end_time
27
+ gauge(:idle, process_start_time - @request_end_time)
28
+ @request_end_time = nil
29
+ end
30
+ end
31
+
32
+ begin
33
+ status, headers, body = @app.call(env)
34
+ [status, headers, body]
35
+ ensure
36
+ if need_route?
37
+ current_route = self.class.extract_route(
38
+ controller: env[ENV_CONTROLLER_PATH],
39
+ path: env[ENV_REQUEST_PATH]
40
+ )
41
+ if Middleware.request_timing_required?
42
+ request_time = env[ENV_REQUEST_TOTAL_TIME].to_f
43
+ env[ENV_REQUEST_TOTAL_TIME] = nil
44
+ end
45
+ if Middleware.request?
46
+ gauge(:request, request_time, current_route)
47
+ end
48
+ if Middleware.apdex?
49
+ apdex(request_time)
50
+ end
51
+ if Middleware.error?
52
+ # We to_i the status because in some circumstances it is a
53
+ # Fixnum and some it is a string. Because why not.
54
+ increment(:error, current_route) if status.to_i >= 500
55
+ end
56
+ if Middleware.response_size?
57
+ # Note that 30xs don't have content-length, so cached
58
+ # items will report other metrics but not this one
59
+ response_size = self.class.get_response_size(headers: headers, body: body)
60
+ if response_size && !response_size.to_s.strip.empty?
61
+ gauge(:response_size, response_size.to_i, current_route)
62
+ end
63
+ end
64
+ end
65
+
66
+ if Middleware.middleware?
67
+ middleware_time = (Time.now.to_f - process_start_time) - request_time
68
+ gauge(:middleware, middleware_time)
69
+ end
70
+
71
+ @request_end_time = Time.now.to_f
72
+ end
73
+ end
74
+
75
+ def gauge(kind, value, route = nil)
76
+ Metrician.gauge("web.#{kind}", value)
77
+ if route && Middleware.route_tracking?
78
+ Metrician.gauge("web.#{kind}.#{route}", value)
79
+ end
80
+ end
81
+
82
+ def increment(kind, route = nil)
83
+ Metrician.increment("web.#{kind}")
84
+ if route && Middleware.route_tracking?
85
+ Metrician.increment("web.#{kind}.#{route}")
86
+ end
87
+ end
88
+
89
+ def need_route?
90
+ Middleware.request? ||
91
+ Middleware.error? ||
92
+ Middleware.response_size?
93
+ end
94
+
95
+ def apdex(request_time)
96
+ satisfied_threshold = Middleware.configuration[:apdex][:satisfied_threshold]
97
+ tolerated_threshold = satisfied_threshold * 4
98
+
99
+ case
100
+ when request_time <= satisfied_threshold
101
+ Metrician.gauge(APDEX_SATISFIED_METRIC, request_time)
102
+ when request_time <= tolerated_threshold
103
+ Metrician.gauge(APDEX_TOLERATED_METRIC, request_time)
104
+ else
105
+ Metrician.gauge(APDEX_FRUSTRATED_METRIC, request_time)
106
+ end
107
+ end
108
+
109
+ def self.extract_request_start_time(env)
110
+ return if no_queue_start_marker?
111
+ unless queue_start_marker
112
+ define_queue_start_marker(env)
113
+ end
114
+ return if no_queue_start_marker?
115
+ result = env[queue_start_marker].to_f
116
+ result > 1_000_000_000 ? result : nil
117
+ end
118
+
119
+ def self.no_queue_start_marker?
120
+ @no_queue_start_marker
121
+ end
122
+
123
+ def self.queue_start_marker
124
+ @queue_start_marker
125
+ end
126
+
127
+ def self.define_queue_start_marker(env)
128
+ @queue_start_marker = ENV_QUEUE_START_KEYS.detect do |key|
129
+ env.keys.include?(key)
130
+ end
131
+ @no_queue_start_marker = @queue_start_marker.nil?
132
+ end
133
+
134
+ def self.extract_route(controller:, path:)
135
+ unless controller
136
+ return ASSET_CONTROLLER_ROUTE if path =~ ASSET_PATH_MATCHER
137
+ return UNKNOWN_CONTROLLER_ROUTE
138
+ end
139
+ controller_name = Metrician.dotify(controller.class)
140
+ action_name = controller.action_name.blank? ? UNKNOWN_ACTION : controller.action_name
141
+ method_name = controller.request.request_method.to_s
142
+ "#{controller_name}.#{action_name}.#{method_name}".downcase
143
+ end
144
+
145
+ def self.get_response_size(headers:, body:)
146
+ return headers[HEADER_CONTENT_LENGTH] if headers[HEADER_CONTENT_LENGTH]
147
+ body.first.length.to_s if body.respond_to?(:length) && body.length == 1
148
+ end
149
+
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,41 @@
1
+ require "set"
2
+
3
+ module Metrician
4
+ class Reporter
5
+
6
+ def self.all
7
+ reporters.select(&:enabled?).map(&:new)
8
+ end
9
+
10
+ class << self
11
+
12
+ attr_reader :reporters
13
+
14
+ end
15
+
16
+ def self.inherited(subclass)
17
+ @reporters ||= Set.new
18
+ @reporters << subclass
19
+ end
20
+
21
+ def self.enabled?
22
+ false
23
+ end
24
+
25
+ def instrument
26
+ nil
27
+ end
28
+
29
+ end
30
+ end
31
+
32
+ require "metrician/reporters/active_record"
33
+ require "metrician/reporters/delayed_job"
34
+ require "metrician/reporters/honeybadger"
35
+ require "metrician/reporters/memcache"
36
+ require "metrician/reporters/method_tracer"
37
+ require "metrician/reporters/middleware"
38
+ require "metrician/reporters/net_http"
39
+ require "metrician/reporters/redis"
40
+ require "metrician/reporters/resque"
41
+ require "metrician/reporters/sidekiq"