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.
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