statsd-instrument 2.3.5 → 2.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -0
- data/.github/workflows/ci.yml +31 -0
- data/.gitignore +1 -0
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
- data/.rubocop.yml +21 -0
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +25 -5
- data/Gemfile +2 -0
- data/Rakefile +3 -1
- data/lib/statsd-instrument.rb +2 -0
- data/lib/statsd/instrument.rb +51 -18
- data/lib/statsd/instrument/assertions.rb +24 -18
- data/lib/statsd/instrument/backend.rb +3 -2
- data/lib/statsd/instrument/backends/capture_backend.rb +2 -1
- data/lib/statsd/instrument/backends/logger_backend.rb +3 -3
- data/lib/statsd/instrument/backends/null_backend.rb +2 -0
- data/lib/statsd/instrument/backends/udp_backend.rb +18 -15
- data/lib/statsd/instrument/environment.rb +2 -0
- data/lib/statsd/instrument/helpers.rb +6 -2
- data/lib/statsd/instrument/matchers.rb +14 -11
- data/lib/statsd/instrument/metric.rb +34 -21
- data/lib/statsd/instrument/metric_expectation.rb +32 -18
- data/lib/statsd/instrument/railtie.rb +2 -1
- data/lib/statsd/instrument/version.rb +3 -1
- data/statsd-instrument.gemspec +13 -10
- data/test/assertions_test.rb +15 -4
- data/test/benchmark/default_tags.rb +47 -0
- data/test/benchmark/metrics.rb +9 -8
- data/test/benchmark/tags.rb +5 -3
- data/test/capture_backend_test.rb +4 -2
- data/test/environment_test.rb +2 -1
- data/test/helpers_test.rb +2 -1
- data/test/integration_test.rb +27 -7
- data/test/logger_backend_test.rb +10 -8
- data/test/matchers_test.rb +34 -20
- data/test/metric_test.rb +15 -4
- data/test/statsd_instrumentation_test.rb +7 -7
- data/test/statsd_test.rb +24 -15
- data/test/test_helper.rb +2 -0
- data/test/udp_backend_test.rb +3 -26
- metadata +22 -3
- data/.travis.yml +0 -12
@@ -1,11 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module StatsD::Instrument::Helpers
|
2
4
|
def capture_statsd_calls(&block)
|
3
5
|
mock_backend = StatsD::Instrument::Backends::CaptureBackend.new
|
4
|
-
old_backend
|
6
|
+
old_backend = StatsD.backend
|
7
|
+
StatsD.backend = mock_backend
|
8
|
+
|
5
9
|
block.call
|
6
10
|
mock_backend.collected_metrics
|
7
11
|
ensure
|
8
|
-
if old_backend.
|
12
|
+
if old_backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
|
9
13
|
old_backend.collected_metrics.concat(mock_backend.collected_metrics)
|
10
14
|
end
|
11
15
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rspec/expectations'
|
2
4
|
require 'rspec/core/version'
|
3
5
|
|
@@ -9,7 +11,7 @@ module StatsD::Instrument::Matchers
|
|
9
11
|
histogram: :h,
|
10
12
|
distribution: :d,
|
11
13
|
set: :s,
|
12
|
-
key_value: :kv
|
14
|
+
key_value: :kv,
|
13
15
|
}
|
14
16
|
|
15
17
|
class Matcher
|
@@ -23,13 +25,10 @@ module StatsD::Instrument::Matchers
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def matches?(block)
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
false
|
32
|
-
end
|
28
|
+
expect_statsd_call(@metric_type, @metric_name, @options, &block)
|
29
|
+
rescue RSpec::Expectations::ExpectationNotMetError => e
|
30
|
+
@message = e.message
|
31
|
+
false
|
33
32
|
end
|
34
33
|
|
35
34
|
def failure_message
|
@@ -50,8 +49,12 @@ module StatsD::Instrument::Matchers
|
|
50
49
|
metrics = capture_statsd_calls(&block)
|
51
50
|
metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }
|
52
51
|
|
53
|
-
|
54
|
-
|
52
|
+
if metrics.empty?
|
53
|
+
raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made."
|
54
|
+
elsif options[:times] && options[:times] != metrics.length
|
55
|
+
raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric #{metric_name} " \
|
56
|
+
"was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}"
|
57
|
+
end
|
55
58
|
|
56
59
|
[:sample_rate, :value, :tags].each do |expectation|
|
57
60
|
next unless options[expectation]
|
@@ -63,7 +66,7 @@ module StatsD::Instrument::Matchers
|
|
63
66
|
|
64
67
|
found = options[:times] ? num_matches == options[:times] : num_matches > 0
|
65
68
|
|
66
|
-
|
69
|
+
unless found
|
67
70
|
message = metric_information(metric_name, options, metrics, expectation)
|
68
71
|
raise RSpec::Expectations::ExpectationNotMetError, message
|
69
72
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# The Metric class represents a metric sample to be send by a backend.
|
2
4
|
#
|
3
5
|
# @!attribute type
|
4
6
|
# @return [Symbol] The metric type. Must be one of {StatsD::Instrument::Metric::TYPES}
|
5
7
|
# @!attribute name
|
6
8
|
# @return [String] The name of the metric. {StatsD#prefix} will automatically be applied
|
7
|
-
# to the metric in the constructor, unless the <tt>:no_prefix</tt> option is set or is
|
9
|
+
# to the metric in the constructor, unless the <tt>:no_prefix</tt> option is set or is
|
8
10
|
# overridden by the <tt>:prefix</tt> option. Note that <tt>:no_prefix</tt> has greater
|
9
11
|
# precedence than <tt>:prefix</tt>.
|
10
12
|
# @!attribute value
|
@@ -29,7 +31,6 @@
|
|
29
31
|
# @see StatsD::Instrument::Backend A StatsD::Instrument::Backend is used to collect metrics.
|
30
32
|
#
|
31
33
|
class StatsD::Instrument::Metric
|
32
|
-
|
33
34
|
attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
|
34
35
|
|
35
36
|
# Initializes a new metric instance.
|
@@ -47,9 +48,18 @@ class StatsD::Instrument::Metric
|
|
47
48
|
# @option options [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
|
48
49
|
# See {.normalize_tags} for more information.
|
49
50
|
def initialize(options = {})
|
50
|
-
|
51
|
-
|
52
|
-
|
51
|
+
if options[:type]
|
52
|
+
@type = options[:type]
|
53
|
+
else
|
54
|
+
raise ArgumentError, "Metric :type is required."
|
55
|
+
end
|
56
|
+
|
57
|
+
if options[:name]
|
58
|
+
@name = normalize_name(options[:name])
|
59
|
+
else
|
60
|
+
raise ArgumentError, "Metric :name is required."
|
61
|
+
end
|
62
|
+
|
53
63
|
unless options[:no_prefix]
|
54
64
|
@name = if options[:prefix]
|
55
65
|
"#{options[:prefix]}.#{@name}"
|
@@ -58,10 +68,13 @@ class StatsD::Instrument::Metric
|
|
58
68
|
end
|
59
69
|
end
|
60
70
|
|
61
|
-
@value
|
71
|
+
@value = options[:value] || default_value
|
62
72
|
@sample_rate = options[:sample_rate] || StatsD.default_sample_rate
|
63
|
-
@tags
|
64
|
-
|
73
|
+
@tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
|
74
|
+
if StatsD.default_tags
|
75
|
+
@tags = Array(@tags) + StatsD.default_tags
|
76
|
+
end
|
77
|
+
@metadata = options.reject { |k, _| [:type, :name, :value, :sample_rate, :tags].include?(k) }
|
65
78
|
end
|
66
79
|
|
67
80
|
# The default value for this metric, which will be used if it is not set.
|
@@ -73,36 +86,36 @@ class StatsD::Instrument::Metric
|
|
73
86
|
# @raise ArgumentError if the metric type doesn't have a default value
|
74
87
|
def default_value
|
75
88
|
case type
|
76
|
-
|
77
|
-
|
89
|
+
when :c then 1
|
90
|
+
else raise ArgumentError, "A value is required for metric type #{type.inspect}."
|
78
91
|
end
|
79
92
|
end
|
80
93
|
|
81
94
|
# @private
|
82
95
|
# @return [String]
|
83
96
|
def to_s
|
84
|
-
str = "#{TYPES[type]} #{name}:#{value}"
|
97
|
+
str = +"#{TYPES[type]} #{name}:#{value}"
|
85
98
|
str << " @#{sample_rate}" if sample_rate != 1.0
|
86
|
-
tags
|
99
|
+
tags&.each { |tag| str << " ##{tag}" }
|
87
100
|
str
|
88
101
|
end
|
89
102
|
|
90
103
|
# @private
|
91
104
|
# @return [String]
|
92
105
|
def inspect
|
93
|
-
"#<StatsD::Instrument::Metric #{self
|
106
|
+
"#<StatsD::Instrument::Metric #{self}>"
|
94
107
|
end
|
95
108
|
|
96
109
|
# The metric types that are supported by this library. Note that every StatsD server
|
97
110
|
# implementation only supports a subset of them.
|
98
111
|
TYPES = {
|
99
|
-
c:
|
112
|
+
c: 'increment',
|
100
113
|
ms: 'measure',
|
101
|
-
g:
|
102
|
-
h:
|
103
|
-
d:
|
114
|
+
g: 'gauge',
|
115
|
+
h: 'histogram',
|
116
|
+
d: 'distribution',
|
104
117
|
kv: 'key/value',
|
105
|
-
s:
|
118
|
+
s: 'set',
|
106
119
|
}
|
107
120
|
|
108
121
|
# Strip metric names of special characters used by StatsD line protocol, replace with underscore
|
@@ -110,7 +123,7 @@ class StatsD::Instrument::Metric
|
|
110
123
|
# @param name [String]
|
111
124
|
# @return [String]
|
112
125
|
def normalize_name(name)
|
113
|
-
name.tr(':|@'
|
126
|
+
name.tr(':|@', '_')
|
114
127
|
end
|
115
128
|
|
116
129
|
# Utility function to convert tags to the canonical form.
|
@@ -122,7 +135,7 @@ class StatsD::Instrument::Metric
|
|
122
135
|
# @return [Array<String>, nil] the list of tags in canonical form.
|
123
136
|
def self.normalize_tags(tags)
|
124
137
|
return unless tags
|
125
|
-
tags = tags.map { |k, v| k.to_s + ":"
|
126
|
-
tags.map { |tag| tag.tr('|,'
|
138
|
+
tags = tags.map { |k, v| k.to_s + ":" + v.to_s } if tags.is_a?(Hash)
|
139
|
+
tags.map { |tag| tag.tr('|,', '') }
|
127
140
|
end
|
128
141
|
end
|
@@ -1,15 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# @private
|
2
4
|
class StatsD::Instrument::MetricExpectation
|
3
|
-
|
4
5
|
attr_accessor :times, :type, :name, :value, :sample_rate, :tags
|
5
6
|
attr_reader :ignore_tags
|
6
7
|
|
7
8
|
def initialize(options = {})
|
8
|
-
|
9
|
-
|
9
|
+
if options[:type]
|
10
|
+
@type = options[:type]
|
11
|
+
else
|
12
|
+
raise ArgumentError, "Metric :type is required."
|
13
|
+
end
|
14
|
+
|
15
|
+
if options[:name]
|
16
|
+
@name = options[:name]
|
17
|
+
else
|
18
|
+
raise ArgumentError, "Metric :name is required."
|
19
|
+
end
|
20
|
+
|
21
|
+
if options[:times]
|
22
|
+
@times = options[:times]
|
23
|
+
else
|
24
|
+
raise ArgumentError, "Metric :times is required."
|
25
|
+
end
|
26
|
+
|
10
27
|
@name = StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name unless options[:no_prefix]
|
11
28
|
@tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
|
12
|
-
@times = options[:times] or raise ArgumentError, "Metric :times is required."
|
13
29
|
@sample_rate = options[:sample_rate]
|
14
30
|
@value = options[:value]
|
15
31
|
@ignore_tags = StatsD::Instrument::Metric.normalize_tags(options[:ignore_tags])
|
@@ -29,7 +45,7 @@ class StatsD::Instrument::MetricExpectation
|
|
29
45
|
actual_tags -= ignored_tags
|
30
46
|
|
31
47
|
if ignore_tags.is_a?(Array)
|
32
|
-
actual_tags.delete_if{ |key| ignore_tags.include?(key.split(":").first) }
|
48
|
+
actual_tags.delete_if { |key| ignore_tags.include?(key.split(":").first) }
|
33
49
|
end
|
34
50
|
end
|
35
51
|
|
@@ -39,30 +55,28 @@ class StatsD::Instrument::MetricExpectation
|
|
39
55
|
end
|
40
56
|
|
41
57
|
def default_value
|
42
|
-
|
43
|
-
when :c; 1
|
44
|
-
end
|
58
|
+
1 if type == :c
|
45
59
|
end
|
46
60
|
|
47
61
|
TYPES = {
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
62
|
+
c: 'increment',
|
63
|
+
ms: 'measure',
|
64
|
+
g: 'gauge',
|
65
|
+
h: 'histogram',
|
66
|
+
d: 'distribution',
|
67
|
+
kv: 'key/value',
|
68
|
+
s: 'set',
|
55
69
|
}
|
56
70
|
|
57
71
|
def to_s
|
58
|
-
str = "#{TYPES[type]} #{name}:#{value}"
|
72
|
+
str = +"#{TYPES[type]} #{name}:#{value}"
|
59
73
|
str << " @#{sample_rate}" if sample_rate != 1.0
|
60
|
-
str << " " << tags.map { |t| "##{t}"}.join(' ') if tags
|
74
|
+
str << " " << tags.map { |t| "##{t}" }.join(' ') if tags
|
61
75
|
str << " times:#{times}" if times > 1
|
62
76
|
str
|
63
77
|
end
|
64
78
|
|
65
79
|
def inspect
|
66
|
-
"#<StatsD::Instrument::MetricExpectation #{self
|
80
|
+
"#<StatsD::Instrument::MetricExpectation #{self}>"
|
67
81
|
end
|
68
82
|
end
|
@@ -1,9 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This Railtie runs some initializers that will set the logger to <tt>Rails#logger</tt>,
|
2
4
|
# and will initialize the {StatsD#backend} based on the Rails environment.
|
3
5
|
#
|
4
6
|
# @see StatsD::Instrument::Environment
|
5
7
|
class StatsD::Instrument::Railtie < Rails::Railtie
|
6
|
-
|
7
8
|
initializer 'statsd-instrument.use_rails_logger' do
|
8
9
|
::StatsD.logger = Rails.logger
|
9
10
|
end
|
data/statsd-instrument.gemspec
CHANGED
@@ -1,21 +1,23 @@
|
|
1
|
+
# frozen-string-literal: true
|
1
2
|
# encoding: utf-8
|
3
|
+
|
2
4
|
lib = File.expand_path('../lib', __FILE__)
|
3
5
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
6
|
require 'statsd/instrument/version'
|
5
7
|
|
6
8
|
Gem::Specification.new do |spec|
|
7
|
-
spec.name
|
8
|
-
spec.version
|
9
|
-
spec.authors
|
10
|
-
spec.email
|
11
|
-
spec.homepage
|
12
|
-
spec.summary
|
9
|
+
spec.name = "statsd-instrument"
|
10
|
+
spec.version = StatsD::Instrument::VERSION
|
11
|
+
spec.authors = ["Jesse Storimer", "Tobias Lutke", "Willem van Bergen"]
|
12
|
+
spec.email = ["jesse@shopify.com"]
|
13
|
+
spec.homepage = "https://github.com/Shopify/statsd-instrument"
|
14
|
+
spec.summary = %q{A StatsD client for Ruby apps}
|
13
15
|
spec.description = %q{A StatsD client for Ruby apps. Provides metaprogramming methods to inject StatsD instrumentation into your code.}
|
14
|
-
spec.license
|
16
|
+
spec.license = "MIT"
|
15
17
|
|
16
|
-
spec.files
|
17
|
-
spec.executables
|
18
|
-
spec.test_files
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
21
|
spec.require_paths = ["lib"]
|
20
22
|
|
21
23
|
spec.add_development_dependency 'rake'
|
@@ -23,5 +25,6 @@ Gem::Specification.new do |spec|
|
|
23
25
|
spec.add_development_dependency 'rspec'
|
24
26
|
spec.add_development_dependency 'mocha'
|
25
27
|
spec.add_development_dependency 'yard'
|
28
|
+
spec.add_development_dependency 'rubocop'
|
26
29
|
spec.add_development_dependency 'benchmark-ips'
|
27
30
|
end
|
data/test/assertions_test.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'test_helper'
|
2
4
|
|
3
5
|
class AssertionsTest < Minitest::Test
|
@@ -163,6 +165,15 @@ class AssertionsTest < Minitest::Test
|
|
163
165
|
end
|
164
166
|
end
|
165
167
|
|
168
|
+
def test_tags_friendly_error
|
169
|
+
@test_case.assert_statsd_increment('counter', tags: { class: "AnotherJob" }) do
|
170
|
+
StatsD.increment('counter', tags: { class: "MyJob" })
|
171
|
+
end
|
172
|
+
rescue MiniTest::Assertion => assertion
|
173
|
+
assert_match(/Captured metrics with the same key/, assertion.message)
|
174
|
+
assert_match(/MyJob/, assertion.message)
|
175
|
+
end
|
176
|
+
|
166
177
|
def test_multiple_metrics_are_not_order_dependent
|
167
178
|
assert_no_assertion_triggered do
|
168
179
|
foo_1_metric = StatsD::Instrument::MetricExpectation.new(type: :c, name: 'counter', times: 1, tags: ['foo:1'])
|
@@ -262,7 +273,7 @@ class AssertionsTest < Minitest::Test
|
|
262
273
|
def test_assert_statsd_call_with_wrong_sample_rate_type
|
263
274
|
assert_assertion_triggered "Unexpected sample rate type for metric counter, must be numeric" do
|
264
275
|
@test_case.assert_statsd_increment('counter', tags: ['a', 'b']) do
|
265
|
-
StatsD.increment('counter', sample_rate: 'abc', tags:
|
276
|
+
StatsD.increment('counter', sample_rate: 'abc', tags: ['a', 'b'])
|
266
277
|
end
|
267
278
|
end
|
268
279
|
end
|
@@ -309,7 +320,7 @@ class AssertionsTest < Minitest::Test
|
|
309
320
|
def assert_no_assertion_triggered(&block)
|
310
321
|
block.call
|
311
322
|
rescue MiniTest::Assertion => assertion
|
312
|
-
flunk
|
323
|
+
flunk("No assertion trigger expected, but one was triggered with message #{assertion.message}.")
|
313
324
|
else
|
314
325
|
pass
|
315
326
|
end
|
@@ -318,12 +329,12 @@ class AssertionsTest < Minitest::Test
|
|
318
329
|
block.call
|
319
330
|
rescue MiniTest::Assertion => assertion
|
320
331
|
if message
|
321
|
-
assert_equal
|
332
|
+
assert_equal(message, assertion.message, "Assertion triggered, but message was not what was expected.")
|
322
333
|
else
|
323
334
|
pass
|
324
335
|
end
|
325
336
|
assertion
|
326
337
|
else
|
327
|
-
flunk
|
338
|
+
flunk("No assertion was triggered, but one was expected.")
|
328
339
|
end
|
329
340
|
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'statsd-instrument'
|
4
|
+
require 'benchmark/ips'
|
5
|
+
|
6
|
+
StatsD.logger = Logger.new('/dev/null')
|
7
|
+
|
8
|
+
class Suite
|
9
|
+
def warming(*args)
|
10
|
+
StatsD.default_tags = if args[0] == "with default tags"
|
11
|
+
{ first_tag: 'first_value', second_tag: 'second_value' }
|
12
|
+
end
|
13
|
+
puts "warming with default tags: #{StatsD.default_tags}"
|
14
|
+
end
|
15
|
+
|
16
|
+
def running(*args)
|
17
|
+
StatsD.default_tags = if args[0] == "with default tags"
|
18
|
+
{ first_tag: 'first_value', second_tag: 'second_value' }
|
19
|
+
end
|
20
|
+
puts "running with default tags: #{StatsD.default_tags}"
|
21
|
+
end
|
22
|
+
|
23
|
+
def warmup_stats(*)
|
24
|
+
end
|
25
|
+
|
26
|
+
def add_report(*)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
suite = Suite.new
|
31
|
+
|
32
|
+
Benchmark.ips do |bench|
|
33
|
+
bench.config(suite: suite)
|
34
|
+
bench.report("without default tags") do
|
35
|
+
StatsD.increment('GoogleBase.insert', tags: {
|
36
|
+
first_tag: 'first_value',
|
37
|
+
second_tag: 'second_value',
|
38
|
+
third_tag: 'third_value',
|
39
|
+
})
|
40
|
+
end
|
41
|
+
|
42
|
+
bench.report("with default tags") do
|
43
|
+
StatsD.increment('GoogleBase.insert', tags: { third_tag: 'third_value' })
|
44
|
+
end
|
45
|
+
|
46
|
+
bench.compare!
|
47
|
+
end
|