librato-rack 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ module Librato
2
+ class Rack
3
+ VERSION = "0.1.0"
4
+ end
5
+ end
@@ -0,0 +1,49 @@
1
+ module Librato
2
+ class Rack
3
+ # Runs a given piece of code periodically, ensuring that
4
+ # it will be run again at the proper interval regardless
5
+ # of how long execution takes.
6
+ #
7
+ class Worker
8
+
9
+ def initialize
10
+ @interrupt = false
11
+ end
12
+
13
+ # run the given block every <period> seconds, looping
14
+ # infinitely unless @interrupt becomes true.
15
+ #
16
+ def run_periodically(period, &block)
17
+ next_run = start_time(period)
18
+ until @interrupt do
19
+ now = Time.now
20
+ if now >= next_run
21
+ block.call
22
+ while next_run <= now
23
+ next_run += period
24
+ end
25
+ else
26
+ sleep(next_run - now)
27
+ end
28
+ end
29
+ end
30
+
31
+ # Give some structure to worker start times so when possible
32
+ # they will be in sync.
33
+ def start_time(period)
34
+ earliest = Time.now + period
35
+ # already on a whole minute
36
+ return earliest if earliest.sec == 0
37
+ if period > 30
38
+ # bump to whole minute
39
+ earliest + (60-earliest.sec)
40
+ else
41
+ # ensure sync to whole minute if minute is evenly divisible
42
+ earliest + (period-(earliest.sec%period))
43
+ end
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ require 'bundler/setup'
2
+ require 'librato-rack'
3
+
4
+ use Librato::Rack
5
+
6
+ def application(env)
7
+ case env['PATH_INFO']
8
+ when '/status/204'
9
+ [204, {"Content-Type" => 'text/html'}, ["Status 204!"]]
10
+ when '/exception'
11
+ raise 'exception raised!'
12
+ when '/slow'
13
+ sleep 0.3
14
+ [200, {"Content-Type" => 'text/html'}, ["Slow request"]]
15
+ else
16
+ [200, {"Content-Type" => 'text/html'}, ["Hello!"]]
17
+ end
18
+ end
19
+
20
+ run method(:application)
@@ -0,0 +1,27 @@
1
+ require 'bundler/setup'
2
+ require 'librato-rack'
3
+
4
+ use Librato::Rack
5
+
6
+ def application(env)
7
+ case env['PATH_INFO']
8
+ when '/increment'
9
+ Librato.increment :hits
10
+ when '/measure'
11
+ Librato.measure 'nodes', 3
12
+ when '/timing'
13
+ Librato.timing 'lookup.time', 2.3
14
+ when '/timing_block'
15
+ Librato.timing 'sleeper' do
16
+ sleep 0.01
17
+ end
18
+ when '/group'
19
+ Librato.group 'did.a' do |g|
20
+ g.increment 'thing'
21
+ g.timing 'timing', 2.3
22
+ end
23
+ end
24
+ [200, {"Content-Type" => 'text/html'}, ["Hello!"]]
25
+ end
26
+
27
+ run method(:application)
@@ -0,0 +1,27 @@
1
+ require 'bundler/setup'
2
+ require 'librato-rack'
3
+
4
+ # Simulate the environment variables Heroku passes along
5
+ # with each request
6
+ #
7
+ class FakeHeroku
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ env['HTTP_X_HEROKU_QUEUE_DEPTH'] = rand(4)
14
+ env['HTTP_X_HEROKU_QUEUE_WAIT_TIME'] = rand(0.1)
15
+ env['HTTP_X_HEROKU_DYNOS_IN_USE'] = 2
16
+ @app.call(env)
17
+ end
18
+ end
19
+
20
+ use FakeHeroku
21
+ use Librato::Rack
22
+
23
+ def application(env)
24
+ [200, {"Content-Type" => 'text/html'}, ["Hello!"]]
25
+ end
26
+
27
+ run method(:application)
@@ -0,0 +1,59 @@
1
+ require 'test_helper'
2
+ require 'rack/test'
3
+
4
+ # Tests for universal tracking for all request paths
5
+ #
6
+ class CustomTest < MiniTest::Unit::TestCase
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ Rack::Builder.parse_file('test/apps/custom.ru').first
11
+ end
12
+
13
+ def teardown
14
+ # clear metrics before each run
15
+ aggregate.delete_all
16
+ counters.delete_all
17
+ end
18
+
19
+ def test_increment
20
+ get '/increment'
21
+ assert_equal 1, counters[:hits]
22
+ 2.times { get '/increment' }
23
+ assert_equal 3, counters[:hits]
24
+ end
25
+
26
+ def test_measure
27
+ get '/measure'
28
+ assert_equal 3.0, aggregate[:nodes][:sum]
29
+ assert_equal 1, aggregate[:nodes][:count]
30
+ end
31
+
32
+ def test_timing
33
+ get '/timing'
34
+ assert_equal 1, aggregate['lookup.time'][:count]
35
+ end
36
+
37
+ def test_timing_block
38
+ get '/timing_block'
39
+ assert_equal 1, aggregate['sleeper'][:count]
40
+ assert_in_delta 10, aggregate['sleeper'][:sum], 10
41
+ end
42
+
43
+ def test_grouping
44
+ get '/group'
45
+ assert_equal 1, counters['did.a.thing']
46
+ assert_equal 1, aggregate['did.a.timing'][:count]
47
+ end
48
+
49
+ private
50
+
51
+ def aggregate
52
+ Librato.tracker.collector.aggregate
53
+ end
54
+
55
+ def counters
56
+ Librato.tracker.collector.counters
57
+ end
58
+
59
+ end
@@ -0,0 +1,36 @@
1
+ require 'test_helper'
2
+ require 'rack/test'
3
+
4
+ # Tests for universal tracking for all request paths
5
+ #
6
+ class HerokuTest < MiniTest::Unit::TestCase
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ Rack::Builder.parse_file('test/apps/heroku.ru').first
11
+ end
12
+
13
+ def teardown
14
+ # clear metrics before each run
15
+ aggregate.delete_all
16
+ counters.delete_all
17
+ end
18
+
19
+ def test_heroku_metrics
20
+ get '/'
21
+ assert_equal 1, aggregate['rack.heroku.queue.depth'][:count]
22
+ assert_equal 1, aggregate['rack.heroku.queue.wait_time'][:count]
23
+ assert_equal 1, aggregate['rack.heroku.dynos'][:count]
24
+ end
25
+
26
+ private
27
+
28
+ def aggregate
29
+ Librato.tracker.collector.aggregate
30
+ end
31
+
32
+ def counters
33
+ Librato.tracker.collector.counters
34
+ end
35
+
36
+ end
@@ -0,0 +1,69 @@
1
+ require 'test_helper'
2
+ require 'rack/test'
3
+
4
+ # Tests for universal tracking for all request paths
5
+ #
6
+ class RequestTest < MiniTest::Unit::TestCase
7
+ include Rack::Test::Methods
8
+
9
+ def app
10
+ Rack::Builder.parse_file('test/apps/basic.ru').first
11
+ end
12
+
13
+ def teardown
14
+ # clear metrics before each run
15
+ aggregate.delete_all
16
+ counters.delete_all
17
+ end
18
+
19
+ def test_increment_total_and_status
20
+ get '/'
21
+ assert last_response.ok?
22
+ assert_equal 1, counters["rack.request.total"]
23
+ assert_equal 1, counters["rack.request.status.200"]
24
+ assert_equal 1, counters["rack.request.status.2xx"]
25
+
26
+ get '/status/204'
27
+ assert_equal 2, counters["rack.request.total"]
28
+ assert_equal 1, counters["rack.request.status.200"], 'should not increment'
29
+ assert_equal 1, counters["rack.request.status.204"], 'should increment'
30
+ assert_equal 2, counters["rack.request.status.2xx"]
31
+ end
32
+
33
+ def test_request_times
34
+ get '/'
35
+
36
+ # common for all paths
37
+ assert_equal 1, aggregate["rack.request.time"][:count],
38
+ 'should track total request time'
39
+
40
+ # status specific
41
+ assert_equal 1, aggregate["rack.request.status.200.time"][:count]
42
+ assert_equal 1, aggregate["rack.request.status.2xx.time"][:count]
43
+ end
44
+
45
+ def test_track_exceptions
46
+ begin
47
+ get '/exception'
48
+ rescue RuntimeError => e
49
+ raise unless e.message == 'exception raised!'
50
+ end
51
+ assert_equal 1, counters["rack.request.exceptions"]
52
+ end
53
+
54
+ def test_track_slow_requests
55
+ get '/slow'
56
+ assert_equal 1, counters["rack.request.slow"]
57
+ end
58
+
59
+ private
60
+
61
+ def aggregate
62
+ Librato.tracker.collector.aggregate
63
+ end
64
+
65
+ def counters
66
+ Librato.tracker.collector.counters
67
+ end
68
+
69
+ end
@@ -0,0 +1,198 @@
1
+ # # encoding: UTF-8
2
+ require 'test_helper'
3
+ require 'rack/test'
4
+
5
+ # Tests for universal tracking for all request paths
6
+ #
7
+ class TrackerRemoteTest < MiniTest::Unit::TestCase
8
+
9
+ # These tests connect to the Metrics server with an account and verify remote
10
+ # functions. They will only run if the below environment variables are set.
11
+ #
12
+ # BE CAREFUL, running these tests will DELETE ALL metrics currently in the
13
+ # test account.
14
+ #
15
+ if ENV['LIBRATO_RACK_TEST_EMAIL'] && ENV['LIBRATO_RACK_TEST_API_KEY']
16
+
17
+ def setup
18
+ config = Librato::Rack::Configuration.new
19
+ config.user = ENV['LIBRATO_RACK_TEST_EMAIL']
20
+ config.token = ENV['LIBRATO_RACK_TEST_API_KEY']
21
+ if ENV['LIBRATO_RACK_TEST_API_ENDPOINT']
22
+ config.api_endpoint = ENV['LIBRATO_RACK_TEST_API_ENDPOINT']
23
+ end
24
+ config.log_target = File.open('/dev/null', 'w') # ignore logs
25
+ @tracker = Librato::Rack::Tracker.new(config)
26
+ delete_all_metrics
27
+ end
28
+
29
+ def test_flush_counters
30
+ tracker.increment :foo # simple
31
+ tracker.increment :bar, 2 # specified
32
+ tracker.increment :foo # multincrement
33
+ tracker.increment :foo, :source => 'baz', :by => 3 # custom source
34
+ tracker.flush
35
+
36
+ metric_names = client.list.map { |m| m['name'] }
37
+ assert metric_names.include?('foo'), 'foo should be present'
38
+ assert metric_names.include?('bar'), 'bar should be present'
39
+
40
+ foo = client.fetch 'foo', :count => 10
41
+ assert_equal 1, foo[source].length
42
+ assert_equal 2, foo[source][0]['value']
43
+
44
+ # custom source
45
+ assert_equal 1, foo['baz'].length
46
+ assert_equal 3, foo['baz'][0]['value']
47
+
48
+ bar = client.fetch 'bar', :count => 10
49
+ assert_equal 1, bar[source].length
50
+ assert_equal 2, bar[source][0]['value']
51
+ end
52
+
53
+ def test_counter_persistent_through_flush
54
+ tracker.increment 'knightrider'
55
+ tracker.increment 'badguys', :sporadic => true
56
+ assert_equal 1, collector.counters['knightrider']
57
+ assert_equal 1, collector.counters['badguys']
58
+
59
+ tracker.flush
60
+ assert_equal 0, collector.counters['knightrider']
61
+ assert_equal nil, collector.counters['badguys']
62
+ end
63
+
64
+ def test_flush_should_send_measures_and_timings
65
+ tracker.timing 'request.time.total', 122.1
66
+ tracker.measure 'items_bought', 20
67
+ tracker.timing 'request.time.total', 81.3
68
+ tracker.timing 'jobs.queued', 5, :source => 'worker.3'
69
+ tracker.flush
70
+
71
+ metric_names = client.list.map { |m| m['name'] }
72
+ assert metric_names.include?('request.time.total'), 'request.time.total should be present'
73
+ assert metric_names.include?('items_bought'), 'request.time.db should be present'
74
+
75
+ total = client.fetch 'request.time.total', :count => 10
76
+ assert_equal 2, total[source][0]['count']
77
+ assert_in_delta 203.4, total[source][0]['sum'], 0.1
78
+
79
+ items = client.fetch 'items_bought', :count => 10
80
+ assert_equal 1, items[source][0]['count']
81
+ assert_in_delta 20, items[source][0]['sum'], 0.1
82
+
83
+ jobs = client.fetch 'jobs.queued', :count => 10
84
+ assert_equal 1, jobs['worker.3'][0]['count']
85
+ assert_in_delta 5, jobs['worker.3'][0]['sum'], 0.1
86
+ end
87
+
88
+ def test_flush_should_purge_measures_and_timings
89
+ tracker.timing 'request.time.total', 122.1
90
+ tracker.measure 'items_bought', 20
91
+ tracker.flush
92
+
93
+ assert collector.aggregate.empty?, 'measures and timings should be cleared with flush'
94
+ end
95
+
96
+ # Disabled for now because we always send a running process
97
+ # count at a minimum
98
+ #
99
+ # def test_empty_flush_should_not_be_sent
100
+ # tracker.flush
101
+ # assert_equal [], client.list
102
+ # end
103
+
104
+ def test_flush_respects_prefix
105
+ config.prefix = 'testyprefix'
106
+
107
+ tracker.timing 'mytime', 221.1
108
+ tracker.increment 'mycount', 4
109
+ tracker.flush
110
+
111
+ metric_names = client.list.map { |m| m['name'] }
112
+ assert metric_names.include?('testyprefix.mytime'), 'testyprefix.mytime should be present'
113
+ assert metric_names.include?('testyprefix.mycount'), 'testyprefix.mycount should be present'
114
+
115
+ mytime = client.fetch 'testyprefix.mytime', :count => 10
116
+ assert_equal 1, mytime[source][0]['count']
117
+
118
+ mycount = client.fetch 'testyprefix.mycount', :count => 10
119
+ assert_equal 4, mycount[source][0]['value']
120
+ end
121
+
122
+ def test_flush_recovers_from_failure
123
+ # create a metric foo of counter type
124
+ client.submit :foo => {:type => :counter, :value => 12}
125
+
126
+ # failing flush - submit a foo measurement as a gauge (type mismatch)
127
+ tracker.measure :foo, 2.12
128
+ tracker.flush
129
+
130
+ foo = client.fetch :foo, :count => 10
131
+ assert_equal 1, foo['unassigned'].length
132
+ assert_nil foo[source] # shouldn't have been accepted
133
+
134
+ tracker.measure :boo, 2.12
135
+ tracker.flush
136
+
137
+ boo = client.fetch :boo, :count => 10
138
+ assert_equal 2.12, boo[source][0]["value"]
139
+ end
140
+
141
+ def test_flush_handles_invalid_metric_names
142
+ tracker.increment :foo # valid
143
+ tracker.increment 'fübar' # invalid
144
+ tracker.measure 'fu/bar/baz', 12.1 # invalid
145
+ tracker.flush
146
+
147
+ metric_names = client.list.map { |m| m['name'] }
148
+ assert metric_names.include?('foo')
149
+
150
+ # should have saved values for foo
151
+ foo = client.fetch :foo, :count => 5
152
+ assert_equal 1.0, foo[source][0]["value"]
153
+ end
154
+
155
+ def test_flush_handles_invalid_sources_names
156
+ tracker.increment :foo, :source => 'atreides' # valid
157
+ tracker.increment :bar, :source => 'glébnöst' # invalid
158
+ tracker.measure 'baz', 2.25, :source => 'b/l/ak/nok' # invalid
159
+ tracker.flush
160
+
161
+ # should have saved values for foo
162
+ foo = client.fetch :foo, :count => 5
163
+ assert_equal 1.0, foo['atreides'][0]["value"]
164
+ end
165
+
166
+ private
167
+
168
+ def tracker
169
+ @tracker
170
+ end
171
+
172
+ def client
173
+ @tracker.send(:client)
174
+ end
175
+
176
+ def collector
177
+ @tracker.collector
178
+ end
179
+
180
+ def config
181
+ @tracker.config
182
+ end
183
+
184
+ def source
185
+ @tracker.qualified_source
186
+ end
187
+
188
+ def delete_all_metrics
189
+ metric_names = client.list.map { |metric| metric['name'] }
190
+ client.delete(*metric_names) if !metric_names.empty?
191
+ end
192
+
193
+ else
194
+ # ENV vars not set
195
+ puts "Skipping remote tests..."
196
+ end
197
+
198
+ end