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,36 @@
1
+ #!/bin/bash
2
+ set -e
3
+ cd "$(dirname "$0")/.."
4
+
5
+ eval "$(rbenv init - --no-rehash)"
6
+
7
+ rspec_file_line="$1"
8
+ if [[ "$rspec_file_line" != "" ]]; then
9
+ rspec_file_line="[${rspec_file_line}]"
10
+ fi
11
+
12
+ success="true"
13
+
14
+ for ruby_version in `ruby -ryaml -e 'puts YAML.load(File.read(".travis.yml"))["rvm"].join(" ")'`; do
15
+ {
16
+ echo "testing rub version $ruby_version" &&
17
+ rbenv shell $ruby_version &&
18
+ bundle exec rake matrix:spec${rspec_file_line}
19
+ } || success="false"
20
+ done
21
+
22
+ if [ $success == "true" ]; then
23
+ tput bold # bold text
24
+ tput setaf 2 # green text
25
+ echo "======================================"
26
+ echo "= Passed ="
27
+ echo "======================================"
28
+ tput sgr0 # reset to default text
29
+ else
30
+ tput bold # bold text
31
+ tput setaf 1 # red text
32
+ echo "======================================"
33
+ echo "= FAILED ="
34
+ echo "======================================"
35
+ tput sgr0 # reset to default text
36
+ fi
@@ -0,0 +1,372 @@
1
+ require "spec_helper"
2
+ require "instrumental_agent"
3
+
4
+ module Metrician
5
+ def self.null_agent(agent_class: Instrumental::Agent)
6
+ self.agent = agent_class.new(nil, enabled: false)
7
+ end
8
+ end
9
+
10
+ RSpec.describe Metrician do
11
+ before do
12
+ Metrician.instance_variable_set("@agent", nil)
13
+ end
14
+
15
+ it "has a version number" do
16
+ Metrician::VERSION.should_not be nil
17
+ end
18
+
19
+ describe "Metrician.activate" do
20
+ it "takes an agent" do
21
+ # if this excepts, the world changed
22
+ Metrician.activate(Metrician.null_agent)
23
+ end
24
+
25
+ it "excepts if the agent is nil" do
26
+ lambda { Metrician.activate(nil) }.should raise_error(Metrician::MissingAgent)
27
+ end
28
+
29
+ it "excepts if the agent doesn't define one of the required agent methods" do
30
+ class BadAgent
31
+ # this list is from Metrician::REQUIRED_AGENT_METHODS
32
+ def cleanup; end
33
+ def gauge; end
34
+ def increment; end
35
+ def logger; end
36
+ # def logger=(value); end
37
+ end
38
+ agent = BadAgent.new
39
+ lambda { Metrician.activate(agent) }.should raise_error(Metrician::IncompatibleAgent)
40
+ end
41
+ end
42
+
43
+ describe "ActiveRecord" do
44
+ before do
45
+ @agent = Metrician.null_agent
46
+ Metrician.activate(@agent)
47
+ end
48
+
49
+ specify "top level queries are instrumented" do
50
+ @agent.stub(:gauge)
51
+ @agent.should_receive(:gauge).with("database.query", anything)
52
+
53
+ User.where(name: "foobar").to_a
54
+ end
55
+
56
+ specify "per command instrumentation" do
57
+ Metrician.configuration[:database][:command][:enabled] = true
58
+ @agent.stub(:gauge)
59
+ @agent.should_receive(:gauge).with("database.select", anything)
60
+
61
+ User.where(name: "foobar").to_a
62
+ end
63
+
64
+ specify "per table instrumentation" do
65
+ Metrician.configuration[:database][:table] = true
66
+ @agent.stub(:gauge)
67
+ @agent.should_receive(:gauge).with("database.users", anything)
68
+
69
+ User.where(name: "foobar").to_a
70
+ end
71
+
72
+ specify "per command and table instrumentation" do
73
+ Metrician.configuration[:database][:command] = true
74
+ Metrician.configuration[:database][:table] = true
75
+ @agent.stub(:gauge)
76
+ @agent.should_receive(:gauge).with("database.select.users", anything)
77
+
78
+ User.where(name: "foobar").to_a
79
+ end
80
+ end
81
+
82
+ describe "job systems" do
83
+ describe "delayed_job" do
84
+ before do
85
+ @agent = Metrician.null_agent
86
+ Metrician.activate(@agent)
87
+ end
88
+
89
+ specify "DelayedJob is instrumented" do
90
+ @agent.stub(:gauge)
91
+
92
+ @agent.should_receive(:gauge).with("jobs.run", anything)
93
+ Delayed::Job.enqueue(TestDelayedJob.new(success: true))
94
+ Delayed::Worker.new(exit_on_complete: true).start
95
+ end
96
+
97
+ specify "job errors are instrumented" do
98
+ @agent.stub(:increment)
99
+
100
+ @agent.should_receive(:increment).with("jobs.error", 1)
101
+ Delayed::Job.enqueue(TestDelayedJob.new(error: true))
102
+ Delayed::Worker.new(exit_on_complete: true).start
103
+ end
104
+
105
+ specify "per job instrumentation" do
106
+ Metrician.configuration[:jobs][:job_specific][:enabled] = true
107
+ @agent.stub(:gauge)
108
+
109
+ @agent.should_receive(:gauge).with("jobs.run.job.TestDelayedJob", anything)
110
+ Delayed::Job.enqueue(TestDelayedJob.new(success: true))
111
+ Delayed::Worker.new(exit_on_complete: true).start
112
+ end
113
+ end
114
+
115
+ describe "resque" do
116
+ before do
117
+ Resque.inline = true
118
+ @agent = Metrician.null_agent
119
+ Metrician.activate(@agent)
120
+ end
121
+
122
+ after do
123
+ Resque.inline = false
124
+ end
125
+
126
+ specify "Resque is instrumented" do
127
+ @agent.stub(:gauge)
128
+ @agent.should_receive(:gauge).with("jobs.run", anything)
129
+
130
+ # typically Metrician would be loaded in an initalizer
131
+ # and this _extend_ could be done inside the job itself, but here
132
+ # we are in a weird situation.
133
+ TestResqueJob.send(:extend, Metrician::Jobs::ResquePlugin)
134
+ Resque.enqueue(TestResqueJob, { "success" => true })
135
+ end
136
+
137
+ specify "job errors are instrumented" do
138
+ @agent.stub(:increment)
139
+ @agent.should_receive(:increment).with("jobs.error", 1)
140
+
141
+ # typically Metrician would be loaded in an initalizer
142
+ # and this _extend_ could be done inside the job itself, but here
143
+ # we are in a weird situation.
144
+ TestResqueJob.send(:extend, Metrician::Jobs::ResquePlugin)
145
+ lambda { Resque.enqueue(TestResqueJob, { "error" => true }) }.should raise_error(StandardError)
146
+ end
147
+ end
148
+
149
+ describe "sidekiq" do
150
+ before do
151
+ Sidekiq::Testing.inline!
152
+ @agent = Metrician.null_agent
153
+ Metrician.activate(@agent)
154
+ # sidekiq doesn't use middleware by design in their testing
155
+ # harness, so we add it just as metrician does
156
+ # https://github.com/mperham/sidekiq/wiki/Testing#testing-server-middleware
157
+ Sidekiq::Testing.server_middleware do |chain|
158
+ chain.add Metrician::Jobs::SidekiqMiddleware
159
+ end
160
+ end
161
+
162
+ after do
163
+ Sidekiq::Testing.disable!
164
+ end
165
+
166
+ specify "Sidekiq is instrumented" do
167
+ @agent.stub(:gauge)
168
+ @agent.should_receive(:gauge).with("jobs.run", anything)
169
+
170
+ # avoid load order error of sidekiq here by just including the
171
+ # worker bits at latest possible time
172
+ TestSidekiqWorker.perform_async({ "success" => true})
173
+ end
174
+
175
+ specify "job errors are instrumented" do
176
+ @agent.stub(:increment)
177
+ @agent.should_receive(:increment).with("jobs.error", 1)
178
+
179
+ # avoid load order error of sidekiq here by just including the
180
+ # worker bits at latest possible time
181
+ lambda { TestSidekiqWorker.perform_async({ "error" => true}) }.should raise_error(StandardError)
182
+ end
183
+ end
184
+ end
185
+
186
+ describe "cache systems" do
187
+ specify "redis is instrumented" do
188
+ agent = Metrician.null_agent
189
+ Metrician.activate(agent)
190
+ client = Redis.new
191
+ agent.stub(:gauge)
192
+ agent.should_receive(:gauge).with("cache.command", anything)
193
+ client.get("foo-#{rand(100_000)}")
194
+ end
195
+
196
+ memcached_clients = [
197
+ defined?(::Memcached) && ::Memcached.new("localhost:11211"),
198
+ defined?(::Dalli::Client) && ::Dalli::Client.new("localhost:11211"),
199
+ ].compact
200
+ raise "no memcached client" if memcached_clients.empty?
201
+
202
+ memcached_clients.each do |client|
203
+ specify "memcached is instrumented (#{client.class.name})" do
204
+ agent = Metrician.null_agent
205
+ Metrician.activate(agent)
206
+ agent.stub(:gauge)
207
+
208
+ agent.should_receive(:gauge).with("cache.command", anything)
209
+ begin
210
+ client.get("foo-#{rand(100_000)}")
211
+ rescue Memcached::NotFound
212
+ # memcached raises this when there is no value for "foo-N" set
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ describe "external service timing" do
219
+ specify "Net::HTTP is instrumented" do
220
+ agent = Metrician.null_agent
221
+ Metrician.activate(agent)
222
+ agent.stub(:gauge)
223
+
224
+ agent.should_receive(:gauge).with("service.request", anything)
225
+ Net::HTTP.get(URI.parse("http://example.com/"))
226
+ end
227
+ end
228
+
229
+ describe "request timing" do
230
+ include Rack::Test::Methods
231
+
232
+ let(:agent) { Metrician.null_agent }
233
+
234
+ describe "success case" do
235
+ def app
236
+ require "metrician/middleware/request_timing"
237
+ require "metrician/middleware/application_timing"
238
+ Rack::Builder.app do
239
+ use Metrician::Middleware::RequestTiming
240
+ use Metrician::Middleware::ApplicationTiming
241
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
242
+ end
243
+ end
244
+
245
+ specify "Rack timing is instrumented" do
246
+ agent.stub(:gauge)
247
+
248
+ agent.should_receive(:gauge).with("web.request", anything)
249
+ get "/"
250
+ end
251
+ end
252
+
253
+ describe "error case" do
254
+ def app
255
+ require "metrician/middleware/request_timing"
256
+ require "metrician/middleware/application_timing"
257
+ Rack::Builder.app do
258
+ use Metrician::Middleware::RequestTiming
259
+ use Metrician::Middleware::ApplicationTiming
260
+ run lambda { |env| [500, {'Content-Type' => 'text/plain'}, ['BOOM']] }
261
+ end
262
+ end
263
+
264
+ specify "500s are instrumented for error tracking" do
265
+ agent.stub(:gauge)
266
+ agent.stub(:increment)
267
+
268
+ agent.should_receive(:gauge).with("web.request", anything)
269
+ agent.should_receive(:increment).with("web.error", 1)
270
+ get "/"
271
+ end
272
+ end
273
+
274
+ describe "queueing timing" do
275
+ def app
276
+ require "metrician/middleware/request_timing"
277
+ require "metrician/middleware/application_timing"
278
+ Rack::Builder.app do
279
+ use Metrician::Middleware::RequestTiming
280
+ use Metrician::Middleware::ApplicationTiming
281
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
282
+ end
283
+ end
284
+
285
+ specify "Queue timing is instrumented" do
286
+ Metrician.configuration[:request_timing][:queue_time][:enabled] = true
287
+ agent.stub(:gauge)
288
+
289
+ agent.should_receive(:gauge).with("web.queue_time", anything)
290
+ get "/", {}, { Metrician::Middleware::ENV_QUEUE_START_KEYS.first => 1.second.ago.to_f }
291
+ end
292
+ end
293
+
294
+ describe "apdex" do
295
+
296
+ let(:agent) { Metrician.null_agent }
297
+
298
+ describe "fast" do
299
+ def app
300
+ require "metrician/middleware/request_timing"
301
+ require "metrician/middleware/application_timing"
302
+ Rack::Builder.app do
303
+ use Metrician::Middleware::RequestTiming
304
+ use Metrician::Middleware::ApplicationTiming
305
+ # This SHOULD be fast enough to fit under our
306
+ # default threshold of 2.5s :)
307
+ run lambda { |env| [200, {'Content-Type' => 'text/plain'}, ['OK']] }
308
+ end
309
+ end
310
+
311
+ specify "satisfied is recorded" do
312
+ agent.stub(:gauge)
313
+
314
+ agent.should_receive(:gauge).with("web.apdex.satisfied", anything)
315
+ agent.should_not_receive(:gauge).with("web.apdex.tolerated", anything)
316
+ agent.should_not_receive(:gauge).with("web.apdex.frustrated", anything)
317
+ get "/"
318
+ end
319
+
320
+ end
321
+
322
+ describe "medium" do
323
+ def app
324
+ require "metrician/middleware/request_timing"
325
+ require "metrician/middleware/application_timing"
326
+ Rack::Builder.app do
327
+ use Metrician::Middleware::RequestTiming
328
+ use Metrician::Middleware::ApplicationTiming
329
+ run ->(env) {
330
+ env[Metrician::Middleware::ENV_REQUEST_TOTAL_TIME] = 3.0 # LOAD-BEARING
331
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
332
+ }
333
+ end
334
+ end
335
+
336
+ specify "tolerated is recorded" do
337
+ agent.stub(:gauge)
338
+
339
+ agent.should_not_receive(:gauge).with("web.apdex.satisfied", anything)
340
+ agent.should_receive(:gauge).with("web.apdex.tolerated", anything)
341
+ agent.should_not_receive(:gauge).with("web.apdex.frustrated", anything)
342
+ get "/"
343
+ end
344
+ end
345
+
346
+ describe "slow" do
347
+ def app
348
+ require "metrician/middleware/request_timing"
349
+ require "metrician/middleware/application_timing"
350
+ Rack::Builder.app do
351
+ use Metrician::Middleware::RequestTiming
352
+ use Metrician::Middleware::ApplicationTiming
353
+ run ->(env) {
354
+ env[Metrician::Middleware::ENV_REQUEST_TOTAL_TIME] = 28.0 # LOAD-BEARING
355
+ [200, {'Content-Type' => 'text/plain'}, ['OK']]
356
+ }
357
+ end
358
+ end
359
+
360
+ specify "frustrated is recorded" do
361
+ agent.stub(:gauge)
362
+
363
+ agent.should_not_receive(:gauge).with("web.apdex.satisfied", anything)
364
+ agent.should_not_receive(:gauge).with("web.apdex.tolerated", anything)
365
+ agent.should_receive(:gauge).with("web.apdex.frustrated", anything)
366
+ get "/"
367
+ end
368
+ end
369
+
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,23 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+
3
+ require "bundler/setup"
4
+ Bundler.require(:default)
5
+ require "metrician"
6
+ require "gemika"
7
+ require "byebug"
8
+
9
+
10
+ Dir["#{File.dirname(__FILE__)}/support/*.rb"].sort.each {|f| require f}
11
+
12
+ RSpec.configure do |config|
13
+ # Enable flags like --only-failures and --next-failure
14
+ config.example_status_persistence_file_path = ".rspec_status"
15
+
16
+ config.expect_with :rspec do |c|
17
+ c.syntax = :should
18
+ end
19
+
20
+ config.mock_with :rspec do |mocks|
21
+ mocks.syntax = :should
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ database = Gemika::Database.new
2
+ database.connect
3
+
4
+ if Gemika::Env.gem?('activerecord', '< 5')
5
+ class ActiveRecord::ConnectionAdapters::Mysql2Adapter
6
+ NATIVE_DATABASE_TYPES[:primary_key] = "int(11) auto_increment PRIMARY KEY"
7
+ end
8
+ end
9
+
10
+ database.rewrite_schema! do
11
+
12
+ create_table :users do |t|
13
+ t.string :name
14
+ t.string :email
15
+ t.string :city
16
+ end
17
+
18
+ create_table :delayed_jobs, force: true do |table|
19
+ table.integer :priority, default: 0, null: false # Allows some jobs to jump to the front of the queue
20
+ table.integer :attempts, default: 0, null: false # Provides for retries, but still fail eventually.
21
+ table.text :handler, null: false # YAML-encoded string of the object that will do work
22
+ table.text :last_error # reason for last failure (See Note below)
23
+ table.datetime :run_at # When to run. Could be Time.zone.now for immediately, or sometime in the future.
24
+ table.datetime :locked_at # Set when a client is working on this object
25
+ table.datetime :failed_at # Set when all retries have failed (actually, by default, the record is deleted instead)
26
+ table.string :locked_by # Who is working on this object (if locked)
27
+ table.string :queue # The name of the queue this job is in
28
+ table.timestamps null: true
29
+ end
30
+
31
+ add_index :delayed_jobs, [:priority, :run_at], name: "delayed_jobs_priority"
32
+
33
+ end