librato-metrics 2.0.2 → 2.1.0.beta

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.tar.gz.sig CHANGED
Binary file
@@ -1,7 +1,7 @@
1
1
  ## Changelog
2
2
 
3
- ### Version 2.0.2
4
- * Filter sensitive headers in exception output (#130, Yannick Schutz)
3
+ ### Version 2.1.0.beta
4
+ * Add support for tagged measurements (#121)
5
5
 
6
6
  ### Version 2.0.1
7
7
  * Fix SmartJSON delegation bug for ruby 2.3.1 (#123)
@@ -13,6 +13,7 @@ require 'metrics/errors'
13
13
  require 'metrics/persistence'
14
14
  require 'metrics/queue'
15
15
  require 'metrics/smart_json'
16
+ require 'metrics/util'
16
17
  require 'metrics/version'
17
18
 
18
19
  module Librato
@@ -65,23 +66,22 @@ module Librato
65
66
  extend SingleForwardable
66
67
 
67
68
  TYPES = [:counter, :gauge]
68
- PLURAL_TYPES = [:counters, :gauges]
69
+ PLURAL_TYPES = TYPES.map { |type| "#{type}s".to_sym }
69
70
  MIN_MEASURE_TIME = (Time.now-(3600*24*365)).to_i
70
71
 
71
72
  # Most of the singleton methods of Librato::Metrics are actually
72
73
  # being called on a global Client instance. See further docs on
73
74
  # Client.
74
75
  #
75
- def_delegators :client, :agent_identifier, :annotate, :api_endpoint,
76
- :api_endpoint=, :authenticate, :connection,
77
- :proxy, :proxy=,
78
- :faraday_adapter, :faraday_adapter=,
79
- :persistence, :persistence=, :persister,
80
- :get_composite, :get_metric, :get_measurements, :metrics,
81
- :delete_metrics, :update_metric, :update_metrics,
82
- :submit,
83
- :sources, :get_source, :update_source,
84
- :create_snapshot, :get_snapshot
76
+ def_delegators :client, :agent_identifier, :annotate,
77
+ :api_endpoint, :api_endpoint=, :authenticate,
78
+ :connection, :create_snapshot, :delete_metrics,
79
+ :faraday_adapter, :faraday_adapter=, :get_composite,
80
+ :get_measurements, :get_metric, :get_series,
81
+ :get_snapshot, :get_source, :metrics,
82
+ :persistence, :persistence=, :persister, :proxy, :proxy=,
83
+ :sources, :submit, :update_metric, :update_metrics,
84
+ :update_source
85
85
 
86
86
  # The Librato::Metrics::Client being used by module-level
87
87
  # access.
@@ -23,7 +23,7 @@ module Librato
23
23
  # queue.merge!(aggregator)
24
24
  #
25
25
  class Aggregator
26
- SOURCE_SEPARATOR = '%%' # must not be in valid source name criteria
26
+ SEPARATOR = '%%' # must not be in valid tags and/or source criteria
27
27
 
28
28
  include Processor
29
29
 
@@ -52,20 +52,29 @@ module Librato
52
52
  # @return [Aggregator] returns self
53
53
  def add(measurements)
54
54
  measurements.each do |metric, data|
55
+ entry = {}
55
56
  if @prefix
56
57
  metric = "#{@prefix}.#{metric}"
57
58
  end
59
+ entry[:name] = metric.to_s
58
60
  if data.respond_to?(:each) # hash form
61
+ validate_parameters(data)
59
62
  value = data[:value]
60
63
  if data[:source]
61
- metric = "#{metric}#{SOURCE_SEPARATOR}#{data[:source]}"
64
+ metric = "#{metric}#{SEPARATOR}#{data[:source]}"
65
+ entry[:source] = data[:source].to_s
66
+ elsif data[:tags] && data[:tags].respond_to?(:each)
67
+ metric = Librato::Metrics::Util.build_key_for(metric.to_s, data[:tags])
68
+ entry[:tags] = data[:tags]
62
69
  end
63
70
  else
64
71
  value = data
65
72
  end
66
73
 
67
- @aggregated[metric] ||= Aggregate.new
68
- @aggregated[metric] << value
74
+ @aggregated[metric] = {} unless @aggregated[metric]
75
+ @aggregated[metric][:aggregate] ||= Aggregate.new
76
+ @aggregated[metric][:aggregate] << value
77
+ @aggregated[metric].merge!(entry)
69
78
  end
70
79
  autosubmit_check
71
80
  self
@@ -88,31 +97,38 @@ module Librato
88
97
  # Returns currently queued data
89
98
  #
90
99
  def queued
91
- gauges = []
100
+ entries = []
101
+ multidimensional = has_tags?
92
102
 
93
- @aggregated.each do |metric, data|
94
- source = nil
95
- metric = metric.to_s
96
- if metric.include?(SOURCE_SEPARATOR)
97
- metric, source = metric.split(SOURCE_SEPARATOR)
98
- end
103
+ @aggregated.each_value do |data|
99
104
  entry = {
100
- name: metric,
101
- count: data.count,
102
- sum: data.sum,
103
-
105
+ name: data[:name],
106
+ count: data[:aggregate].count,
107
+ sum: data[:aggregate].sum,
104
108
  # TODO: make float/non-float consistent in the gem
105
- min: data.min.to_f,
106
- max: data.max.to_f
109
+ min: data[:aggregate].min.to_f,
110
+ max: data[:aggregate].max.to_f
107
111
  # TODO: expose v.sum2 and include
108
112
  }
109
- entry[:source] = source if source
110
- gauges << entry
113
+ if data[:source]
114
+ entry[:source] = data[:source]
115
+ elsif data[:tags]
116
+ multidimensional = true
117
+ entry[:tags] = data[:tags]
118
+ end
119
+ multidimensional = true if data[:time]
120
+ entries << entry
111
121
  end
112
-
113
- req = { gauges: gauges }
122
+ time = multidimensional ? :time : :measure_time
123
+ req =
124
+ if multidimensional
125
+ { measurements: entries }
126
+ else
127
+ { gauges: entries }
128
+ end
114
129
  req[:source] = @source if @source
115
- req[:measure_time] = @measure_time if @measure_time
130
+ req[:tags] = @tags if has_tags?
131
+ req[time] = @time if @time
116
132
 
117
133
  req
118
134
  end
@@ -178,6 +178,40 @@ module Librato
178
178
  parsed
179
179
  end
180
180
 
181
+ # Retrieve series of measurements for a given metric
182
+ #
183
+ # @example Get series for metric
184
+ # series = Librato::Metrics.get_series :requests, resolution: 1, duration: 3600
185
+ #
186
+ # @example Get series for metric grouped by tag
187
+ # query = { duration: 3600, resolution: 1, group_by: "environment", group_by_function: "sum" }
188
+ # series = Librato::Metrics.get_series :requests, query
189
+ #
190
+ # @example Get series for metric grouped by tag and negated by tag filter
191
+ # query = { duration: 3600, resolution: 1, group_by: "environment", group_by_function: "sum", tags_search: "environment=!staging" }
192
+ # series = Librato::Metrics.get_series :requests, query
193
+ #
194
+ # @param [Symbol|String] metric_name Metric name
195
+ # @param [Hash] options Query options
196
+ def get_series(metric_name, options={})
197
+ raise ArgumentError, ":resolution and :duration or :start_time must be set" if options.empty?
198
+ query = options.dup
199
+ if query[:start_time].respond_to?(:year)
200
+ query[:start_time] = query[:start_time].to_i
201
+ end
202
+ if query[:end_time].respond_to?(:year)
203
+ query[:end_time] = query[:end_time].to_i
204
+ end
205
+ query[:resolution] ||= 1
206
+ unless query[:start_time] || query[:end_time]
207
+ query[:duration] ||= 3600
208
+ end
209
+ url = connection.build_url("measurements/#{metric_name}", query)
210
+ response = connection.get(url)
211
+ parsed = SmartJSON.read(response.body)
212
+ parsed["series"]
213
+ end
214
+
181
215
  # Retrieve data points for a specific metric
182
216
  #
183
217
  # @example Get 20 most recent data points for metric
@@ -9,6 +9,7 @@ module Librato
9
9
  class NoClientProvided < MetricsError; end
10
10
  class InvalidMeasureTime < MetricsError; end
11
11
  class NotMergeable < MetricsError; end
12
+ class InvalidParameters < MetricsError; end
12
13
 
13
14
  class NetworkError < StandardError
14
15
  attr_reader :response
@@ -5,34 +5,25 @@ module Librato
5
5
  class ExpectsStatus < Faraday::Response::Middleware
6
6
 
7
7
  def on_complete(env)
8
- sanitized = sanitize_request(env)
8
+ # TODO: make exception output prettier
9
9
  case env[:status]
10
10
  when 401
11
- raise Unauthorized.new(sanitized.to_s, sanitized)
11
+ raise Unauthorized.new(env.to_s, env)
12
12
  when 403
13
- raise Forbidden.new(sanitized.to_s, sanitized)
13
+ raise Forbidden.new(env.to_s, env)
14
14
  when 404
15
- raise NotFound.new(sanitized.to_s, sanitized)
15
+ raise NotFound.new(env.to_s, env)
16
16
  when 422
17
- raise EntityAlreadyExists.new(sanitized.to_s, sanitized)
17
+ raise EntityAlreadyExists.new(env.to_s, env)
18
18
  when 400..499
19
- raise ClientError.new(sanitized.to_s, sanitized)
19
+ raise ClientError.new(env.to_s, env)
20
20
  when 500..599
21
- raise ServerError.new(sanitized.to_s, sanitized)
21
+ raise ServerError.new(env.to_s, env)
22
22
  end
23
23
  end
24
24
 
25
- def sanitize_request(env)
26
- {
27
- status: env.status,
28
- url: env.url.to_s,
29
- user_agent: env.request_headers["User-Agent"],
30
- request_body: env[:request_body],
31
- response_headers: env.response_headers,
32
- response_body: env.body
33
- }
34
- end
35
25
  end
26
+
36
27
  end
37
28
  end
38
- end
29
+ end
@@ -5,8 +5,6 @@ module Librato
5
5
  module Metrics
6
6
  module Persistence
7
7
  class Direct
8
- MEASUREMENT_TYPES = [:gauges, :counters]
9
-
10
8
  # Persist the queued metrics directly to the
11
9
  # Metrics web API.
12
10
  #
@@ -18,9 +16,15 @@ module Librato
18
16
  requests = [queued]
19
17
  end
20
18
  requests.each do |request|
19
+ resource =
20
+ if queued[:gauges] || queued[:counters]
21
+ "metrics"
22
+ else
23
+ "measurements"
24
+ end
21
25
  payload = SmartJSON.write(request)
22
26
  # expects 200
23
- client.connection.post('metrics', payload)
27
+ client.connection.post(resource, payload)
24
28
  end
25
29
  end
26
30
 
@@ -31,16 +35,16 @@ module Librato
31
35
  reqs = []
32
36
  # separate metric-containing values from global values
33
37
  globals = fetch_globals(queued)
34
- MEASUREMENT_TYPES.each do |metric_type|
35
- metrics = queued[metric_type]
38
+ top_level_keys.each do |key|
39
+ metrics = queued[key]
36
40
  next unless metrics
37
41
  if metrics.size <= per_request
38
42
  # we can fit all of this metric type in a single request
39
- reqs << build_request(metric_type, metrics, globals)
43
+ reqs << build_request(key, metrics, globals)
40
44
  else
41
45
  # going to have to split things up
42
46
  metrics.each_slice(per_request) do |elements|
43
- reqs << build_request(metric_type, elements, globals)
47
+ reqs << build_request(key, elements, globals)
44
48
  end
45
49
  end
46
50
  end
@@ -51,8 +55,12 @@ module Librato
51
55
  {type => metrics}.merge(globals)
52
56
  end
53
57
 
58
+ def top_level_keys
59
+ [Librato::Metrics::PLURAL_TYPES, :measurements].flatten
60
+ end
61
+
54
62
  def fetch_globals(queued)
55
- queued.reject {|k, v| MEASUREMENT_TYPES.include?(k)}
63
+ queued.reject { |k, v| top_level_keys.include?(k) }
56
64
  end
57
65
 
58
66
  def queue_count(queued)
@@ -62,4 +70,4 @@ module Librato
62
70
  end
63
71
  end
64
72
  end
65
- end
73
+ end
@@ -1,3 +1,5 @@
1
+ require "set"
2
+
1
3
  module Librato
2
4
  module Metrics
3
5
 
@@ -7,7 +9,11 @@ module Librato
7
9
  MEASUREMENTS_PER_REQUEST = 500
8
10
 
9
11
  attr_reader :per_request, :last_submit_time
10
- attr_accessor :prefix
12
+ attr_accessor :prefix, :tags
13
+
14
+ def tags
15
+ @tags ||= {}
16
+ end
11
17
 
12
18
  # The current Client instance this queue is using to authenticate
13
19
  # and connect to Librato Metrics. This will default to the primary
@@ -19,6 +25,11 @@ module Librato
19
25
  @client ||= Librato::Metrics.client
20
26
  end
21
27
 
28
+ def has_tags?
29
+ !@tags.empty?
30
+ end
31
+ alias :tags? :has_tags?
32
+
22
33
  # The object this MetricSet will use to persist
23
34
  #
24
35
  def persister
@@ -82,11 +93,13 @@ module Librato
82
93
  end
83
94
 
84
95
  def setup_common_options(options)
96
+ validate_parameters(options)
85
97
  @autosubmit_interval = options[:autosubmit_interval]
86
98
  @client = options[:client] || Librato::Metrics.client
87
99
  @per_request = options[:per_request] || MEASUREMENTS_PER_REQUEST
88
100
  @source = options[:source]
89
- @measure_time = options[:measure_time] && options[:measure_time].to_i
101
+ @tags = options.fetch(:tags, {})
102
+ @time = (options[:time] && options[:time].to_i || options[:measure_time] && options[:measure_time].to_i)
90
103
  @create_time = Time.now
91
104
  @clear_on_failure = options[:clear_failures] || false
92
105
  @prefix = options[:prefix]
@@ -99,6 +112,18 @@ module Librato
99
112
  end
100
113
  end
101
114
 
115
+ def validate_parameters(options)
116
+ invalid_combinations = [
117
+ [:source, :tags],
118
+ ]
119
+ opts = options.keys.to_set
120
+ invalid_combinations.each do |combo|
121
+ if combo.to_set.subset?(opts)
122
+ raise InvalidParameters, "#{combo} cannot be simultaneously set"
123
+ end
124
+ end
125
+ end
126
+
102
127
  end
103
128
 
104
129
  end
@@ -28,7 +28,9 @@ module Librato
28
28
  # @return [Queue] returns self
29
29
  def add(measurements)
30
30
  measurements.each do |key, value|
31
+ multidimensional = has_tags?
31
32
  if value.respond_to?(:each)
33
+ validate_parameters(value)
32
34
  metric = value
33
35
  metric[:name] = key.to_s
34
36
  type = metric.delete(:type) || metric.delete('type') || 'gauge'
@@ -39,15 +41,24 @@ module Librato
39
41
  if @prefix
40
42
  metric[:name] = "#{@prefix}.#{metric[:name]}"
41
43
  end
44
+ multidimensional = true if metric[:tags] || metric[:time]
42
45
  type = ("#{type}s").to_sym
43
- if metric[:measure_time]
44
- metric[:measure_time] = metric[:measure_time].to_i
46
+ time_key = multidimensional ? :time : :measure_time
47
+ metric[:time] = metric.delete(:measure_time) if multidimensional && metric[:measure_time]
48
+
49
+ if metric[time_key]
50
+ metric[time_key] = metric[time_key].to_i
45
51
  check_measure_time(metric)
46
52
  elsif !skip_measurement_times
47
- metric[:measure_time] = epoch_time
53
+ metric[time_key] = epoch_time
54
+ end
55
+ if multidimensional
56
+ @queued[:measurements] ||= []
57
+ @queued[:measurements] << metric
58
+ else
59
+ @queued[type] ||= []
60
+ @queued[type] << metric
48
61
  end
49
- @queued[type] ||= []
50
- @queued[type] << metric
51
62
  end
52
63
  submit_check
53
64
  self
@@ -81,6 +92,10 @@ module Librato
81
92
  @queued[:gauges] || []
82
93
  end
83
94
 
95
+ def measurements
96
+ @queued[:measurements] || []
97
+ end
98
+
84
99
  # Combines queueable measures from the given object
85
100
  # into this queue.
86
101
  #
@@ -99,14 +114,24 @@ module Librato
99
114
  end
100
115
  Metrics::PLURAL_TYPES.each do |type|
101
116
  if to_merge[type]
102
- measurements = reconcile_source(to_merge[type], to_merge[:source])
117
+ payload = reconcile(to_merge[type], to_merge[:source])
103
118
  if @queued[type]
104
- @queued[type] += measurements
119
+ @queued[type] += payload
105
120
  else
106
- @queued[type] = measurements
121
+ @queued[type] = payload
107
122
  end
108
123
  end
109
124
  end
125
+
126
+ if to_merge[:measurements]
127
+ payload = reconcile(to_merge[:measurements], to_merge[:tags])
128
+ if @queued[:measurements]
129
+ @queued[:measurements] += payload
130
+ else
131
+ @queued[:measurements] = payload
132
+ end
133
+ end
134
+
110
135
  submit_check
111
136
  self
112
137
  end
@@ -117,8 +142,10 @@ module Librato
117
142
  def queued
118
143
  return {} if @queued.empty?
119
144
  globals = {}
145
+ time = has_tags? ? :time : :measure_time
146
+ globals[time] = @time if @time
120
147
  globals[:source] = @source if @source
121
- globals[:measure_time] = @measure_time if @measure_time
148
+ globals[:tags] = @tags if has_tags?
122
149
  @queued.merge(globals)
123
150
  end
124
151
 
@@ -133,16 +160,19 @@ module Librato
133
160
  private
134
161
 
135
162
  def check_measure_time(data)
136
- if data[:measure_time] < Metrics::MIN_MEASURE_TIME
163
+ time_keys = [:measure_time, :time]
164
+
165
+ if time_keys.any? { |key| data[key] && data[key] < Metrics::MIN_MEASURE_TIME }
137
166
  raise InvalidMeasureTime, "Measure time for submitted metric (#{data}) is invalid."
138
167
  end
139
168
  end
140
169
 
141
- def reconcile_source(measurements, source)
142
- return measurements if !source || source == @source
170
+ def reconcile(measurements, val)
171
+ arr = val.is_a?(Hash) ? [@tags, :tags] : [@source, :source]
172
+ return measurements if !val || val == arr.first
143
173
  measurements.map! do |measurement|
144
- unless measurement[:source]
145
- measurement[:source] = source
174
+ unless measurement[arr.last]
175
+ measurement[arr.last] = val
146
176
  end
147
177
  measurement
148
178
  end
@@ -0,0 +1,25 @@
1
+ module Librato
2
+ module Metrics
3
+
4
+ class Util
5
+ SEPARATOR = "%%"
6
+
7
+ # Builds a Hash key from metric name and tags.
8
+ #
9
+ # @param metric_name [String] The unique identifying metric name of the property being tracked.
10
+ # @param tags [Hash] A set of name=value tag pairs that describe the particular data stream.
11
+ # @return [String] the Hash key
12
+ def self.build_key_for(metric_name, tags)
13
+ key_name = metric_name
14
+ tags.sort.each do |key, value|
15
+ k = key.to_s.downcase
16
+ v = value.is_a?(String) ? value.downcase : value
17
+ key_name = "#{key_name}#{SEPARATOR}#{k}=#{v}"
18
+ end
19
+ key_name
20
+ end
21
+
22
+ end
23
+
24
+ end
25
+ end
@@ -1,5 +1,5 @@
1
1
  module Librato
2
2
  module Metrics
3
- VERSION = "2.0.2"
3
+ VERSION = "2.1.0.beta"
4
4
  end
5
5
  end
@@ -5,7 +5,9 @@ module Librato
5
5
 
6
6
  describe Queue do
7
7
  before(:all) { prep_integration_tests }
8
- before(:each) { delete_all_metrics }
8
+ before(:each) do
9
+ delete_all_metrics
10
+ end
9
11
 
10
12
  context "with a large number of metrics" do
11
13
  it "submits them in multiple requests" do
@@ -70,6 +72,24 @@ module Librato
70
72
  expect(bar['barsource'][0]['value']).to eq(456)
71
73
  end
72
74
 
75
+ context "with tags" do
76
+ let(:queue) { Queue.new(tags: { hostname: "metrics-web-stg-1" }) }
77
+
78
+ it "respects default and individual tags" do
79
+ queue.add test_1: 123
80
+ queue.add test_2: { value: 456, tags: { hostname: "metrics-web-stg-2" }}
81
+ queue.submit
82
+
83
+ test_1 = Librato::Metrics.get_series :test_1, resolution: 1, duration: 3600
84
+ expect(test_1[0]["tags"]["hostname"]).to eq("metrics-web-stg-1")
85
+ expect(test_1[0]["measurements"][0]["value"]).to eq(123)
86
+
87
+ test_2 = Librato::Metrics.get_series :test_2, resolution: 1, duration: 3600
88
+ expect(test_2[0]["tags"]["hostname"]).to eq("metrics-web-stg-2")
89
+ expect(test_2[0]["measurements"][0]["value"]).to eq(456)
90
+ end
91
+ end
92
+
73
93
  end
74
94
 
75
95
  end
@@ -360,6 +360,17 @@ module Librato
360
360
 
361
361
  end
362
362
 
363
+ describe "#get_series" do
364
+ before { Metrics.submit test_series: { value: 123, tags: { hostname: "metrics-web-stg-1" } } }
365
+
366
+ it "gets series" do
367
+ series = Metrics.get_series :test_series, resolution: 1, duration: 3600
368
+
369
+ expect(series[0]["tags"]["hostname"]).to eq("metrics-web-stg-1")
370
+ expect(series[0]["measurements"][0]["value"]).to eq(123)
371
+ end
372
+ end
373
+
363
374
  # Note: These are challenging to test end-to-end, should probably
364
375
  # unit test instead. Disabling for now.
365
376
  #
@@ -38,6 +38,69 @@ module Librato
38
38
  expect(a.source).to be_nil
39
39
  end
40
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
41
104
  end
42
105
 
43
106
  describe "#add" do
@@ -45,6 +108,14 @@ module Librato
45
108
  expect(subject.add(foo: 1234)).to eq(subject)
46
109
  end
47
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
+
48
119
  context "with single hash argument" do
49
120
  it "records a single aggregate" do
50
121
  subject.add foo: 3000
@@ -91,6 +162,21 @@ module Librato
91
162
  expect(subject.queued).to equal_unordered(expected)
92
163
  end
93
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
+
94
180
  context "with a prefix set" do
95
181
  it "auto-prepends names" do
96
182
  subject = Aggregator.new(prefix: 'foo')
@@ -160,6 +246,63 @@ module Librato
160
246
  expect(subject.queued).to equal_unordered(expected)
161
247
  end
162
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
163
306
  end
164
307
 
165
308
  describe "#queued" do
@@ -171,10 +314,30 @@ module Librato
171
314
 
172
315
  it "includes global measure_time if set" do
173
316
  measure_time = (Time.now-1000).to_i
174
- a = Aggregator.new(measure_time: measure_time)
317
+ a = Aggregator.new(source: "foo", measure_time: measure_time)
175
318
  a.add foo: 12
176
319
  expect(a.queued[:measure_time]).to eq(measure_time)
177
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
178
341
  end
179
342
 
180
343
  describe "#submit" do
@@ -5,16 +5,22 @@ module Librato
5
5
 
6
6
  describe Queue do
7
7
 
8
- before(:each) do
8
+ before(:all) do
9
9
  @time = (Time.now.to_i - 1*60)
10
10
  allow_any_instance_of(Queue).to receive(:epoch_time).and_return(@time)
11
11
  end
12
12
 
13
13
  describe "initialization" do
14
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
+
15
23
  it "sets to client" do
16
- barney = Client
17
- queue = Queue.new(client: barney)
18
24
  expect(queue.client).to eq(barney)
19
25
  end
20
26
  end
@@ -25,6 +31,69 @@ module Librato
25
31
  expect(queue.client).to eq(Librato::Metrics.client)
26
32
  end
27
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
28
97
  end
29
98
 
30
99
  describe "#add" do
@@ -32,6 +101,14 @@ module Librato
32
101
  expect(subject.add(foo: 123)).to eq(subject)
33
102
  end
34
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
+
35
112
  context "with single hash argument" do
36
113
  it "records a key-value gauge" do
37
114
  expected = {gauges: [{name: 'foo', value: 3000, measure_time: @time}]}
@@ -137,6 +214,70 @@ module Librato
137
214
  }.to raise_error(InvalidMeasureTime)
138
215
  end
139
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
140
281
  end
141
282
 
142
283
  describe "#counters" do
@@ -219,6 +360,39 @@ module Librato
219
360
  expect(q2.queued).to equal_unordered(expected)
220
361
  end
221
362
 
363
+ context "with tags" do
364
+ it "maintains specified tags" do
365
+ q1 = Queue.new
366
+ q1.add test: { tags: { hostname: "metrics-web-stg-1" }, value: 123 }
367
+ q2 = Queue.new(tags: { hostname: "metrics-web-stg-2" })
368
+ q2.merge!(q1)
369
+
370
+ expect(q2.queued[:measurements].first[:tags][:hostname]).to eq("metrics-web-stg-1")
371
+ end
372
+
373
+ it "does not change top-level tags" do
374
+ q1 = Queue.new(tags: { hostname: "metrics-web-stg-1" })
375
+ q1.add test: 456
376
+ q2 = Queue.new(tags: { hostname: "metrics-web-stg-2" })
377
+ q2.merge!(q1)
378
+
379
+ expect(q2.queued[:tags][:hostname]).to eq("metrics-web-stg-2")
380
+ end
381
+
382
+ it "tracks previous default tags" do
383
+ q1 = Queue.new(tags: { instance_id: "i-1234567a" })
384
+ q1.add test_1: 123
385
+ q2 = Queue.new(tags: { instance_type: "m3.medium" })
386
+ q2.add test_2: 456
387
+ q2.merge!(q1)
388
+ metric = q2.measurements.find { |measurement| measurement[:name] == "test_1" }
389
+
390
+ expect(metric[:tags][:instance_id]).to eq("i-1234567a")
391
+ expect(q2.queued[:tags]).to eq({ instance_type: "m3.medium" })
392
+
393
+ end
394
+ end
395
+
222
396
  it "maintains specified sources" do
223
397
  q1 = Queue.new
224
398
  q1.add neo: {source: 'matrix', value: 123}
@@ -247,6 +421,7 @@ module Librato
247
421
  end
248
422
  end
249
423
  end
424
+ end
250
425
 
251
426
  it "handles empty cases" do
252
427
  q1 = Queue.new
@@ -257,7 +432,6 @@ module Librato
257
432
  gauges: [{name:"foo", value:123, measure_time: @time}]}
258
433
  expect(q2.queued).to eq(expected)
259
434
  end
260
- end
261
435
 
262
436
  context "with an aggregator" do
263
437
  it "merges" do
@@ -301,10 +475,29 @@ module Librato
301
475
 
302
476
  it "includes global measure_time if set" do
303
477
  measure_time = (Time.now-1000).to_i
304
- q = Queue.new(measure_time: measure_time)
478
+ q = Queue.new(source: "foo", measure_time: measure_time)
305
479
  q.add foo: 12
306
480
  expect(q.queued[:measure_time]).to eq(measure_time)
307
481
  end
482
+
483
+ context "when tags are set" do
484
+ it "includes global tags" do
485
+ expected_tags = { region: "us-east-1" }
486
+ queue = Queue.new(tags: expected_tags)
487
+ queue.add test: 5
488
+ expect(queue.queued[:tags]).to eq(expected_tags)
489
+ end
490
+ end
491
+
492
+ context "when time is set" do
493
+ it "includes global time" do
494
+ expected_time = (Time.now-1000).to_i
495
+ queue = Queue.new(tags: { foo: "bar" }, time: expected_time)
496
+ queue.add test: 10
497
+ expect(queue.queued[:time]).to eq(expected_time)
498
+ end
499
+ end
500
+
308
501
  end
309
502
 
310
503
  describe "#size" do
@@ -318,6 +511,15 @@ module Librato
318
511
  register_cents: {type: :gauge, value: 211101}
319
512
  expect(subject.size).to eq(4)
320
513
  end
514
+
515
+ context "when measurement present" do
516
+ it "returns count of measurements" do
517
+ subject.add test_1: { tags: { hostname: "metrics-web-stg-1" }, value: 1 },
518
+ test_2: { tags: { hostname: "metrics-web-stg-2" }, value: 2}
519
+
520
+ expect(subject.size).to eq(2)
521
+ end
522
+ end
321
523
  end
322
524
 
323
525
  describe "#submit" do
@@ -0,0 +1,23 @@
1
+ require "spec_helper"
2
+
3
+ module Librato
4
+ module Metrics
5
+
6
+ describe Util do
7
+
8
+ describe "#build_key_for" do
9
+ it "builds a Hash key" do
10
+ metric_name = "requests"
11
+ tags = { status: 200, MeThoD: "GET", controller: "users", ACTION: "show" }
12
+ expected = "requests%%action=show%%method=get%%controller=users%%status=200"
13
+ actual = Util.build_key_for(metric_name, tags)
14
+
15
+ expect(expected).to eq(actual)
16
+ end
17
+
18
+ end
19
+
20
+ end
21
+
22
+ end
23
+ end
metadata CHANGED
@@ -1,8 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: librato-metrics
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.2
5
- prerelease:
4
+ version: 2.1.0.beta
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Matt Sanders
@@ -36,7 +36,7 @@ cert_chain:
36
36
  dzJsaG9DTUxDU1NqOVQvWXlSVEwzVVFZb0s1cFBBNzYKaHZqeDBXSlA4cHpa
37
37
  TUpQS0pCUlpRWEpPNWlmRVB5S2paeU1pNVhNSG1ydERjbEhMajNzeDRSQXZF
38
38
  WmpHV2tSUApKU1E9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
39
- date: 2016-11-09 00:00:00.000000000 Z
39
+ date: 2016-11-04 00:00:00.000000000 Z
40
40
  dependencies:
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: faraday
@@ -106,6 +106,7 @@ files:
106
106
  - lib/librato/metrics/processor.rb
107
107
  - lib/librato/metrics/queue.rb
108
108
  - lib/librato/metrics/smart_json.rb
109
+ - lib/librato/metrics/util.rb
109
110
  - lib/librato/metrics/version.rb
110
111
  - librato-metrics.gemspec
111
112
  - spec/integration/metrics/annotator_spec.rb
@@ -121,6 +122,7 @@ files:
121
122
  - spec/unit/metrics/queue/autosubmission_spec.rb
122
123
  - spec/unit/metrics/queue_spec.rb
123
124
  - spec/unit/metrics/smart_json_spec.rb
125
+ - spec/unit/metrics/util_spec.rb
124
126
  - spec/unit/metrics_spec.rb
125
127
  homepage: https://github.com/librato/librato-metrics
126
128
  licenses:
@@ -139,9 +141,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
141
  required_rubygems_version: !ruby/object:Gem::Requirement
140
142
  none: false
141
143
  requirements:
142
- - - ! '>='
144
+ - - ! '>'
143
145
  - !ruby/object:Gem::Version
144
- version: '0'
146
+ version: 1.3.1
145
147
  requirements: []
146
148
  rubyforge_project:
147
149
  rubygems_version: 1.8.23.2
@@ -162,5 +164,6 @@ test_files:
162
164
  - spec/unit/metrics/queue/autosubmission_spec.rb
163
165
  - spec/unit/metrics/queue_spec.rb
164
166
  - spec/unit/metrics/smart_json_spec.rb
167
+ - spec/unit/metrics/util_spec.rb
165
168
  - spec/unit/metrics_spec.rb
166
169
  has_rdoc:
metadata.gz.sig CHANGED
@@ -1,3 +1,2 @@
1
- �4�5�k,y'�� �=���> �VpT�qa5��# ��&+�dzuf���kSu7٭�u*Y
2
- � ��r+¿�2+q�%p٨%n��C�jH_ӱ4|�f�S���(D��B|b���/(�L.\^�%GQ�`��x����C3>���� �D���x��}22���.JP�����??�j"m���~
3
- ��R�!�aBl�|6��ҳt���Ư�R6�0`7�CZ�*�wjq� ��/G�a�&e�D3�I��
1
+ ��ZWn�V��K䓨 %U
2
+ �3��,/���������y�����rC�U��y޼�$���:�6���O0��30��Ip