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,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