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,127 @@
1
+ require 'spec_helper'
2
+
3
+ module Appoptics
4
+ module Metrics
5
+
6
+ describe Client do
7
+
8
+ describe "#agent_identifier" do
9
+ context "when given a single string argument" do
10
+ it "sets agent_identifier" do
11
+ subject.agent_identifier 'mycollector/0.1 (dev_id:foo)'
12
+ expect(subject.agent_identifier).to eq('mycollector/0.1 (dev_id:foo)')
13
+ end
14
+ end
15
+
16
+ context "when given three arguments" do
17
+ it "composes an agent string" do
18
+ subject.agent_identifier('test_app', '0.5', 'foobar')
19
+ expect(subject.agent_identifier).to eq('test_app/0.5 (dev_id:foobar)')
20
+ end
21
+
22
+ context "when given an empty string" do
23
+ it "sets to empty" do
24
+ subject.agent_identifier ''
25
+ expect(subject.agent_identifier).to be_empty
26
+ end
27
+ end
28
+ end
29
+
30
+ context "when given two arguments" do
31
+ it "raises error" do
32
+ expect { subject.agent_identifier('test_app', '0.5') }.to raise_error(ArgumentError)
33
+ end
34
+ end
35
+ end
36
+
37
+ describe "#api_endpoint" do
38
+ it "defaults to metrics" do
39
+ expect(subject.api_endpoint).to eq('https://api.appoptics.com')
40
+ end
41
+ end
42
+
43
+ describe "#api_endpoint=" do
44
+ it "sets api_endpoint" do
45
+ subject.api_endpoint = 'http://test.com/'
46
+ expect(subject.api_endpoint).to eq('http://test.com/')
47
+ end
48
+
49
+ # TODO:
50
+ # it "should ensure trailing slash"
51
+ # it "should ensure real URI"
52
+ end
53
+
54
+ describe "#authenticate" do
55
+ context "when given two arguments" do
56
+ it "stores them as email and api_key" do
57
+ subject.authenticate 'test@Appoptics.com', 'api_key'
58
+ expect(subject.email).to eq('test@Appoptics.com')
59
+ expect(subject.api_key).to eq('api_key')
60
+ end
61
+ end
62
+ end
63
+
64
+ describe "#connection" do
65
+ it "raises exception without authentication" do
66
+ subject.flush_authentication
67
+ expect { subject.connection }.to raise_error(Appoptics::Metrics::CredentialsMissing)
68
+ end
69
+ end
70
+
71
+ describe "#faraday_adapter" do
72
+ it "defaults to Metrics default adapter" do
73
+ Metrics.faraday_adapter = :typhoeus
74
+ expect(Client.new.faraday_adapter).to eq(Metrics.faraday_adapter)
75
+ Metrics.faraday_adapter = nil
76
+ end
77
+ end
78
+
79
+ describe "#faraday_adapter=" do
80
+ it "allows setting of faraday adapter" do
81
+ subject.faraday_adapter = :excon
82
+ expect(subject.faraday_adapter).to eq(:excon)
83
+ subject.faraday_adapter = :patron
84
+ expect(subject.faraday_adapter).to eq(:patron)
85
+ end
86
+ end
87
+
88
+ describe "#new_queue" do
89
+ it "returns a new queue with client set" do
90
+ queue = subject.new_queue
91
+ expect(queue.client).to eq(subject)
92
+ end
93
+ end
94
+
95
+ describe "#persistence" do
96
+ it "defaults to direct" do
97
+ subject.send(:flush_persistence)
98
+ expect(subject.persistence).to eq(:direct)
99
+ end
100
+
101
+ it "allows configuration of persistence method" do
102
+ subject.persistence = :fake
103
+ expect(subject.persistence).to eq(:fake)
104
+ end
105
+ end
106
+
107
+ describe "#submit" do
108
+ it "persists metrics immediately" do
109
+ subject.authenticate 'me@Appoptics.com', 'foo'
110
+ subject.persistence = :test
111
+ expect(subject.submit(foo: 123)).to be true
112
+ expect(subject.persister.persisted).to eq({gauges: [{name: 'foo', value: 123}]})
113
+ end
114
+
115
+ it "tolerates muliple metrics" do
116
+ subject.authenticate 'me@Appoptics.com', 'foo'
117
+ subject.persistence = :test
118
+ expect { subject.submit foo: 123, bar: 456 }.not_to raise_error
119
+ expected = {gauges: [{name: 'foo', value: 123}, {name: 'bar', value: 456}]}
120
+ expect(subject.persister.persisted).to equal_unordered(expected)
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+ end
127
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ module Appoptics
4
+ module Metrics
5
+
6
+ describe Connection do
7
+
8
+ describe "#api_endpoint" do
9
+ context "when not provided" do
10
+ it "uses default" do
11
+ expect(subject.api_endpoint).to eq('https://api.appoptics.com')
12
+ end
13
+ end
14
+
15
+ context "when provided" do
16
+ it "uses provided endpoint" do
17
+ connection = Connection.new(api_endpoint: 'http://test.com/')
18
+ expect(connection.api_endpoint).to eq('http://test.com/')
19
+ end
20
+ end
21
+ end
22
+
23
+ describe "#user_agent" do
24
+ context "without an agent_identifier" do
25
+ it "renders standard string" do
26
+ connection = Connection.new(client: Client.new)
27
+ expect(connection.user_agent).to start_with('appoptics-api-ruby')
28
+ end
29
+ end
30
+
31
+ context "with an agent_identifier" do
32
+ it "renders agent_identifier first" do
33
+ client = Client.new
34
+ client.agent_identifier('foo', '0.5', 'bar')
35
+ connection = Connection.new(client: client)
36
+ expect(connection.user_agent).to start_with('foo/0.5')
37
+ end
38
+ end
39
+
40
+ context "with a custom user agent set" do
41
+ it "uses custom user agent" do
42
+ client = Client.new
43
+ client.custom_user_agent = 'foo agent'
44
+ connection = Connection.new(client: client)
45
+ expect(connection.user_agent).to eq('foo agent')
46
+ end
47
+ end
48
+
49
+ # TODO: verify user agent is being sent with rackup test
50
+ end
51
+
52
+ describe "network operations" do
53
+ context "when missing client" do
54
+ it "raises exception" do
55
+ expect { subject.get 'metrics' }.to raise_error(NoClientProvided)
56
+ end
57
+ end
58
+
59
+ let(:client) do
60
+ client = Client.new
61
+ client.api_endpoint = 'http://127.0.0.1:9296'
62
+ client.authenticate 'foo', 'bar'
63
+ client
64
+ end
65
+
66
+ context "with 400 class errors" do
67
+ it "does not retry" do
68
+ Middleware::CountRequests.reset
69
+ with_rackup('status.ru') do
70
+ expect {
71
+ client.connection.transport.post 'not_found'
72
+ }.to raise_error(NotFound)
73
+ expect {
74
+ client.connection.transport.post 'forbidden'
75
+ }.to raise_error(ClientError)
76
+ end
77
+ expect(Middleware::CountRequests.total_requests).to eq(2) # no retries
78
+ end
79
+ end
80
+
81
+ context "with 500 class errors" do
82
+ it "retries" do
83
+ Middleware::CountRequests.reset
84
+ with_rackup('status.ru') do
85
+ expect {
86
+ client.connection.transport.post 'service_unavailable'
87
+ }.to raise_error(ServerError)
88
+ end
89
+ expect(Middleware::CountRequests.total_requests).to eq(4) # did retries
90
+ end
91
+
92
+ it "sends consistent body with retries" do
93
+ Middleware::CountRequests.reset
94
+ status = 0
95
+ begin
96
+ with_rackup('status.ru') do
97
+ response = client.connection.transport.post do |req|
98
+ req.url 'retry_body'
99
+ req.body = '{"foo": "bar", "baz": "kaboom"}'
100
+ end
101
+ end
102
+ rescue Exception => error
103
+ status = error.response[:status].to_i
104
+ end
105
+ expect(Middleware::CountRequests.total_requests).to eq(4) # did retries
106
+ expect(status).to eq(502) # body is sent for retries
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ end
113
+ end
@@ -0,0 +1,57 @@
1
+ require "spec_helper"
2
+
3
+ module Appoptics
4
+ module Metrics
5
+
6
+ describe Queue do
7
+
8
+ let(:client) { Client.new.tap{ |c| c.persistence = :test } }
9
+
10
+ context "with an autosubmit count" do
11
+ it "submits when the max is reached" do
12
+ vol_queue = Queue.new(client: client, autosubmit_count: 2)
13
+ vol_queue.add foo: 1
14
+ vol_queue.add bar: 2
15
+ expect(vol_queue.persister.persisted).not_to be_nil # sent
16
+ end
17
+
18
+ it "does not submit if the max has not been reached" do
19
+ vol_queue = Queue.new(client: client, autosubmit_count: 5)
20
+ vol_queue.add foo: 1
21
+ vol_queue.add bar: 2
22
+ expect(vol_queue.persister.persisted).to be_nil # nothing sent
23
+ end
24
+
25
+ it 'submits when merging' do
26
+ queue = Queue.new(client: client, autosubmit_count: 5)
27
+ (1..3).each {|i| queue.add "metric_#{i}" => 1 }
28
+
29
+ to_merge = Queue.new(client: client)
30
+ (1..5).each {|i| to_merge.add "metric_#{i}" => 1 }
31
+
32
+ queue.merge!(to_merge)
33
+
34
+ expect(queue.persister.persisted[:gauges].length).to eq(8)
35
+ expect(queue.queued).to be_empty
36
+ end
37
+ end
38
+
39
+ context "with an autosubmit interval" do
40
+ it "does not submit immediately" do
41
+ vol_queue = Queue.new(client: client, autosubmit_interval: 1)
42
+ vol_queue.add foo: 1
43
+ expect(vol_queue.persister.persisted).to be_nil # nothing sent
44
+ end
45
+
46
+ it "submits after interval" do
47
+ vol_queue = Queue.new(client: client, autosubmit_interval: 1)
48
+ vol_queue.add foo: 1
49
+ sleep 1
50
+ vol_queue.add foo: 2
51
+ expect(vol_queue.persister.persisted).not_to be_nil # sent
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,593 @@
1
+ require "spec_helper"
2
+
3
+ module Appoptics
4
+ module Metrics
5
+
6
+ describe Queue do
7
+
8
+ before(:all) do
9
+ @time = (Time.now.to_i - 1*60)
10
+ allow_any_instance_of(Queue).to receive(:epoch_time).and_return(@time)
11
+ end
12
+
13
+ describe "initialization" do
14
+ context "with specified client" do
15
+ let(:barney) { Client }
16
+ let(:queue) { Queue.new(client: barney) }
17
+ before do
18
+ allow(barney).to receive(:has_tags?).and_return(false)
19
+ allow(barney).to receive(:tags).and_return({})
20
+ allow(barney).to receive(:add_tags).and_return({})
21
+ end
22
+
23
+ it "sets to client" do
24
+ expect(queue.client).to eq(barney)
25
+ end
26
+ end
27
+
28
+ context "without specified client" do
29
+ it "uses Appoptics::Metrics client" do
30
+ queue = Queue.new
31
+ expect(queue.client).to eq(Appoptics::Metrics.client)
32
+ end
33
+ end
34
+
35
+ context "with valid arguments" do
36
+ it "initializes Queue" do
37
+ expect { Queue.new }.not_to raise_error
38
+ expect { Queue.new(source: "metrics-web-stg-1") }.not_to raise_error
39
+ expect { Queue.new(tags: { hostname: "metrics-web-stg-1" }) }.not_to raise_error
40
+ end
41
+ end
42
+
43
+ context "with invalid arguments" do
44
+ it "raises exception" do
45
+ expect {
46
+ Queue.new(
47
+ source: "metrics-web-stg-1",
48
+ tags: { hostname: "metrics-web-stg-1" }
49
+ )
50
+ }.to raise_error(InvalidParameters)
51
+ end
52
+ end
53
+ end
54
+
55
+ describe "#tags" do
56
+ context "when set" do
57
+ let(:queue) { Queue.new(tags: { instance_id: "i-1234567a" }) }
58
+ it "gets @tags" do
59
+ expect(queue.tags).to be_a(Hash)
60
+ expect(queue.tags.keys).to include(:instance_id)
61
+ expect(queue.tags[:instance_id]).to eq("i-1234567a")
62
+ end
63
+ end
64
+
65
+ context "when not set" do
66
+ let(:queue) { Queue.new }
67
+ it "defaults to empty hash" do
68
+ expect(queue.tags).to be_a(Hash)
69
+ expect(queue.tags).to be_empty
70
+ end
71
+ end
72
+ end
73
+
74
+ describe "#tags=" do
75
+ it "sets @tags" do
76
+ expected_tags = { instance_id: "i-1234567b" }
77
+ expect{subject.tags = expected_tags}.to change{subject.tags}.from({}).to(expected_tags)
78
+ expect(subject.tags).to be_a(Hash)
79
+ expect(subject.tags).to eq(expected_tags)
80
+ end
81
+ end
82
+
83
+ describe "#has_tags?" do
84
+ context "when tags are set" do
85
+ it "returns true" do
86
+ subject.tags = { instance_id: "i-1234567f" }
87
+
88
+ expect(subject.has_tags?).to eq(true)
89
+ end
90
+ end
91
+
92
+ context "when tags are not set" do
93
+ it "returns false" do
94
+ expect(subject.has_tags?).to eq(false)
95
+ end
96
+ end
97
+ end
98
+
99
+ describe "#add" do
100
+ it "allows chaining" do
101
+ expect(subject.add(foo: 123)).to eq(subject)
102
+ end
103
+
104
+ context "with invalid arguments" do
105
+ it "raises exception" do
106
+ expect {
107
+ subject.add test: { source: "metrics-web-stg-1", tags: { hostname: "metrics-web-stg-1" }, value: 123 }
108
+ }.to raise_error(InvalidParameters)
109
+ end
110
+ end
111
+
112
+ context "with single hash argument" do
113
+ it "records a key-value gauge" do
114
+ expected = {gauges: [{name: 'foo', value: 3000, measure_time: @time}]}
115
+ subject.add foo: 3000
116
+ expect(subject.queued).to equal_unordered(expected)
117
+ end
118
+ end
119
+
120
+ context "with specified metric type" do
121
+ it "records counters" do
122
+ subject.add total_visits: {type: :counter, value: 4000}
123
+ expected = {counters: [{name: 'total_visits', value: 4000, measure_time: @time}]}
124
+ expect(subject.queued).to equal_unordered(expected)
125
+ end
126
+
127
+ it "records gauges" do
128
+ subject.add temperature: {type: :gauge, value: 34}
129
+ expected = {gauges: [{name: 'temperature', value: 34, measure_time: @time}]}
130
+ expect(subject.queued).to equal_unordered(expected)
131
+ end
132
+
133
+ it "accepts type key as string or a symbol" do
134
+ subject.add total_visits: {type: "counter", value: 4000}
135
+ expected = {counters: [{name: 'total_visits', value: 4000, measure_time: @time}]}
136
+ expect(subject.queued).to equal_unordered(expected)
137
+ end
138
+ end
139
+
140
+ context "with extra attributes" do
141
+ it "records" do
142
+ measure_time = Time.now
143
+ subject.add disk_use: {value: 35.4, period: 2,
144
+ description: 'current disk utilization', measure_time: measure_time,
145
+ source: 'db2'}
146
+ expected = {gauges: [{value: 35.4, name: 'disk_use', period: 2,
147
+ description: 'current disk utilization', measure_time: measure_time.to_i,
148
+ source: 'db2'}]}
149
+ expect(subject.queued).to equal_unordered(expected)
150
+ end
151
+
152
+ context "with a prefix set" do
153
+ it "auto-prepends names" do
154
+ subject = Queue.new(prefix: 'foo')
155
+ subject.add bar: 1
156
+ subject.add baz: {value: 23}
157
+ expected = {gauges: [{name:'foo.bar', value: 1, measure_time: @time},
158
+ {name: 'foo.baz', value: 23, measure_time: @time}]}
159
+ expect(subject.queued).to equal_unordered(expected)
160
+ end
161
+ end
162
+
163
+ context "when dynamically changing prefix" do
164
+ it "auto-appends names" do
165
+ subject.add bar: 12
166
+ subject.prefix = 'foo' # with string
167
+ subject.add bar: 23
168
+ subject.prefix = :foo # with symbol
169
+ subject.add bar: 34
170
+ subject.prefix = nil # unsetting
171
+ subject.add bar: 45
172
+ expected = {gauges: [
173
+ {name: 'bar', value: 12, measure_time: @time},
174
+ {name: 'foo.bar', value: 23, measure_time: @time},
175
+ {name: 'foo.bar', value: 34, measure_time: @time},
176
+ {name: 'bar', value: 45, measure_time: @time}]}
177
+ expect(subject.queued).to equal_unordered(expected)
178
+ end
179
+ end
180
+ end
181
+
182
+ context "with multiple metrics" do
183
+ it "records" do
184
+ subject.add foo: 123, bar: 345, baz: 567
185
+ expected = {gauges:[{name:"foo", value:123, measure_time: @time},
186
+ {name:"bar", value:345, measure_time: @time},
187
+ {name:"baz", value:567, measure_time: @time}]}
188
+ expect(subject.queued).to equal_unordered(expected)
189
+ end
190
+ end
191
+
192
+ context "with a measure_time" do
193
+ it "accepts time objects" do
194
+ time = Time.now-5
195
+ subject.add foo: {measure_time: time, value: 123}
196
+ expect(subject.queued[:gauges][0][:measure_time]).to eq(time.to_i)
197
+ end
198
+
199
+ it "accepts integers" do
200
+ time = @time.to_i
201
+ subject.add foo: {measure_time: time, value: 123}
202
+ expect(subject.queued[:gauges][0][:measure_time]).to eq(time)
203
+ end
204
+
205
+ it "accepts strings" do
206
+ time = @time.to_s
207
+ subject.add foo: {measure_time: time, value: 123}
208
+ expect(subject.queued[:gauges][0][:measure_time]).to eq(time.to_i)
209
+ end
210
+
211
+ it "raises exception in invalid time" do
212
+ expect {
213
+ subject.add foo: {measure_time: '12', value: 123}
214
+ }.to raise_error(InvalidMeasureTime)
215
+ end
216
+ end
217
+
218
+ context "with tags" do
219
+ context "when Queue is initialized with tags" do
220
+ let(:queue) { Queue.new(tags: { region: "us-east-1" }) }
221
+
222
+ it "applies top-level tags" do
223
+ expected = { name: "test", value: 1, time: @time }
224
+ queue.add test: 1
225
+
226
+ expect(queue.queued[:tags]).to eq({ region: "us-east-1" })
227
+ expect(queue.queued[:measurements].first).to eq(expected)
228
+ end
229
+ end
230
+
231
+ context "when tags are used as arguments" do
232
+ let(:queue) { Queue.new }
233
+
234
+ it "applies per-measurement tags" do
235
+ expected = { name: "test", value: 2, tags: { hostname: "metrics-web-stg-1" }, time: @time }
236
+ queue.add test: { value: 2, tags: { hostname: "metrics-web-stg-1" } }
237
+
238
+ expect(queue.queued[:tags]).to be_nil
239
+ expect(queue.queued[:measurements].first).to eq(expected)
240
+ end
241
+
242
+ it "converts legacy measure_time to time" do
243
+ expected_time = Time.now.to_i
244
+ expected_tags = { foo: "bar" }
245
+ expected = {
246
+ measurements: [{
247
+ name: "test", value: 1, tags: expected_tags, time: expected_time
248
+ }]
249
+ }
250
+
251
+ subject.add test: { value: 1, tags: expected_tags, measure_time: expected_time }
252
+
253
+ expect(subject.queued).to equal_unordered(expected)
254
+ end
255
+ end
256
+
257
+ context "when Queue is initialized with tags and when tags are used as arguments" do
258
+ let(:queue) { Queue.new(tags: { region: "us-east-1" }) }
259
+
260
+ it "applies top-level tags and per-measurement tags" do
261
+ expected = { name: "test", value: 3, tags: { hostname: "metrics-web-stg-1" }, time: @time }
262
+ queue.add test: { value: 3, tags: { hostname: "metrics-web-stg-1" } }
263
+
264
+ expect(queue.queued[:tags]).to eq({ region: "us-east-1" })
265
+ expect(queue.queued[:measurements].first).to eq(expected)
266
+ end
267
+ end
268
+ end
269
+ end
270
+
271
+ describe "#measurements" do
272
+ it "returns currently queued measurements" do
273
+ subject.add test_1: { tags: { region: "us-east-1" }, value: 1 },
274
+ test_2: { type: :counter, value: 2 }
275
+ expect(subject.measurements).to eq([{ name: "test_1", value: 1, tags: { region: "us-east-1" }, time: @time }])
276
+ end
277
+
278
+ it "returns [] when no queued measurements" do
279
+ expect(subject.measurements).to be_empty
280
+ end
281
+ end
282
+
283
+ describe "#counters" do
284
+ it "returns currently queued counters" do
285
+ subject.add transactions: {type: :counter, value: 12345},
286
+ register_cents: {type: :gauge, value: 211101}
287
+ expect(subject.counters).to eq([{name: 'transactions', value: 12345, measure_time: @time}])
288
+ end
289
+
290
+ it "returns [] when no queued counters" do
291
+ expect(subject.counters).to be_empty
292
+ end
293
+ end
294
+
295
+ describe "#empty?" do
296
+ it "returns true when nothing queued" do
297
+ expect(subject.empty?).to be true
298
+ end
299
+
300
+ it "returns false with queued items" do
301
+ subject.add foo: {type: :gauge, value: 121212}
302
+ expect(subject.empty?).to be false
303
+ end
304
+
305
+ it "returns true when nothing merged" do
306
+ subject.merge!(Appoptics::Metrics::Aggregator.new)
307
+ expect(subject.empty?).to be true
308
+ end
309
+ end
310
+
311
+ describe "#gauges" do
312
+ it "returns currently queued gauges" do
313
+ subject.add transactions: {type: :counter, value: 12345},
314
+ register_cents: {type: :gauge, value: 211101}
315
+ expect(subject.gauges).to eq([{name: 'register_cents', value: 211101, measure_time: @time}])
316
+ end
317
+
318
+ it "returns [] when no queued gauges" do
319
+ expect(subject.gauges).to be_empty
320
+ end
321
+
322
+ context "when there are no metrics" do
323
+ it "it does not persist and returns true" do
324
+ subject.merge!(Appoptics::Metrics::Aggregator.new)
325
+ subject.persister.return_value(false)
326
+ expect(subject.submit).to be true
327
+ end
328
+ end
329
+ end
330
+
331
+ describe "#last_submit_time" do
332
+ before(:all) do
333
+ Appoptics::Metrics.authenticate 'me@Appoptics.com', 'foo'
334
+ Appoptics::Metrics.persistence = :test
335
+ end
336
+
337
+ it "defaults to nil" do
338
+ expect(subject.last_submit_time).to be_nil
339
+ end
340
+
341
+ it "stores last submission time" do
342
+ prior = Time.now
343
+ subject.add foo: 123
344
+ subject.submit
345
+ expect(subject.last_submit_time).to be >= prior
346
+ end
347
+ end
348
+
349
+ describe "#merge!" do
350
+ context "with another queue" do
351
+ it "merges gauges" do
352
+ q1 = Queue.new
353
+ q1.add foo: 123, bar: 456
354
+ q2 = Queue.new
355
+ q2.add baz: 678
356
+ q2.merge!(q1)
357
+ expected = {gauges:[{name:"foo", value:123, measure_time: @time},
358
+ {name:"bar", value:456, measure_time: @time},
359
+ {name:"baz", value:678, measure_time: @time}]}
360
+ expect(q2.queued).to equal_unordered(expected)
361
+ end
362
+
363
+ it "merges counters" do
364
+ q1 = Queue.new
365
+ q1.add users: {type: :counter, value: 1000}
366
+ q1.add sales: {type: :counter, value: 250}
367
+ q2 = Queue.new
368
+ q2.add signups: {type: :counter, value: 500}
369
+ q2.merge!(q1)
370
+ expected = {counters:[{name:"users", value:1000, measure_time: @time},
371
+ {name:"sales", value:250, measure_time: @time},
372
+ {name:"signups", value:500, measure_time: @time}]}
373
+ expect(q2.queued).to equal_unordered(expected)
374
+ end
375
+
376
+ context "with tags" do
377
+ it "maintains specified tags" do
378
+ q1 = Queue.new
379
+ q1.add test: { tags: { hostname: "metrics-web-stg-1" }, value: 123 }
380
+ q2 = Queue.new(tags: { hostname: "metrics-web-stg-2" })
381
+ q2.merge!(q1)
382
+
383
+ expect(q2.queued[:measurements].first[:tags][:hostname]).to eq("metrics-web-stg-1")
384
+ end
385
+
386
+ it "does not change top-level tags" do
387
+ q1 = Queue.new(tags: { hostname: "metrics-web-stg-1" })
388
+ q1.add test: 456
389
+ q2 = Queue.new(tags: { hostname: "metrics-web-stg-2" })
390
+ q2.merge!(q1)
391
+
392
+ expect(q2.queued[:tags][:hostname]).to eq("metrics-web-stg-2")
393
+ end
394
+
395
+ it "tracks previous default tags" do
396
+ q1 = Queue.new(tags: { instance_id: "i-1234567a" })
397
+ q1.add test_1: 123
398
+ q2 = Queue.new(tags: { instance_type: "m3.medium" })
399
+ q2.add test_2: 456
400
+ q2.merge!(q1)
401
+ metric = q2.measurements.find { |measurement| measurement[:name] == "test_1" }
402
+
403
+ expect(metric[:tags][:instance_id]).to eq("i-1234567a")
404
+ expect(q2.queued[:tags]).to eq({ instance_type: "m3.medium" })
405
+
406
+ end
407
+ end
408
+
409
+ it "maintains specified sources" do
410
+ q1 = Queue.new
411
+ q1.add neo: {source: 'matrix', value: 123}
412
+ q2 = Queue.new(source: 'red_pill')
413
+ q2.merge!(q1)
414
+ expect(q2.queued[:gauges][0][:source]).to eq('matrix')
415
+ end
416
+
417
+ it "does not change default source" do
418
+ q1 = Queue.new(source: 'matrix')
419
+ q1.add neo: 456
420
+ q2 = Queue.new(source: 'red_pill')
421
+ q2.merge!(q1)
422
+ expect(q2.queued[:source]).to eq('red_pill')
423
+ end
424
+
425
+ it "tracks previous default source" do
426
+ q1 = Queue.new(source: 'matrix')
427
+ q1.add neo: 456
428
+ q2 = Queue.new(source: 'red_pill')
429
+ q2.add morpheus: 678
430
+ q2.merge!(q1)
431
+ q2.queued[:gauges].each do |gauge|
432
+ if gauge[:name] == 'neo'
433
+ expect(gauge[:source]).to eq('matrix')
434
+ end
435
+ end
436
+ end
437
+ end
438
+
439
+ it "handles empty cases" do
440
+ q1 = Queue.new
441
+ q1.add foo: 123, users: {type: :counter, value: 1000}
442
+ q2 = Queue.new
443
+ q2.merge!(q1)
444
+ expected = {counters: [{name:"users", value:1000, measure_time: @time}],
445
+ gauges: [{name:"foo", value:123, measure_time: @time}]}
446
+ expect(q2.queued).to eq(expected)
447
+ end
448
+
449
+ context "with an aggregator" do
450
+ it "merges" do
451
+ aggregator = Aggregator.new(source: 'aggregator')
452
+ aggregator.add timing: 102
453
+ aggregator.add timing: 203
454
+ queue = Queue.new(source: 'queue')
455
+ queue.add gauge: 42
456
+ queue.merge!(aggregator)
457
+ expected = {gauges:[{name:"gauge", value:42, measure_time:@time},
458
+ {name:"timing", count:2, sum:305.0, min:102.0, max:203.0, source:"aggregator"}],
459
+ source:'queue'}
460
+ expect(queue.queued).to equal_unordered(expected)
461
+ end
462
+ end
463
+
464
+ context "with a hash" do
465
+ it "merges" do
466
+ to_merge = {gauges:[{name: 'foo', value: 123}],
467
+ counters:[{name: 'bar', value: 456}]}
468
+ q = Queue.new
469
+ q.merge!(to_merge)
470
+ expect(q.gauges.length).to eq(1)
471
+ expect(q.counters.length).to eq(1)
472
+ end
473
+ end
474
+ end
475
+
476
+ describe "#per_request" do
477
+ it "defaults to 500" do
478
+ expect(subject.per_request).to eq(500)
479
+ end
480
+ end
481
+
482
+ describe "#queued" do
483
+ it "includes global source if set" do
484
+ q = Queue.new(source: 'blah')
485
+ q.add foo: 12
486
+ expect(q.queued[:source]).to eq('blah')
487
+ end
488
+
489
+ it "includes global measure_time if set" do
490
+ measure_time = (Time.now-1000).to_i
491
+ q = Queue.new(source: "foo", measure_time: measure_time)
492
+ q.add foo: 12
493
+ expect(q.queued[:measure_time]).to eq(measure_time)
494
+ end
495
+
496
+ context "when tags are set" do
497
+ it "includes global tags" do
498
+ expected_tags = { region: "us-east-1" }
499
+ queue = Queue.new(tags: expected_tags)
500
+ queue.add test: 5
501
+ expect(queue.queued[:tags]).to eq(expected_tags)
502
+ end
503
+ end
504
+
505
+ context "when time is set" do
506
+ it "includes global time" do
507
+ expected_time = (Time.now-1000).to_i
508
+ queue = Queue.new(tags: { foo: "bar" }, time: expected_time)
509
+ queue.add test: 10
510
+ expect(queue.queued[:time]).to eq(expected_time)
511
+ end
512
+ end
513
+
514
+ end
515
+
516
+ describe "#size" do
517
+ it "returns empty if gauges and counters are emtpy" do
518
+ expect(subject.size).to be_zero
519
+ end
520
+ it "returns count of gauges and counters if added" do
521
+ subject.add transactions: {type: :counter, value: 12345},
522
+ register_cents: {type: :gauge, value: 211101}
523
+ subject.add transactions: {type: :counter, value: 12345},
524
+ register_cents: {type: :gauge, value: 211101}
525
+ expect(subject.size).to eq(4)
526
+ end
527
+
528
+ context "when measurement present" do
529
+ it "returns count of measurements" do
530
+ subject.add test_1: { tags: { hostname: "metrics-web-stg-1" }, value: 1 },
531
+ test_2: { tags: { hostname: "metrics-web-stg-2" }, value: 2}
532
+
533
+ expect(subject.size).to eq(2)
534
+ end
535
+ end
536
+ end
537
+
538
+ describe "#submit" do
539
+ before(:all) do
540
+ Appoptics::Metrics.authenticate 'me@Appoptics.com', 'foo'
541
+ Appoptics::Metrics.persistence = :test
542
+ end
543
+
544
+ context "when successful" do
545
+ it "flushes queued metrics and return true" do
546
+ subject.add steps: 2042, distance: 1234
547
+ expect(subject.submit).to be true
548
+ expect(subject.queued).to be_empty
549
+ end
550
+ end
551
+
552
+ context "when failed" do
553
+ it "preserves queue and return false" do
554
+ subject.add steps: 2042, distance: 1234
555
+ subject.persister.return_value(false)
556
+ expect(subject.submit).to be false
557
+ expect(subject.queued).not_to be_empty
558
+ end
559
+ end
560
+ end
561
+
562
+ describe "#time" do
563
+ context "with metric name only" do
564
+ it "queues metric with timed value" do
565
+ subject.time :sleeping do
566
+ sleep 0.1
567
+ end
568
+ queued = subject.queued[:gauges][0]
569
+ expect(queued[:name]).to eq('sleeping')
570
+ expect(queued[:value]).to be >= 100
571
+ expect(queued[:value]).to be_within(30).of(100)
572
+ end
573
+ end
574
+
575
+ context "with metric and options" do
576
+ it "queues metric with value and options" do
577
+ subject.time :sleep_two, source: 'app1', period: 2 do
578
+ sleep 0.05
579
+ end
580
+ queued = subject.queued[:gauges][0]
581
+ expect(queued[:name]).to eq('sleep_two')
582
+ expect(queued[:period]).to eq(2)
583
+ expect(queued[:source]).to eq('app1')
584
+ expect(queued[:value]).to be >= 50
585
+ expect(queued[:value]).to be_within(30).of(50)
586
+ end
587
+ end
588
+ end
589
+
590
+ end # Queue
591
+
592
+ end
593
+ end