librato-rack 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +24 -0
- data/README.md +176 -0
- data/Rakefile +25 -0
- data/lib/librato-rack.rb +1 -0
- data/lib/librato/collector.rb +48 -0
- data/lib/librato/collector/aggregator.rb +97 -0
- data/lib/librato/collector/counter_cache.rb +113 -0
- data/lib/librato/collector/group.rb +29 -0
- data/lib/librato/rack.rb +116 -0
- data/lib/librato/rack/configuration.rb +61 -0
- data/lib/librato/rack/errors.rb +7 -0
- data/lib/librato/rack/logger.rb +71 -0
- data/lib/librato/rack/tracker.rb +137 -0
- data/lib/librato/rack/validating_queue.rb +41 -0
- data/lib/librato/rack/version.rb +5 -0
- data/lib/librato/rack/worker.rb +49 -0
- data/test/apps/basic.ru +20 -0
- data/test/apps/custom.ru +27 -0
- data/test/apps/heroku.ru +27 -0
- data/test/integration/custom_test.rb +59 -0
- data/test/integration/heroku_test.rb +36 -0
- data/test/integration/request_test.rb +69 -0
- data/test/remote/tracker_test.rb +198 -0
- data/test/test_helper.rb +22 -0
- data/test/unit/collector/aggregator_test.rb +69 -0
- data/test/unit/collector/counter_cache_test.rb +96 -0
- data/test/unit/collector/group_test.rb +53 -0
- data/test/unit/collector_test.rb +23 -0
- data/test/unit/rack/configuration_test.rb +86 -0
- data/test/unit/rack/logger_test.rb +91 -0
- data/test/unit/rack/tracker_test.rb +44 -0
- data/test/unit/rack/worker_test.rb +36 -0
- metadata +132 -0
@@ -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
|
data/test/apps/basic.ru
ADDED
@@ -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)
|
data/test/apps/custom.ru
ADDED
@@ -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)
|
data/test/apps/heroku.ru
ADDED
@@ -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
|