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
@@ -9,6 +9,7 @@ module StatsD::Instrument::Backends
9
9
  # @see StatsD::Instrument::Assertions
10
10
  class CaptureBackend < StatsD::Instrument::Backend
11
11
  attr_reader :collected_metrics
12
+ attr_accessor :parent
12
13
 
13
14
  def initialize
14
15
  reset
@@ -18,6 +19,7 @@ module StatsD::Instrument::Backends
18
19
  # @param metric [StatsD::Instrument::Metric] The metric to collect.
19
20
  # @return [void]
20
21
  def collect_metric(metric)
22
+ parent&.collect_metric(metric)
21
23
  @collected_metrics << metric
22
24
  end
23
25
 
@@ -1,18 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StatsD::Instrument::Helpers
4
- def capture_statsd_calls(&block)
5
- mock_backend = StatsD::Instrument::Backends::CaptureBackend.new
4
+ def with_capture_backend(backend, &block)
5
+ if StatsD.backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
6
+ backend.parent = StatsD.backend
7
+ end
8
+
6
9
  old_backend = StatsD.backend
7
- StatsD.backend = mock_backend
10
+ StatsD.backend = backend
8
11
 
9
12
  block.call
10
- mock_backend.collected_metrics
11
13
  ensure
12
- if old_backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
13
- old_backend.collected_metrics.concat(mock_backend.collected_metrics)
14
- end
15
-
16
14
  StatsD.backend = old_backend
17
15
  end
16
+
17
+ def capture_statsd_calls(&block)
18
+ capture_backend = StatsD::Instrument::Backends::CaptureBackend.new
19
+ with_capture_backend(capture_backend, &block)
20
+ capture_backend.collected_metrics
21
+ end
18
22
  end
@@ -31,50 +31,24 @@
31
31
  # @see StatsD::Instrument::Backend A StatsD::Instrument::Backend is used to collect metrics.
32
32
  #
33
33
  class StatsD::Instrument::Metric
34
- attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
35
-
36
- # Initializes a new metric instance.
37
- # Normally, you don't want to call this method directly, but use one of the metric collection
38
- # methods on the {StatsD} module.
39
- #
40
- # @option options [Symbol] :type The type of the metric.
41
- # @option options [String] :name The name of the metric without prefix.
42
- # @option options [String] :prefix Override the default StatsD prefix.
43
- # @option options [Boolean] :no_prefix Set to <tt>true</tt> if you don't want to apply a prefix.
44
- # @option options [Numeric, String, nil] :value The value to collect for the metric. If set to
45
- # <tt>nil>/tt>, {#default_value} will be used.
46
- # @option options [Numeric, nil] :sample_rate The sample rate to use. If not set, it will use
47
- # {StatsD#default_sample_rate}.
48
- # @option options [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
49
- # See {.normalize_tags} for more information.
50
- def initialize(options = {})
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
-
63
- unless options[:no_prefix]
64
- @name = if options[:prefix]
65
- "#{options[:prefix]}.#{@name}"
66
- else
67
- StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name
34
+ unless Regexp.method_defined?(:match?) # for ruby 2.3
35
+ module RubyBackports
36
+ refine Regexp do
37
+ def match?(str)
38
+ (self =~ str) != nil
39
+ end
68
40
  end
69
41
  end
70
42
 
71
- @value = options[:value] || default_value
72
- @sample_rate = options[:sample_rate] || StatsD.default_sample_rate
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) }
43
+ using RubyBackports
44
+ end
45
+
46
+ def self.new(
47
+ type:, name:, value: default_value(type), sample_rate: StatsD.default_sample_rate, tags: nil, metadata: nil
48
+ )
49
+ # pass keyword arguments as positional arguments for performance reasons,
50
+ # since MRI's C implementation of new turns keyword arguments into a hash
51
+ super(type, name, value, sample_rate, tags, metadata)
78
52
  end
79
53
 
80
54
  # The default value for this metric, which will be used if it is not set.
@@ -82,15 +56,48 @@ class StatsD::Instrument::Metric
82
56
  # A default value is only defined for counter metrics (<tt>1</tt>). For all other
83
57
  # metric types, this emthod will raise an <tt>ArgumentError</tt>.
84
58
  #
59
+ #
60
+ # A default value is only defined for counter metrics (<tt>1</tt>). For all other
61
+ # metric types, this emthod will raise an <tt>ArgumentError</tt>.
62
+ #
85
63
  # @return [Numeric, String] The default value for this metric.
86
64
  # @raise ArgumentError if the metric type doesn't have a default value
87
- def default_value
65
+ def self.default_value(type)
88
66
  case type
89
67
  when :c then 1
90
68
  else raise ArgumentError, "A value is required for metric type #{type.inspect}."
91
69
  end
92
70
  end
93
71
 
72
+ attr_accessor :type, :name, :value, :sample_rate, :tags, :metadata
73
+
74
+ # Initializes a new metric instance.
75
+ # Normally, you don't want to call this method directly, but use one of the metric collection
76
+ # methods on the {StatsD} module.
77
+ #
78
+ # @param type [Symbol] The type of the metric.
79
+ # @option name [String] :name The name of the metric without prefix.
80
+ # @option value [Numeric, String, nil] The value to collect for the metric.
81
+ # @option sample_rate [Numeric, nil] The sample rate to use. If not set, it will use
82
+ # {StatsD#default_sample_rate}.
83
+ # @option tags [Array<String>, Hash<String, String>, nil] :tags The tags to apply to this metric.
84
+ # See {.normalize_tags} for more information.
85
+ def initialize(type, name, value, sample_rate, tags, metadata) # rubocop:disable Metrics/ParameterLists
86
+ raise ArgumentError, "Metric :type is required." unless type
87
+ raise ArgumentError, "Metric :name is required." unless name
88
+ raise ArgumentError, "Metric :value is required." unless value
89
+
90
+ @type = type
91
+ @name = normalize_name(name)
92
+ @value = value
93
+ @sample_rate = sample_rate
94
+ @tags = StatsD::Instrument::Metric.normalize_tags(tags)
95
+ if StatsD.default_tags
96
+ @tags = Array(@tags) + StatsD.default_tags
97
+ end
98
+ @metadata = metadata
99
+ end
100
+
94
101
  # @private
95
102
  # @return [String]
96
103
  def to_s
@@ -123,6 +130,9 @@ class StatsD::Instrument::Metric
123
130
  # @param name [String]
124
131
  # @return [String]
125
132
  def normalize_name(name)
133
+ # fast path when no normalization is needed to avoid copying the string
134
+ return name unless /[:|@]/.match?(name)
135
+
126
136
  name.tr(':|@', '_')
127
137
  end
128
138
 
@@ -136,6 +146,10 @@ class StatsD::Instrument::Metric
136
146
  def self.normalize_tags(tags)
137
147
  return unless tags
138
148
  tags = tags.map { |k, v| k.to_s + ":" + v.to_s } if tags.is_a?(Hash)
149
+
150
+ # fast path when no string replacement is needed
151
+ return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
152
+
139
153
  tags.map { |tag| tag.tr('|,', '') }
140
154
  end
141
155
  end
@@ -0,0 +1,46 @@
1
+ # frozen-string-literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ # This Rubocop will check for using the metaprogramming macros for positional
7
+ # argument usage, which is deprecated. These macros include `statd_count_if`,
8
+ # `statsd_measure`, etc.
9
+ #
10
+ # Use the following Rubocop invocation to check your project's codebase:
11
+ #
12
+ # rubocop --only StatsD/MetaprogrammingPositionalArguments
13
+ # -r `bundle show statsd-instrument`/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb
14
+ #
15
+ #
16
+ # This cop will not autocorrect the offenses it finds, but generally the fixes are easy to fix
17
+ class MetaprogrammingPositionalArguments < Cop
18
+ MSG = 'Use keyword arguments for StatsD metaprogramming macros'
19
+
20
+ METAPROGRAMMING_METHODS = %i{
21
+ statsd_measure
22
+ statsd_distribution
23
+ statsd_count_success
24
+ statsd_count_if
25
+ statsd_count
26
+ }
27
+
28
+ def on_send(node)
29
+ if METAPROGRAMMING_METHODS.include?(node.method_name)
30
+ arguments = node.arguments.dup
31
+ arguments.shift # method
32
+ arguments.shift # metric
33
+ arguments.pop if arguments.last&.type == :block_pass
34
+ case arguments.length
35
+ when 0
36
+ when 1
37
+ add_offense(node) if arguments.first.type != :hash
38
+ else
39
+ add_offense(node)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,31 @@
1
+ # frozen-string-literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ # This Rubocop will check for using the return value of StatsD metric calls, which is deprecated.
7
+ # To check your codebase, use the following Rubocop invocation:
8
+ #
9
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop/metric_return_value.rb \
10
+ # --only StatsD/MetricReturnValue
11
+ #
12
+ # This cop cannot autocorrect offenses. In production code, StatsD should be used in a fire-and-forget
13
+ # fashion. This means that you shouldn't rely on the return value. If you really need to access the
14
+ # emitted metrics, you can look into `capture_statsd_calls`
15
+ class MetricReturnValue < Cop
16
+ MSG = 'Do not use the return value of StatsD metric methods'
17
+
18
+ STATSD_METRIC_METHODS = %i{increment gauge measure set histogram distribution key_value}
19
+ INVALID_PARENTS = %i{lvasgn array pair send return yield}
20
+
21
+ def on_send(node)
22
+ if node.receiver&.type == :const && node.receiver&.const_name == "StatsD"
23
+ if STATSD_METRIC_METHODS.include?(node.method_name) && node.arguments.last&.type != :block_pass
24
+ add_offense(node.parent) if INVALID_PARENTS.include?(node.parent&.type)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,45 @@
1
+ # frozen-string-literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ # This Rubocop will check for providing the value for a metric using a keyword argument, which is
7
+ # deprecated. Use the following Rubocop invocation to check your project's codebase:
8
+ #
9
+ # rubocop --require \
10
+ # `bundle show statsd-instrument`/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb \
11
+ # --only StatsD/MetricValueKeywordArgument
12
+ #
13
+ # This cop will not autocorrect offenses. Most of the time, these are easy to fix by providing the
14
+ # value as the second argument, rather than a keyword argument.
15
+ #
16
+ # `StatsD.increment('foo', value: 3)` => `StatsD.increment('foo', 3)`
17
+ class MetricValueKeywordArgument < Cop
18
+ MSG = 'Do not use the value keyword argument, but use a positional argument'
19
+
20
+ STATSD_METRIC_METHODS = %i{increment gauge measure set histogram distribution key_value}
21
+
22
+ def on_send(node)
23
+ if node.receiver&.type == :const && node.receiver&.const_name == "StatsD"
24
+ if STATSD_METRIC_METHODS.include?(node.method_name)
25
+ last_argument = if node.arguments.last&.type == :block_pass
26
+ node.arguments[node.arguments.length - 2]
27
+ else
28
+ node.arguments[node.arguments.length - 1]
29
+ end
30
+
31
+ check_keyword_arguments_for_value_entry(node, last_argument) if last_argument&.type == :hash
32
+ end
33
+ end
34
+ end
35
+
36
+ def check_keyword_arguments_for_value_entry(node, keyword_arguments)
37
+ value_pair_found = keyword_arguments.child_nodes.any? do |pair|
38
+ pair.child_nodes[0].type == :sym && pair.child_nodes[0].value == :value
39
+ end
40
+ add_offense(node) if value_pair_found
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,99 @@
1
+ # frozen-string-literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ # This Rubocop will check for using the StatsD metric methods (e.g. `StatsD.instrument`)
7
+ # for positional argument usage, which is deprecated.
8
+ #
9
+ # Use the following Rubocop invocation to check your project's codebase:
10
+ #
11
+ # rubocop --require `bundle show statsd-instrument`/lib/statsd/instrument/rubocop/positional_arguments.rb \
12
+ # --only StatsD/PositionalArguments
13
+ #
14
+ # This cop can autocorrect some offenses it finds, but not all of them.
15
+ class PositionalArguments < Cop
16
+ MSG = 'Use keyword arguments for StatsD calls'
17
+
18
+ STATSD_SINGLETON_METHODS = %i{increment gauge measure set histogram distribution key_value}
19
+
20
+ POSITIONAL_ARGUMENT_TYPES = Set[:int, :float, :nil]
21
+ UNKNOWN_ARGUMENT_TYPES = Set[:send, :const, :lvar, :splat]
22
+ REFUSED_ARGUMENT_TYPES = POSITIONAL_ARGUMENT_TYPES | UNKNOWN_ARGUMENT_TYPES
23
+
24
+ KEYWORD_ARGUMENT_TYPES = Set[:hash]
25
+ BLOCK_ARGUMENT_TYPES = Set[:block_pass]
26
+ ACCEPTED_ARGUMENT_TYPES = KEYWORD_ARGUMENT_TYPES | BLOCK_ARGUMENT_TYPES
27
+
28
+ def on_send(node)
29
+ if node.receiver&.type == :const && node.receiver&.const_name == "StatsD"
30
+ if STATSD_SINGLETON_METHODS.include?(node.method_name) && node.arguments.length >= 3
31
+ case node.arguments[2].type
32
+ when *REFUSED_ARGUMENT_TYPES
33
+ add_offense(node)
34
+ when *ACCEPTED_ARGUMENT_TYPES
35
+ nil
36
+ else
37
+ $stderr.puts "[StatsD/PositionalArguments] Unhandled argument type: #{node.arguments[2].type.inspect}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def autocorrect(node)
44
+ -> (corrector) do
45
+ positial_arguments = if node.arguments.last.type == :block_pass
46
+ node.arguments[2...node.arguments.length - 1]
47
+ else
48
+ node.arguments[2...node.arguments.length]
49
+ end
50
+
51
+ case positial_arguments[0].type
52
+ when *UNKNOWN_ARGUMENT_TYPES
53
+ # We don't know whether the method returns a hash, in which case it would be interpreted
54
+ # as keyword arguments. In this case, the fix would be to add a keywordf splat:
55
+ #
56
+ # `StatsD.instrument('foo', 1, method_call)`
57
+ # => `StatsD.instrument('foo', 1, **method_call)`
58
+ #
59
+ # However, it's also possible this method returns a sample rate, in which case the fix
60
+ # above will not do the right thing.
61
+ #
62
+ # `StatsD.instrument('foo', 1, SAMPLE_RATE_CONSTANT)`
63
+ # => `StatsD.instrument('foo', 1, sample_rate: SAMPLE_RATE_CONSTANT)`
64
+ #
65
+ # Because of this, we will not auto-correct and let the user fix the issue manually.
66
+ return
67
+
68
+ when *POSITIONAL_ARGUMENT_TYPES
69
+ value_argument = node.arguments[1]
70
+ from = value_argument.source_range.end_pos
71
+ to = positial_arguments.last.source_range.end_pos
72
+ range = Parser::Source::Range.new(node.source_range.source_buffer, from, to)
73
+ corrector.remove(range)
74
+
75
+ keyword_arguments = []
76
+ sample_rate = positial_arguments[0]
77
+ if sample_rate && sample_rate.type != :nil
78
+ keyword_arguments << "sample_rate: #{sample_rate.source}"
79
+ end
80
+
81
+ tags = positial_arguments[1]
82
+ if tags && tags.type != :nil
83
+ keyword_arguments << if tags.type == :hash && tags.source[0] != '{'
84
+ "tags: { #{tags.source} }"
85
+ else
86
+ "tags: #{tags.source}"
87
+ end
88
+ end
89
+
90
+ unless keyword_arguments.empty?
91
+ corrector.insert_after(value_argument.source_range, ", #{keyword_arguments.join(', ')}")
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,37 @@
1
+ # frozen-string-literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module StatsD
6
+ # This Rubocop will check for using splat arguments (*args) in StatsD metric calls. To run
7
+ # this rule on your codebase, invoke Rubocop this way:
8
+ #
9
+ # rubocop --require \
10
+ # `bundle show statsd-instrument`/lib/statsd/instrument/rubocop/splat_arguments.rb \
11
+ # --only StatsD/SplatArguments
12
+ #
13
+ # This cop will not autocorrect offenses.
14
+ class SplatArguments < Cop
15
+ MSG = 'Do not use splat arguments in StatsD metric calls'
16
+
17
+ STATSD_METRIC_METHODS = %i{increment gauge measure set histogram distribution key_value}
18
+
19
+ def on_send(node)
20
+ if node.receiver&.type == :const && node.receiver&.const_name == "StatsD"
21
+ if STATSD_METRIC_METHODS.include?(node.method_name)
22
+ check_for_splat_arguments(node)
23
+ end
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def check_for_splat_arguments(node)
30
+ if node.arguments.any? { |arg| arg.type == :splat }
31
+ add_offense(node)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end