statsd-instrument 2.3.5 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|