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