appoptics-api-ruby 2.1.3

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.
Files changed (49) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +25 -0
  5. data/CHANGELOG.md +184 -0
  6. data/Gemfile +36 -0
  7. data/LICENSE +24 -0
  8. data/README.md +271 -0
  9. data/Rakefile +63 -0
  10. data/appoptics-api-ruby.gemspec +31 -0
  11. data/benchmarks/array_vs_set.rb +29 -0
  12. data/certs/librato-public.pem +20 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/submit_every.rb +27 -0
  15. data/lib/appoptics/metrics.rb +95 -0
  16. data/lib/appoptics/metrics/aggregator.rb +138 -0
  17. data/lib/appoptics/metrics/annotator.rb +145 -0
  18. data/lib/appoptics/metrics/client.rb +361 -0
  19. data/lib/appoptics/metrics/collection.rb +43 -0
  20. data/lib/appoptics/metrics/connection.rb +101 -0
  21. data/lib/appoptics/metrics/errors.rb +32 -0
  22. data/lib/appoptics/metrics/middleware/count_requests.rb +28 -0
  23. data/lib/appoptics/metrics/middleware/expects_status.rb +38 -0
  24. data/lib/appoptics/metrics/middleware/request_body.rb +18 -0
  25. data/lib/appoptics/metrics/middleware/retry.rb +31 -0
  26. data/lib/appoptics/metrics/persistence.rb +2 -0
  27. data/lib/appoptics/metrics/persistence/direct.rb +73 -0
  28. data/lib/appoptics/metrics/persistence/test.rb +27 -0
  29. data/lib/appoptics/metrics/processor.rb +130 -0
  30. data/lib/appoptics/metrics/queue.rb +191 -0
  31. data/lib/appoptics/metrics/smart_json.rb +43 -0
  32. data/lib/appoptics/metrics/util.rb +25 -0
  33. data/lib/appoptics/metrics/version.rb +5 -0
  34. data/spec/integration/metrics/annotator_spec.rb +190 -0
  35. data/spec/integration/metrics/connection_spec.rb +14 -0
  36. data/spec/integration/metrics/middleware/count_requests_spec.rb +28 -0
  37. data/spec/integration/metrics/queue_spec.rb +96 -0
  38. data/spec/integration/metrics_spec.rb +375 -0
  39. data/spec/rackups/status.ru +30 -0
  40. data/spec/spec_helper.rb +88 -0
  41. data/spec/unit/metrics/aggregator_spec.rb +417 -0
  42. data/spec/unit/metrics/client_spec.rb +127 -0
  43. data/spec/unit/metrics/connection_spec.rb +113 -0
  44. data/spec/unit/metrics/queue/autosubmission_spec.rb +57 -0
  45. data/spec/unit/metrics/queue_spec.rb +593 -0
  46. data/spec/unit/metrics/smart_json_spec.rb +79 -0
  47. data/spec/unit/metrics/util_spec.rb +23 -0
  48. data/spec/unit/metrics_spec.rb +63 -0
  49. metadata +135 -0
@@ -0,0 +1,30 @@
1
+ require 'sinatra'
2
+
3
+ class App < Sinatra::Base
4
+ get('/v1/success') do
5
+ status 200
6
+ end
7
+
8
+ post('/v1/forbidden') do
9
+ status 403
10
+ end
11
+
12
+ get('/v1/not_found') do
13
+ status 404
14
+ end
15
+
16
+ post('/v1/service_unavailable') do
17
+ status 503
18
+ end
19
+
20
+ post('/v1/retry_body') do
21
+ body = request.env["rack.input"].read
22
+ if body.empty?
23
+ status 504 # body not sent
24
+ else
25
+ status 502 # body sent
26
+ end
27
+ end
28
+ end
29
+
30
+ run App
@@ -0,0 +1,88 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ # only load pry for MRI > 1.8
4
+ require 'pry' if RUBY_ENGINE == 'ruby' rescue nil
5
+ require 'popen4'
6
+ require 'rspec'
7
+ require 'rspec/mocks/standalone'
8
+ require 'set'
9
+
10
+ require 'appoptics-api-ruby'
11
+
12
+ RSpec.configure do |config|
13
+
14
+ # only accept expect syntax instead of should
15
+ config.expect_with :rspec do |c|
16
+ c.syntax = :expect
17
+ end
18
+
19
+ # purge all metrics from test account
20
+ def delete_all_metrics
21
+ connection = Appoptics::Metrics.client.connection
22
+ Appoptics::Metrics.metrics.each do |metric|
23
+ #puts "deleting #{metric['name']}..."
24
+ # expects 204
25
+ connection.delete("metrics/#{metric['name']}")
26
+ end
27
+ end
28
+
29
+ # purge all annotations from test account
30
+ def delete_all_annotations
31
+ annotator = Appoptics::Metrics::Annotator.new
32
+ streams = annotator.list
33
+ if streams['annotations']
34
+ names = streams['annotations'].map{|s| s['name']}
35
+ names.each { |name| annotator.delete name}
36
+ end
37
+ end
38
+
39
+ # set up test account credentials for integration tests
40
+ def prep_integration_tests
41
+ raise 'no TEST_API_USER specified in environment' unless ENV['TEST_API_USER']
42
+ raise 'no TEST_API_KEY specified in environment' unless ENV['TEST_API_KEY']
43
+ if ENV['TEST_API_ENDPOINT']
44
+ Appoptics::Metrics.api_endpoint = ENV['TEST_API_ENDPOINT']
45
+ end
46
+ Appoptics::Metrics.authenticate ENV['TEST_API_USER'], ENV['TEST_API_KEY']
47
+ end
48
+
49
+ def rackup_path(*parts)
50
+ File.expand_path(File.join(File.dirname(__FILE__), 'rackups', *parts))
51
+ end
52
+
53
+ # fire up a given rackup file for the enclosed tests
54
+ def with_rackup(name)
55
+ if RUBY_PLATFORM == 'java'
56
+ pid, w, r, e = IO.popen4("rackup", rackup_path(name), '-p 9296')
57
+ else
58
+ GC.disable
59
+ pid, w, r, e = Open4.popen4("rackup", rackup_path(name), '-p 9296')
60
+ end
61
+ until e.gets =~ /HTTPServer#start:/; end
62
+ yield
63
+ ensure
64
+ Process.kill(9, pid)
65
+ if RUBY_PLATFORM != 'java'
66
+ GC.enable
67
+ Process.wait(pid)
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ # Compares hashes of arrays by converting the arrays to
74
+ # sets before comparision
75
+ #
76
+ # @example
77
+ # {foo: [1,3,2]}.should equal_unordered({foo: [1,2,3]})
78
+ RSpec::Matchers.define :equal_unordered do |result|
79
+ result.each do |key, value|
80
+ result[key] = value.to_set if value.respond_to?(:to_set)
81
+ end
82
+ match do |target|
83
+ target.each do |key, value|
84
+ target[key] = value.to_set if value.respond_to?(:to_set)
85
+ end
86
+ target == result
87
+ end
88
+ end
@@ -0,0 +1,417 @@
1
+ require "spec_helper.rb"
2
+ module Appoptics
3
+ module Metrics
4
+
5
+ describe Aggregator do
6
+
7
+ before(:all) do
8
+ @time = 1354720160 #Time.now.to_i
9
+ allow_any_instance_of(Aggregator).to receive(:epoch_time).and_return(@time)
10
+ end
11
+
12
+ describe "initialization" do
13
+ context "with specified client" do
14
+ it "sets to client" do
15
+ barney = Client.new
16
+ a = Aggregator.new(client: barney)
17
+ expect(a.client).to eq(barney)
18
+ end
19
+ end
20
+
21
+ context "without specified client" do
22
+ it "uses Appoptics::Metrics client" do
23
+ a = Aggregator.new
24
+ expect(a.client).to eq(Appoptics::Metrics.client)
25
+ end
26
+ end
27
+
28
+ context "with specified source" do
29
+ it "sets to source" do
30
+ a = Aggregator.new(source: 'rubble')
31
+ expect(a.source).to eq('rubble')
32
+ end
33
+ end
34
+
35
+ context "without specified source" do
36
+ it "does not have a source" do
37
+ a = Aggregator.new
38
+ expect(a.source).to be_nil
39
+ end
40
+ end
41
+
42
+ context "with valid arguments" do
43
+ it "initializes Aggregator" do
44
+ expect { Aggregator.new }.not_to raise_error
45
+ expect { Aggregator.new(source: "metrics-web-stg-1") }.not_to raise_error
46
+ expect { Aggregator.new(tags: { hostname: "metrics-web-stg-1" }) }.not_to raise_error
47
+ end
48
+ end
49
+
50
+ context "with invalid arguments" do
51
+ it "raises exception" do
52
+ expect {
53
+ Aggregator.new(
54
+ source: "metrics-web-stg-1",
55
+ tags: { hostname: "metrics-web-stg-1" }
56
+ )
57
+ }.to raise_error(InvalidParameters)
58
+ end
59
+ end
60
+ end
61
+
62
+ describe "#tags" do
63
+ context "when set" do
64
+ let(:aggregator) { Aggregator.new(tags: { instance_id: "i-1234567a" }) }
65
+ it "gets @tags" do
66
+ expect(aggregator.tags).to be_a(Hash)
67
+ expect(aggregator.tags.keys).to include(:instance_id)
68
+ expect(aggregator.tags[:instance_id]).to eq("i-1234567a")
69
+ end
70
+ end
71
+
72
+ context "when not set" do
73
+ let(:aggregator) { Aggregator.new }
74
+ it "defaults to empty hash" do
75
+ expect(aggregator.tags).to be_a(Hash)
76
+ expect(aggregator.tags).to be_empty
77
+ end
78
+ end
79
+ end
80
+
81
+ describe "#tags=" do
82
+ it "sets @tags" do
83
+ expected_tags = { instance_id: "i-1234567b" }
84
+ expect{subject.tags = expected_tags}.to change{subject.tags}.from({}).to(expected_tags)
85
+ expect(subject.tags).to be_a(Hash)
86
+ expect(subject.tags).to eq(expected_tags)
87
+ end
88
+ end
89
+
90
+ describe "#has_tags?" do
91
+ context "when tags are set" do
92
+ it "returns true" do
93
+ subject.tags = { instance_id: "i-1234567f" }
94
+
95
+ expect(subject.has_tags?).to eq(true)
96
+ end
97
+ end
98
+
99
+ context "when tags are not set" do
100
+ it "returns false" do
101
+ expect(subject.has_tags?).to eq(false)
102
+ end
103
+ end
104
+ end
105
+
106
+ describe "#add" do
107
+ it "allows chaining" do
108
+ expect(subject.add(foo: 1234)).to eq(subject)
109
+ end
110
+
111
+ context "with invalid arguments" do
112
+ it "raises exception" do
113
+ expect {
114
+ subject.add test: { source: "metrics-web-stg-1", tags: { hostname: "metrics-web-stg-1" }, value: 123 }
115
+ }.to raise_error(InvalidParameters)
116
+ end
117
+ end
118
+
119
+ context "with single hash argument" do
120
+ it "records a single aggregate" do
121
+ subject.add foo: 3000
122
+ expected = { #measure_time: @time, TODO: support specific time
123
+ gauges: [
124
+ { name: 'foo',
125
+ count: 1,
126
+ sum: 3000.0,
127
+ min: 3000.0,
128
+ max: 3000.0}
129
+ ]
130
+ }
131
+ expect(subject.queued).to equal_unordered(expected)
132
+ end
133
+
134
+ it "aggregates multiple measurements" do
135
+ subject.add foo: 1
136
+ subject.add foo: 2
137
+ subject.add foo: 3
138
+ subject.add foo: 4
139
+ subject.add foo: 5
140
+ expected = { gauges: [
141
+ { name: 'foo',
142
+ count: 5,
143
+ sum: 15.0,
144
+ min: 1.0,
145
+ max: 5.0}
146
+ ]
147
+ }
148
+ expect(subject.queued).to equal_unordered(expected)
149
+ end
150
+
151
+ it "respects source argument" do
152
+ subject.add foo: {source: 'alpha', value: 1}
153
+ subject.add foo: 5
154
+ subject.add foo: {source: :alpha, value: 6}
155
+ subject.add foo: 10
156
+ expected = { gauges: [
157
+ { name: 'foo', source: 'alpha', count: 2,
158
+ sum: 7.0, min: 1.0, max: 6.0 },
159
+ { name: 'foo', count: 2,
160
+ sum: 15.0, min: 5.0, max: 10.0 }
161
+ ]}
162
+ expect(subject.queued).to equal_unordered(expected)
163
+ end
164
+
165
+ context "when per-measurement tags" do
166
+ it "maintains specified tags" do
167
+ subject.add test: { tags: { hostname: "metrics-web-stg-1" }, value: 1 }
168
+ subject.add test: 5
169
+ subject.add test: { tags: { hostname: "metrics-web-stg-1" }, value: 6 }
170
+ subject.add test: 10
171
+ expected = [
172
+ { name: "test", tags: { hostname: "metrics-web-stg-1" }, count: 2, sum: 7.0, min: 1.0, max: 6.0 },
173
+ { name: "test", count: 2, sum: 15.0, min: 5.0, max: 10.0 }
174
+ ]
175
+
176
+ expect(subject.queued[:measurements]).to equal_unordered(expected)
177
+ end
178
+ end
179
+
180
+ context "with a prefix set" do
181
+ it "auto-prepends names" do
182
+ subject = Aggregator.new(prefix: 'foo')
183
+ subject.add bar: 1
184
+ subject.add bar: 12
185
+ expected = {gauges: [
186
+ { name:'foo.bar',
187
+ count: 2,
188
+ sum: 13.0,
189
+ min: 1.0,
190
+ max: 12.0
191
+ }
192
+ ]
193
+ }
194
+ expect(subject.queued).to equal_unordered(expected)
195
+ end
196
+ end
197
+ end
198
+
199
+ context "with multiple hash arguments" do
200
+ it "records a single aggregate" do
201
+ subject.add foo: 3000
202
+ subject.add bar: 30
203
+ expected = {
204
+ #measure_time: @time, TODO: support specific time
205
+ gauges: [
206
+ { name: 'foo',
207
+ count: 1,
208
+ sum: 3000.0,
209
+ min: 3000.0,
210
+ max: 3000.0},
211
+ { name: 'bar',
212
+ count: 1,
213
+ sum: 30.0,
214
+ min: 30.0,
215
+ max: 30.0},
216
+ ]
217
+ }
218
+ expect(subject.queued).to equal_unordered(expected)
219
+ end
220
+
221
+ it "aggregates multiple measurements" do
222
+ subject.add foo: 1
223
+ subject.add foo: 2
224
+ subject.add foo: 3
225
+ subject.add foo: 4
226
+ subject.add foo: 5
227
+
228
+ subject.add bar: 6
229
+ subject.add bar: 7
230
+ subject.add bar: 8
231
+ subject.add bar: 9
232
+ subject.add bar: 10
233
+ expected = { gauges: [
234
+ { name: 'foo',
235
+ count: 5,
236
+ sum: 15.0,
237
+ min: 1.0,
238
+ max: 5.0},
239
+ { name: 'bar',
240
+ count: 5,
241
+ sum: 40.0,
242
+ min: 6.0,
243
+ max: 10.0}
244
+ ]
245
+ }
246
+ expect(subject.queued).to equal_unordered(expected)
247
+ end
248
+ end
249
+
250
+ context "with tags" do
251
+ context "when Aggregator is initialized with tags" do
252
+ let(:aggregator) { Aggregator.new(tags: { region: "us-east-1" }) }
253
+
254
+ it "applies top-level tags" do
255
+ expected = { name: "test", count: 2, sum: 3, min: 1, max: 2 }
256
+ aggregator.add test: 1
257
+ aggregator.add test: 2
258
+
259
+ expect(aggregator.queued[:tags]).to eq({ region: "us-east-1" })
260
+ expect(aggregator.queued[:measurements].first).to eq(expected)
261
+ end
262
+ end
263
+
264
+ context "when tags are used as arguments" do
265
+ let(:aggregator) { Aggregator.new }
266
+
267
+ it "applies per-measurement tags" do
268
+ expected = { name: "test", count: 2, sum: 3, min: 1, max: 2, tags: { hostname: "metrics-web-stg-1" } }
269
+ aggregator.add test: { value: 1, tags: { hostname: "metrics-web-stg-1" } }
270
+ aggregator.add test: { value: 2, tags: { hostname: "metrics-web-stg-1" } }
271
+
272
+ expect(aggregator.queued[:tags]).to be_nil
273
+ expect(aggregator.queued[:measurements].first).to eq(expected)
274
+ end
275
+
276
+ context "when tags arguments are not sorted" do
277
+ let(:aggregator) { Aggregator.new }
278
+
279
+ it "uses sorted tags hash key" do
280
+ expected = { name: "test", count: 2, sum: 3, min: 1, max: 2, tags: { a: 1, b: 2, c: 3 } }
281
+ aggregator.add test: { value: 1, tags: { c: 3, b: 2, a: 1 } }
282
+ aggregator.add test: { value: 2, tags: { b: 2, a: 1, c: 3 } }
283
+
284
+ expect(aggregator.queued[:tags]).to be_nil
285
+ expect(aggregator.queued[:measurements].first).to eq(expected)
286
+ end
287
+ end
288
+ end
289
+
290
+ context "when Aggregator is initialized with tags and when tags are used as arguments" do
291
+ let(:aggregator) { Aggregator.new(tags: { region: "us-east-1" }) }
292
+
293
+ it "applies top-level tags and per-measurement tags" do
294
+ expected = { name: "test", count: 3, sum: 12, min: 3, max: 5, tags: { hostname: "metrics-web-stg-1" } }
295
+ aggregator.add test: { value: 3, tags: { hostname: "metrics-web-stg-1" } }
296
+ aggregator.add test: { value: 4, tags: { hostname: "metrics-web-stg-1" } }
297
+ aggregator.add test: { value: 5, tags: { hostname: "metrics-web-stg-1" } }
298
+ aggregator.add test: { value: 1, tags: { hostname: "metrics-web-stg-2" } }
299
+ aggregator.add test: { value: 2, tags: { region: "us-tirefire-1" } }
300
+
301
+ expect(aggregator.queued[:tags]).to eq({ region: "us-east-1" })
302
+ expect(aggregator.queued[:measurements].first).to eq(expected)
303
+ end
304
+ end
305
+ end
306
+ end
307
+
308
+ describe "#queued" do
309
+ it "includes global source if set" do
310
+ a = Aggregator.new(source: 'blah')
311
+ a.add foo: 12
312
+ expect(a.queued[:source]).to eq('blah')
313
+ end
314
+
315
+ it "includes global measure_time if set" do
316
+ measure_time = (Time.now-1000).to_i
317
+ a = Aggregator.new(source: "foo", measure_time: measure_time)
318
+ a.add foo: 12
319
+ expect(a.queued[:measure_time]).to eq(measure_time)
320
+ end
321
+
322
+ context "when tags are set" do
323
+ it "includes global tags" do
324
+ expected_tags = { region: "us-east-1" }
325
+ subject = Aggregator.new(tags: expected_tags)
326
+ subject.add test: 5
327
+
328
+ expect(subject.queued[:tags]).to eq(expected_tags)
329
+ end
330
+ end
331
+
332
+ context "when time is set" do
333
+ it "includes global time" do
334
+ expected_time = (Time.now-1000).to_i
335
+ subject = Aggregator.new(tags: { foo: "bar" }, time: expected_time)
336
+ subject.add test: 10
337
+
338
+ expect(subject.queued[:time]).to eq(expected_time)
339
+ end
340
+ end
341
+ end
342
+
343
+ describe "#submit" do
344
+ before(:all) do
345
+ Appoptics::Metrics.authenticate 'me@AppOptics.com', 'foo'
346
+ Appoptics::Metrics.persistence = :test
347
+ end
348
+
349
+ context "when successful" do
350
+ it "flushes queued metrics and return true" do
351
+ subject.add steps: 2042, distance: 1234
352
+ expect(subject.submit).to be true
353
+ expect(subject.empty?).to be true
354
+ end
355
+ end
356
+
357
+ context "when failed" do
358
+ it "preserves queue and return false" do
359
+ subject.add steps: 2042, distance: 1234
360
+ subject.persister.return_value(false)
361
+ expect(subject.submit).to be false
362
+ expect(subject.empty?).to be false
363
+ end
364
+ end
365
+ end
366
+
367
+ describe "#time" do
368
+ context "with metric name only" do
369
+ it "queues metric with timed value" do
370
+ 1.upto(5) do
371
+ subject.time :sleeping do
372
+ sleep 0.1
373
+ end
374
+ end
375
+ queued = subject.queued[:gauges][0]
376
+ expect(queued[:name]).to eq('sleeping')
377
+ expect(queued[:count]).to eq(5)
378
+ expect(queued[:sum]).to be >= 500.0
379
+ expect(queued[:sum]).to be_within(150).of(500)
380
+ end
381
+
382
+ it "returns the result of the block" do
383
+ result = subject.time :returning do
384
+ :hi_there
385
+ end
386
+
387
+ expect(result).to eq(:hi_there)
388
+ end
389
+ end
390
+ end
391
+
392
+ context "with an autosubmit interval" do
393
+ let(:client) do
394
+ client = Client.new
395
+ client.persistence = :test
396
+ client
397
+ end
398
+
399
+ it "does not submit immediately" do
400
+ timed_agg = Aggregator.new(client: client, autosubmit_interval: 1)
401
+ timed_agg.add foo: 1
402
+ expect(timed_agg.persister.persisted).to be_nil # nothing sent
403
+ end
404
+
405
+ it "submits after interval" do
406
+ timed_agg = Aggregator.new(client: client, autosubmit_interval: 1)
407
+ timed_agg.add foo: 1
408
+ sleep 1
409
+ timed_agg.add foo: 2
410
+ expect(timed_agg.persister.persisted).not_to be_nil # sent
411
+ end
412
+ end
413
+
414
+ end
415
+
416
+ end
417
+ end