librato-rack 0.1.0

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.
@@ -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