statsd-instrument 2.4.0 → 2.5.0

Sign up to get free protection for your applications and to get access to all the features.
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