librato-metrics 2.0.2 → 2.1.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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