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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/ci.yml +31 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
  6. data/.rubocop.yml +21 -0
  7. data/CHANGELOG.md +9 -0
  8. data/CONTRIBUTING.md +25 -5
  9. data/Gemfile +2 -0
  10. data/Rakefile +3 -1
  11. data/lib/statsd-instrument.rb +2 -0
  12. data/lib/statsd/instrument.rb +51 -18
  13. data/lib/statsd/instrument/assertions.rb +24 -18
  14. data/lib/statsd/instrument/backend.rb +3 -2
  15. data/lib/statsd/instrument/backends/capture_backend.rb +2 -1
  16. data/lib/statsd/instrument/backends/logger_backend.rb +3 -3
  17. data/lib/statsd/instrument/backends/null_backend.rb +2 -0
  18. data/lib/statsd/instrument/backends/udp_backend.rb +18 -15
  19. data/lib/statsd/instrument/environment.rb +2 -0
  20. data/lib/statsd/instrument/helpers.rb +6 -2
  21. data/lib/statsd/instrument/matchers.rb +14 -11
  22. data/lib/statsd/instrument/metric.rb +34 -21
  23. data/lib/statsd/instrument/metric_expectation.rb +32 -18
  24. data/lib/statsd/instrument/railtie.rb +2 -1
  25. data/lib/statsd/instrument/version.rb +3 -1
  26. data/statsd-instrument.gemspec +13 -10
  27. data/test/assertions_test.rb +15 -4
  28. data/test/benchmark/default_tags.rb +47 -0
  29. data/test/benchmark/metrics.rb +9 -8
  30. data/test/benchmark/tags.rb +5 -3
  31. data/test/capture_backend_test.rb +4 -2
  32. data/test/environment_test.rb +2 -1
  33. data/test/helpers_test.rb +2 -1
  34. data/test/integration_test.rb +27 -7
  35. data/test/logger_backend_test.rb +10 -8
  36. data/test/matchers_test.rb +34 -20
  37. data/test/metric_test.rb +15 -4
  38. data/test/statsd_instrumentation_test.rb +7 -7
  39. data/test/statsd_test.rb +24 -15
  40. data/test/test_helper.rb +2 -0
  41. data/test/udp_backend_test.rb +3 -26
  42. metadata +22 -3
  43. data/.travis.yml +0 -12
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'logger'
2
4
 
3
5
  # The environment module is used to detect, and initialize the environment in
@@ -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, StatsD.backend = StatsD.backend, mock_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.kind_of?(StatsD::Instrument::Backends::CaptureBackend)
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
- begin
27
- expect_statsd_call(@metric_type, @metric_name, @options, &block)
28
- rescue RSpec::Expectations::ExpectationNotMetError => e
29
- @message = e.message
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
- raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made." if metrics.empty?
54
- raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric #{metric_name} was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}" if options[:times] && options[:times] != metrics.length
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
- if !found
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
- @type = options[:type] or raise ArgumentError, "Metric :type is required."
51
- @name = options[:name] or raise ArgumentError, "Metric :name is required."
52
- @name = normalize_name(@name)
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 = options[:value] || default_value
71
+ @value = options[:value] || default_value
62
72
  @sample_rate = options[:sample_rate] || StatsD.default_sample_rate
63
- @tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
64
- @metadata = options.reject { |k, _| [:type, :name, :value, :sample_rate, :tags].include?(k) }
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
- when :c; 1
77
- else raise ArgumentError, "A value is required for metric type #{type.inspect}."
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.each { |tag| str << " ##{tag}" } if 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.to_s}>"
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: 'increment',
112
+ c: 'increment',
100
113
  ms: 'measure',
101
- g: 'gauge',
102
- h: 'histogram',
103
- d: 'distribution',
114
+ g: 'gauge',
115
+ h: 'histogram',
116
+ d: 'distribution',
104
117
  kv: 'key/value',
105
- s: 'set',
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(':|@'.freeze, '_')
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 + ":".freeze + v.to_s } if tags.is_a?(Hash)
126
- tags.map { |tag| tag.tr('|,'.freeze, ''.freeze) }
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
- @type = options[:type] or raise ArgumentError, "Metric :type is required."
9
- @name = options[:name] or raise ArgumentError, "Metric :name is required."
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
- case type
43
- when :c; 1
44
- end
58
+ 1 if type == :c
45
59
  end
46
60
 
47
61
  TYPES = {
48
- c: 'increment',
49
- ms: 'measure',
50
- g: 'gauge',
51
- h: 'histogram',
52
- d: 'distribution',
53
- kv: 'key/value',
54
- s: 'set',
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.to_s}>"
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
@@ -1,5 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StatsD
2
4
  module Instrument
3
- VERSION = "2.3.5"
5
+ VERSION = "2.4.0"
4
6
  end
5
7
  end
@@ -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 = "statsd-instrument"
8
- spec.version = StatsD::Instrument::VERSION
9
- spec.authors = ["Jesse Storimer", "Tobias Lutke", "Willem van Bergen"]
10
- spec.email = ["jesse@shopify.com"]
11
- spec.homepage = "https://github.com/Shopify/statsd-instrument"
12
- spec.summary = %q{A StatsD client for Ruby apps}
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 = "MIT"
16
+ spec.license = "MIT"
15
17
 
16
- spec.files = `git ls-files`.split($/)
17
- spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
- spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
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
@@ -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: ['a', 'b'])
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 "No assertion trigger expected, but one was triggered with message #{assertion.message}."
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 message, assertion.message, "Assertion triggered, but message was not what was expected."
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 "No assertion was triggered, but one was expected."
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