metrician 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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"