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