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.
- checksums.yaml +7 -0
- data/.gitignore +23 -0
- data/.rspec +2 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +184 -0
- data/Gemfile +36 -0
- data/LICENSE +24 -0
- data/README.md +271 -0
- data/Rakefile +63 -0
- data/appoptics-api-ruby.gemspec +31 -0
- data/benchmarks/array_vs_set.rb +29 -0
- data/certs/librato-public.pem +20 -0
- data/examples/simple.rb +24 -0
- data/examples/submit_every.rb +27 -0
- data/lib/appoptics/metrics.rb +95 -0
- data/lib/appoptics/metrics/aggregator.rb +138 -0
- data/lib/appoptics/metrics/annotator.rb +145 -0
- data/lib/appoptics/metrics/client.rb +361 -0
- data/lib/appoptics/metrics/collection.rb +43 -0
- data/lib/appoptics/metrics/connection.rb +101 -0
- data/lib/appoptics/metrics/errors.rb +32 -0
- data/lib/appoptics/metrics/middleware/count_requests.rb +28 -0
- data/lib/appoptics/metrics/middleware/expects_status.rb +38 -0
- data/lib/appoptics/metrics/middleware/request_body.rb +18 -0
- data/lib/appoptics/metrics/middleware/retry.rb +31 -0
- data/lib/appoptics/metrics/persistence.rb +2 -0
- data/lib/appoptics/metrics/persistence/direct.rb +73 -0
- data/lib/appoptics/metrics/persistence/test.rb +27 -0
- data/lib/appoptics/metrics/processor.rb +130 -0
- data/lib/appoptics/metrics/queue.rb +191 -0
- data/lib/appoptics/metrics/smart_json.rb +43 -0
- data/lib/appoptics/metrics/util.rb +25 -0
- data/lib/appoptics/metrics/version.rb +5 -0
- data/spec/integration/metrics/annotator_spec.rb +190 -0
- data/spec/integration/metrics/connection_spec.rb +14 -0
- data/spec/integration/metrics/middleware/count_requests_spec.rb +28 -0
- data/spec/integration/metrics/queue_spec.rb +96 -0
- data/spec/integration/metrics_spec.rb +375 -0
- data/spec/rackups/status.ru +30 -0
- data/spec/spec_helper.rb +88 -0
- data/spec/unit/metrics/aggregator_spec.rb +417 -0
- data/spec/unit/metrics/client_spec.rb +127 -0
- data/spec/unit/metrics/connection_spec.rb +113 -0
- data/spec/unit/metrics/queue/autosubmission_spec.rb +57 -0
- data/spec/unit/metrics/queue_spec.rb +593 -0
- data/spec/unit/metrics/smart_json_spec.rb +79 -0
- data/spec/unit/metrics/util_spec.rb +23 -0
- data/spec/unit/metrics_spec.rb +63 -0
- 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
|