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.
- checksums.yaml +7 -0
- data/.gitignore +18 -0
- data/.rspec +2 -0
- data/.rubocop.yml +65 -0
- data/.rubocop_todo.yml +24 -0
- data/.ruby-version +1 -0
- data/.travis.yml +36 -0
- data/Gemfile +1 -0
- data/METRICS.MD +48 -0
- data/README.md +77 -0
- data/Rakefile +12 -0
- data/config/metrician.yaml +136 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4 +24 -0
- data/gemfiles/Gemfile.4.2.sidekiq-4.lock +173 -0
- data/gemfiles/Gemfile.5.0.pg +24 -0
- data/gemfiles/Gemfile.5.0.pg.lock +182 -0
- data/lib/metrician.rb +80 -0
- data/lib/metrician/configuration.rb +33 -0
- data/lib/metrician/jobs.rb +32 -0
- data/lib/metrician/jobs/delayed_job_callbacks.rb +33 -0
- data/lib/metrician/jobs/resque_plugin.rb +36 -0
- data/lib/metrician/jobs/sidekiq_middleware.rb +28 -0
- data/lib/metrician/middleware.rb +64 -0
- data/lib/metrician/middleware/application_timing.rb +29 -0
- data/lib/metrician/middleware/request_timing.rb +152 -0
- data/lib/metrician/reporter.rb +41 -0
- data/lib/metrician/reporters/active_record.rb +63 -0
- data/lib/metrician/reporters/delayed_job.rb +17 -0
- data/lib/metrician/reporters/honeybadger.rb +26 -0
- data/lib/metrician/reporters/memcache.rb +49 -0
- data/lib/metrician/reporters/method_tracer.rb +70 -0
- data/lib/metrician/reporters/middleware.rb +22 -0
- data/lib/metrician/reporters/net_http.rb +28 -0
- data/lib/metrician/reporters/redis.rb +31 -0
- data/lib/metrician/reporters/resque.rb +17 -0
- data/lib/metrician/reporters/sidekiq.rb +19 -0
- data/lib/metrician/version.rb +5 -0
- data/metrician.gemspec +25 -0
- data/script/setup +72 -0
- data/script/test +36 -0
- data/spec/metrician_spec.rb +372 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/support/database.rb +33 -0
- data/spec/support/database.sample.yml +10 -0
- data/spec/support/database.travis.yml +9 -0
- data/spec/support/models.rb +2 -0
- data/spec/support/test_delayed_job.rb +12 -0
- data/spec/support/test_resque_job.rb +8 -0
- data/spec/support/test_sidekiq_worker.rb +8 -0
- 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"
|