statsd-instrument 2.4.0 → 2.5.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/benchmark.yml +32 -0
  3. data/.github/workflows/ci.yml +24 -8
  4. data/.rubocop.yml +24 -0
  5. data/CHANGELOG.md +116 -3
  6. data/CONTRIBUTING.md +8 -6
  7. data/Gemfile +3 -0
  8. data/Rakefile +1 -1
  9. data/benchmark/README.md +29 -0
  10. data/benchmark/send-metrics-to-dev-null-log +47 -0
  11. data/benchmark/send-metrics-to-local-udp-receiver +57 -0
  12. data/lib/statsd/instrument.rb +126 -94
  13. data/lib/statsd/instrument/assertions.rb +69 -37
  14. data/lib/statsd/instrument/backends/capture_backend.rb +2 -0
  15. data/lib/statsd/instrument/helpers.rb +12 -8
  16. data/lib/statsd/instrument/metric.rb +56 -42
  17. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +46 -0
  18. data/lib/statsd/instrument/rubocop/metric_return_value.rb +31 -0
  19. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +45 -0
  20. data/lib/statsd/instrument/rubocop/positional_arguments.rb +99 -0
  21. data/lib/statsd/instrument/rubocop/splat_arguments.rb +37 -0
  22. data/lib/statsd/instrument/strict.rb +145 -0
  23. data/lib/statsd/instrument/version.rb +1 -1
  24. data/test/assertions_test.rb +37 -0
  25. data/test/benchmark/clock_gettime.rb +27 -0
  26. data/test/benchmark/default_tags.rb +1 -1
  27. data/test/deprecations_test.rb +86 -0
  28. data/test/helpers/rubocop_helper.rb +47 -0
  29. data/test/integration_test.rb +6 -2
  30. data/test/matchers_test.rb +9 -9
  31. data/test/metric_test.rb +3 -18
  32. data/test/rubocop/metaprogramming_positional_arguments_test.rb +58 -0
  33. data/test/rubocop/metric_return_value_test.rb +78 -0
  34. data/test/rubocop/metric_value_keyword_argument_test.rb +39 -0
  35. data/test/rubocop/positional_arguments_test.rb +110 -0
  36. data/test/rubocop/splat_arguments_test.rb +27 -0
  37. data/test/statsd_instrumentation_test.rb +77 -86
  38. data/test/statsd_test.rb +32 -65
  39. data/test/test_helper.rb +12 -1
  40. data/test/udp_backend_test.rb +8 -0
  41. metadata +28 -2
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statsd-instrument' unless Object.const_defined?(:StatsD)
4
+
5
+ module StatsD
6
+ module Instrument
7
+ UNSPECIFIED = Object.new.freeze
8
+ private_constant :UNSPECIFIED
9
+
10
+ # The Strict monkeypatch can be loaded to see if you're using the StatsD library in
11
+ # a deprecated way.
12
+ #
13
+ # - The metric methods are not retuning a Metric instance.
14
+ # - Only accept keyword arguments for tags and sample_rate, rather than position arguments.
15
+ # - Only accept a position argument for value, rather than a keyword argument.
16
+ # - The provided arguments have the right type.
17
+ #
18
+ # You can enable thois monkeypatch by changing your Gemfile as follows:
19
+ #
20
+ # gem 'statsd-instrument', require: 'statsd/instrument/strict'
21
+ #
22
+ # By doing this as part of your QA/CI, you can find where you are still using deprecated patterns,
23
+ # and fix them before the deprecated behavior is removed in the next major version.
24
+ #
25
+ # This monkeypatch is not meant to be used in production.
26
+ module Strict
27
+ def increment(key, value = 1, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
28
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
29
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
30
+ check_tags_and_sample_rate(sample_rate, tags)
31
+
32
+ super
33
+ end
34
+
35
+ def gauge(key, value, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
36
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
37
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
38
+ check_tags_and_sample_rate(sample_rate, tags)
39
+
40
+ super
41
+ end
42
+
43
+ def histogram(key, value, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
44
+ raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
45
+ raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
46
+ check_tags_and_sample_rate(sample_rate, tags)
47
+
48
+ super
49
+ end
50
+
51
+ def set(key, value, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
52
+ raise ArgumentError, "StatsD.set does not accept a block" if block_given?
53
+ check_tags_and_sample_rate(sample_rate, tags)
54
+
55
+ super
56
+ end
57
+
58
+ def measure(key, value = UNSPECIFIED, sample_rate: nil, tags: nil,
59
+ prefix: StatsD.prefix, no_prefix: false, as_dist: false, &block)
60
+
61
+ check_block_or_numeric_value(value, &block)
62
+ check_tags_and_sample_rate(sample_rate, tags)
63
+
64
+ super
65
+ end
66
+
67
+ def distribution(key, value = UNSPECIFIED, sample_rate: nil, tags: nil,
68
+ prefix: StatsD.prefix, no_prefix: false, &block)
69
+
70
+ check_block_or_numeric_value(value, &block)
71
+ check_tags_and_sample_rate(sample_rate, tags)
72
+
73
+ super
74
+ end
75
+
76
+ private
77
+
78
+ def check_block_or_numeric_value(value)
79
+ if block_given?
80
+ raise ArgumentError, "The value argument should not be set when providing a block" unless value == UNSPECIFIED
81
+ else
82
+ raise ArgumentError, "The value argument should be a number, got #{value.inspect}" unless value.is_a?(Numeric)
83
+ end
84
+ end
85
+
86
+ def check_tags_and_sample_rate(sample_rate, tags)
87
+ unless sample_rate.nil? || sample_rate.is_a?(Numeric)
88
+ raise ArgumentError, "The sample_rate argument should be a number, got #{sample_rate}"
89
+ end
90
+ unless tags.nil? || tags.is_a?(Hash) || tags.is_a?(Array)
91
+ raise ArgumentError, "The tags argument should be a hash or an array, got #{tags.inspect}"
92
+ end
93
+ end
94
+
95
+ def collect_metric(type, name, value, sample_rate:, tags: nil, prefix:, metadata: nil)
96
+ super
97
+ nil # We explicitly discard the return value, so people cannot depend on it.
98
+ end
99
+ end
100
+
101
+ module StrictMetaprogramming
102
+ def statsd_measure(method, name, sample_rate: nil, tags: nil,
103
+ prefix: StatsD.prefix, no_prefix: false, as_dist: false)
104
+
105
+ check_method_and_metric_name(method, name)
106
+ super
107
+ end
108
+
109
+ def statsd_distribution(method, name, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
110
+ check_method_and_metric_name(method, name)
111
+ super
112
+ end
113
+
114
+ def statsd_count_success(method, name, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
115
+ check_method_and_metric_name(method, name)
116
+ super
117
+ end
118
+
119
+ def statsd_count_if(method, name, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
120
+ check_method_and_metric_name(method, name)
121
+ super
122
+ end
123
+
124
+ def statsd_count(method, name, sample_rate: nil, tags: nil, prefix: StatsD.prefix, no_prefix: false)
125
+ check_method_and_metric_name(method, name)
126
+ super
127
+ end
128
+
129
+ private
130
+
131
+ def check_method_and_metric_name(method, metric_name)
132
+ unless method.is_a?(Symbol)
133
+ raise ArgumentError, "The method name should be provided as symbol, got #{method.inspect}"
134
+ end
135
+
136
+ unless metric_name.is_a?(String) || metric_name.is_a?(Proc)
137
+ raise ArgumentError, "The metric name should be a proc or string, got #{metric_name.inspect}"
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+
144
+ StatsD.singleton_class.prepend(StatsD::Instrument::Strict)
145
+ StatsD::Instrument.prepend(StatsD::Instrument::StrictMetaprogramming)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "2.4.0"
5
+ VERSION = "2.5.0"
6
6
  end
7
7
  end
@@ -271,6 +271,7 @@ class AssertionsTest < Minitest::Test
271
271
  end
272
272
 
273
273
  def test_assert_statsd_call_with_wrong_sample_rate_type
274
+ skip("In Strict mode, the StatsD.increment call will raise") if StatsD::Instrument.strict_mode_enabled?
274
275
  assert_assertion_triggered "Unexpected sample rate type for metric counter, must be numeric" do
275
276
  @test_case.assert_statsd_increment('counter', tags: ['a', 'b']) do
276
277
  StatsD.increment('counter', sample_rate: 'abc', tags: ['a', 'b'])
@@ -315,6 +316,42 @@ class AssertionsTest < Minitest::Test
315
316
  end
316
317
  end
317
318
 
319
+ def test_assertion_with_exceptions
320
+ assert_no_assertion_triggered do
321
+ @test_case.assert_raises(RuntimeError) do
322
+ @test_case.assert_statsd_increment('counter') do
323
+ StatsD.increment('counter')
324
+ raise "foo"
325
+ end
326
+ end
327
+ end
328
+
329
+ assert_no_assertion_triggered do
330
+ @test_case.assert_statsd_increment('counter') do
331
+ @test_case.assert_raises(RuntimeError) do
332
+ StatsD.increment('counter')
333
+ raise "foo"
334
+ end
335
+ end
336
+ end
337
+
338
+ assert_assertion_triggered do
339
+ @test_case.assert_statsd_increment('counter') do
340
+ @test_case.assert_raises(RuntimeError) do
341
+ raise "foo"
342
+ end
343
+ end
344
+ end
345
+
346
+ assert_assertion_triggered do
347
+ @test_case.assert_raises(RuntimeError) do
348
+ @test_case.assert_statsd_increment('counter') do
349
+ raise "foo"
350
+ end
351
+ end
352
+ end
353
+ end
354
+
318
355
  private
319
356
 
320
357
  def assert_no_assertion_triggered(&block)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'benchmark/ips'
4
+
5
+ Benchmark.ips do |bench|
6
+ bench.report("Process.clock_gettime in milliseconds (int)") do
7
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
8
+ end
9
+
10
+ bench.report("Process.clock_gettime in milliseconds (float)") do
11
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
12
+ end
13
+
14
+ bench.report("Process.clock_gettime in seconds (float), multiplied by 1000") do
15
+ 1000 * Process.clock_gettime(Process::CLOCK_MONOTONIC)
16
+ end
17
+
18
+ bench.report("Process.clock_gettime in seconds (float), multiplied by 1000.0") do
19
+ 1000.0 * Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ end
21
+
22
+ bench.report("Time.now, multiplied by 1000") do
23
+ 1000 * Time.now.to_f
24
+ end
25
+
26
+ bench.compare!
27
+ end
@@ -3,7 +3,7 @@
3
3
  require 'statsd-instrument'
4
4
  require 'benchmark/ips'
5
5
 
6
- StatsD.logger = Logger.new('/dev/null')
6
+ StatsD.logger = Logger.new(File::NULL)
7
7
 
8
8
  class Suite
9
9
  def warming(*args)
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
4
+
5
+ class DeprecationsTest < Minitest::Test
6
+ unless StatsD::Instrument.strict_mode_enabled?
7
+ # rubocop:disable StatsD/MetaprogrammingPositionalArguments
8
+ class InstrumentedClass
9
+ extend StatsD::Instrument
10
+ def foo; end
11
+ statsd_count :foo, 'metric', 0.5, ['tag']
12
+ end
13
+ # rubocop:enable StatsD/MetaprogrammingPositionalArguments
14
+ end
15
+
16
+ include StatsD::Instrument::Assertions
17
+
18
+ def setup
19
+ skip("Deprecation are not supported in strict mode") if StatsD::Instrument.strict_mode_enabled?
20
+ end
21
+
22
+ # rubocop:disable StatsD/MetricValueKeywordArgument
23
+ def test__deprecated__statsd_measure_with_explicit_value_as_keyword_argument
24
+ metric = capture_statsd_call { StatsD.measure('values.foobar', value: 42) }
25
+ assert_equal 'values.foobar', metric.name
26
+ assert_equal 42, metric.value
27
+ assert_equal :ms, metric.type
28
+ end
29
+
30
+ def test__deprecated__statsd_measure_with_explicit_value_keyword_and_distribution_override
31
+ metric = capture_statsd_call { StatsD.measure('values.foobar', value: 42, as_dist: true) }
32
+ assert_equal 42, metric.value
33
+ assert_equal :d, metric.type
34
+ end
35
+
36
+ def test__deprecated__statsd_increment_with_value_as_keyword_argument
37
+ metric = capture_statsd_call { StatsD.increment('values.foobar', value: 2) }
38
+ assert_equal StatsD.default_sample_rate, metric.sample_rate
39
+ assert_equal 2, metric.value
40
+ end
41
+
42
+ def test__deprecated__statsd_gauge_with_keyword_argument
43
+ metric = capture_statsd_call { StatsD.gauge('values.foobar', value: 13) }
44
+ assert_equal :g, metric.type
45
+ assert_equal 'values.foobar', metric.name
46
+ assert_equal 13, metric.value
47
+ end
48
+ # rubocop:enable StatsD/MetricValueKeywordArgument
49
+
50
+ # rubocop:disable StatsD/MetricReturnValue
51
+ def test__deprecated__statsd_increment_retuns_metric_instance
52
+ metric = StatsD.increment('key')
53
+ assert_kind_of StatsD::Instrument::Metric, metric
54
+ assert_equal 'key', metric.name
55
+ assert_equal :c, metric.type
56
+ assert_equal 1, metric.value
57
+ end
58
+ # rubocop:enable StatsD/MetricReturnValue
59
+
60
+ # rubocop:disable StatsD/PositionalArguments
61
+ def test__deprecated__statsd_increment_with_positional_argument_for_tags
62
+ metric = capture_statsd_call { StatsD.increment('values.foobar', 12, nil, ['test']) }
63
+ assert_equal StatsD.default_sample_rate, metric.sample_rate
64
+ assert_equal ['test'], metric.tags
65
+ assert_equal 12, metric.value
66
+ assert_equal StatsD.default_sample_rate, metric.sample_rate
67
+ end
68
+ # rubocop:enable StatsD/PositionalArguments
69
+
70
+ def test__deprecated__metaprogramming_method_with_positional_arguments
71
+ metric = capture_statsd_call { InstrumentedClass.new.foo }
72
+ assert_equal :c, metric.type
73
+ assert_equal 'metric', metric.name
74
+ assert_equal 1, metric.value
75
+ assert_equal 0.5, metric.sample_rate
76
+ assert_equal ["tag"], metric.tags
77
+ end
78
+
79
+ protected
80
+
81
+ def capture_statsd_call(&block)
82
+ metrics = capture_statsd_calls(&block)
83
+ assert_equal 1, metrics.length
84
+ metrics.first
85
+ end
86
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rubocop'
4
+
5
+ module RubocopHelper
6
+ attr_accessor :cop
7
+
8
+ private
9
+
10
+ def assert_no_offenses(source)
11
+ investigate(RuboCop::ProcessedSource.new(source, 2.3, nil))
12
+ assert_predicate cop.offenses, :empty?, "Did not expect Rubocop to find offenses"
13
+ end
14
+
15
+ def assert_offense(source)
16
+ investigate(RuboCop::ProcessedSource.new(source, 2.3, nil))
17
+ refute_predicate cop.offenses, :empty?, "Expected Rubocop to find offenses"
18
+ end
19
+
20
+ def assert_no_autocorrect(source)
21
+ corrected = autocorrect_source(source)
22
+ assert_equal source, corrected
23
+ end
24
+
25
+ def autocorrect_source(source)
26
+ RuboCop::Formatter::DisabledConfigFormatter.config_to_allow_offenses = {}
27
+ RuboCop::Formatter::DisabledConfigFormatter.detected_styles = {}
28
+ cop.instance_variable_get(:@options)[:auto_correct] = true
29
+
30
+ processed_source = RuboCop::ProcessedSource.new(source, 2.3, nil)
31
+ investigate(processed_source)
32
+
33
+ corrector = RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections)
34
+ corrector.rewrite
35
+ end
36
+
37
+ def investigate(processed_source)
38
+ forces = RuboCop::Cop::Force.all.each_with_object([]) do |klass, instances|
39
+ next unless cop.join_force?(klass)
40
+ instances << klass.new([cop])
41
+ end
42
+
43
+ commissioner = RuboCop::Cop::Commissioner.new([cop], forces, raise_error: true)
44
+ commissioner.investigate(processed_source)
45
+ commissioner
46
+ end
47
+ end
@@ -33,8 +33,12 @@ class IntegrationTest < Minitest::Test
33
33
  end
34
34
 
35
35
  Process.kill('TERM', pid)
36
- Process.waitpid(pid)
36
+ _, exit_status = Process.waitpid2(pid)
37
37
 
38
- assert_equal "exiting:1|c", @server.recvfrom(100).first
38
+ assert_equal 0, exit_status, "The foked process did not exit cleanly"
39
+ assert_equal "exiting:1|c", @server.recvfrom_nonblock(100).first
40
+
41
+ rescue NotImplementedError
42
+ pass("Fork is not implemented on #{RUBY_PLATFORM}")
39
43
  end
40
44
  end
@@ -41,7 +41,7 @@ class MatchersTest < Minitest::Test
41
41
 
42
42
  def test_statsd_increment_with_times_not_matched
43
43
  refute StatsD::Instrument::Matchers::Increment.new(:c, 'counter', times: 2)
44
- .matches?(lambda { StatsD.increment('counter', times: 3) })
44
+ .matches? lambda { 3.times { StatsD.increment('counter') } }
45
45
  end
46
46
 
47
47
  def test_statsd_increment_with_sample_rate_matched
@@ -61,15 +61,15 @@ class MatchersTest < Minitest::Test
61
61
 
62
62
  def test_statsd_increment_with_value_matched_when_multiple_metrics
63
63
  assert StatsD::Instrument::Matchers::Increment.new(:c, 'counter', value: 1).matches?(lambda {
64
- StatsD.increment('counter', value: 2)
65
- StatsD.increment('counter', value: 1)
64
+ StatsD.increment('counter', 2)
65
+ StatsD.increment('counter', 1)
66
66
  })
67
67
  end
68
68
 
69
69
  def test_statsd_increment_with_value_not_matched_when_multiple_metrics
70
70
  refute StatsD::Instrument::Matchers::Increment.new(:c, 'counter', value: 1).matches?(lambda {
71
- StatsD.increment('counter', value: 2)
72
- StatsD.increment('counter', value: 3)
71
+ StatsD.increment('counter', 2)
72
+ StatsD.increment('counter', 3)
73
73
  })
74
74
  end
75
75
 
@@ -90,15 +90,15 @@ class MatchersTest < Minitest::Test
90
90
 
91
91
  def test_statsd_increment_with_times_and_value_matched
92
92
  assert StatsD::Instrument::Matchers::Increment.new(:c, 'counter', times: 2, value: 1).matches?(lambda {
93
- StatsD.increment('counter', value: 1)
94
- StatsD.increment('counter', value: 1)
93
+ StatsD.increment('counter', 1)
94
+ StatsD.increment('counter', 1)
95
95
  })
96
96
  end
97
97
 
98
98
  def test_statsd_increment_with_times_and_value_not_matched
99
99
  refute StatsD::Instrument::Matchers::Increment.new(:c, 'counter', times: 2, value: 1).matches?(lambda {
100
- StatsD.increment('counter', value: 1)
101
- StatsD.increment('counter', value: 2)
100
+ StatsD.increment('counter', 1)
101
+ StatsD.increment('counter', 2)
102
102
  })
103
103
  end
104
104
 
data/test/metric_test.rb CHANGED
@@ -16,27 +16,12 @@ class MetricTest < Minitest::Test
16
16
  assert m.tags.nil?
17
17
  end
18
18
 
19
- def test_name_prefix
20
- StatsD.stubs(:prefix).returns('prefix')
21
- m = StatsD::Instrument::Metric.new(type: :c, name: 'counter')
22
- assert_equal 'prefix.counter', m.name
23
-
24
- m = StatsD::Instrument::Metric.new(type: :c, name: 'counter', no_prefix: true)
25
- assert_equal 'counter', m.name
26
-
27
- m = StatsD::Instrument::Metric.new(type: :c, name: 'counter', prefix: "foobar")
28
- assert_equal 'foobar.counter', m.name
29
-
30
- m = StatsD::Instrument::Metric.new(type: :c, name: 'counter', prefix: "foobar", no_prefix: true)
31
- assert_equal 'counter', m.name
32
- end
33
-
34
19
  def test_bad_metric_name
35
- m = StatsD::Instrument::Metric.new(type: :c, name: 'my:metric', no_prefix: true)
20
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my:metric')
36
21
  assert_equal 'my_metric', m.name
37
- m = StatsD::Instrument::Metric.new(type: :c, name: 'my|metric', no_prefix: true)
22
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my|metric')
38
23
  assert_equal 'my_metric', m.name
39
- m = StatsD::Instrument::Metric.new(type: :c, name: 'my@metric', no_prefix: true)
24
+ m = StatsD::Instrument::Metric.new(type: :c, name: 'my@metric')
40
25
  assert_equal 'my_metric', m.name
41
26
  end
42
27