statsd-instrument 2.6.0 → 2.7.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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -9
  3. data/benchmark/datagram-client +0 -1
  4. data/benchmark/send-metrics-to-dev-null-log +0 -1
  5. data/lib/statsd/instrument.rb +66 -292
  6. data/lib/statsd/instrument/assertions.rb +83 -93
  7. data/lib/statsd/instrument/client.rb +2 -2
  8. data/lib/statsd/instrument/datagram.rb +12 -3
  9. data/lib/statsd/instrument/environment.rb +9 -3
  10. data/lib/statsd/instrument/expectation.rb +93 -0
  11. data/lib/statsd/instrument/helpers.rb +29 -11
  12. data/lib/statsd/instrument/legacy_client.rb +301 -0
  13. data/lib/statsd/instrument/metric.rb +8 -8
  14. data/lib/statsd/instrument/rubocop.rb +18 -0
  15. data/lib/statsd/instrument/rubocop/singleton_configuration.rb +53 -0
  16. data/lib/statsd/instrument/version.rb +1 -1
  17. data/test/assertions_on_legacy_client_test.rb +376 -0
  18. data/test/assertions_test.rb +105 -39
  19. data/test/capture_sink_test.rb +0 -2
  20. data/test/client_test.rb +0 -2
  21. data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +0 -1
  22. data/test/datagram_builder_test.rb +1 -3
  23. data/test/datagram_test.rb +14 -0
  24. data/test/dogstatsd_datagram_builder_test.rb +0 -2
  25. data/test/environment_test.rb +1 -1
  26. data/test/helpers_test.rb +17 -0
  27. data/test/log_sink_test.rb +0 -2
  28. data/test/logger_backend_test.rb +2 -2
  29. data/test/metric_test.rb +2 -2
  30. data/test/null_sink_test.rb +0 -2
  31. data/test/rubocop/singleton_configuration_test.rb +43 -0
  32. data/test/statsd_datagram_builder_test.rb +0 -2
  33. data/test/statsd_instrumentation_test.rb +4 -6
  34. data/test/statsd_test.rb +1 -1
  35. data/test/test_helper.rb +0 -2
  36. data/test/udp_backend_test.rb +1 -1
  37. data/test/udp_sink_test.rb +0 -2
  38. metadata +11 -3
  39. data/lib/statsd/instrument/metric_expectation.rb +0 -82
@@ -52,20 +52,14 @@ module StatsD::Instrument::Assertions
52
52
  # @return [void]
53
53
  # @raise [Minitest::Assertion] If an exception occurs, or if any metric (with the
54
54
  # provided name, or any), occurred during the execution of the provided block.
55
- def assert_no_statsd_calls(metric_name = nil, &block)
56
- metrics = capture_statsd_calls(&block)
57
- metrics.select! { |m| m.name == metric_name } if metric_name
58
- assert(metrics.empty?, "No StatsD calls for metric #{metrics.map(&:name).join(', ')} expected.")
59
- rescue => exception
60
- flunk(<<~MESSAGE)
61
- An exception occurred in the block provided to the StatsD assertion.
62
-
63
- #{exception.class.name}: #{exception.message}
64
- \t#{exception.backtrace.join("\n\t")}
55
+ def assert_no_statsd_calls(metric_name = nil, datagrams: nil, client: nil, &block)
56
+ if datagrams.nil?
57
+ raise LocalJumpError, "assert_no_statsd_calls requires a block" unless block_given?
58
+ datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
59
+ end
65
60
 
66
- If this exception is expected, make sure to handle it using `assert_raises`
67
- inside the block provided to the StatsD assertion.
68
- MESSAGE
61
+ datagrams.select! { |m| m.name == metric_name } if metric_name
62
+ assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(', ')} expected.")
69
63
  end
70
64
 
71
65
  # Asserts that a given counter metric occurred inside the provided block.
@@ -77,8 +71,9 @@ module StatsD::Instrument::Assertions
77
71
  # @return [void]
78
72
  # @raise [Minitest::Assertion] If an exception occurs, or if the metric did
79
73
  # not occur as specified during the execution the block.
80
- def assert_statsd_increment(metric_name, options = {}, &block)
81
- assert_statsd_call(:c, metric_name, options, &block)
74
+ def assert_statsd_increment(metric_name, datagrams: nil, client: nil, **options, &block)
75
+ expectation = StatsD::Instrument::Expectation.increment(metric_name, **options)
76
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
82
77
  end
83
78
 
84
79
  # Asserts that a given timing metric occurred inside the provided block.
@@ -88,8 +83,9 @@ module StatsD::Instrument::Assertions
88
83
  # @yield (see #assert_statsd_increment)
89
84
  # @return [void]
90
85
  # @raise (see #assert_statsd_increment)
91
- def assert_statsd_measure(metric_name, options = {}, &block)
92
- assert_statsd_call(:ms, metric_name, options, &block)
86
+ def assert_statsd_measure(metric_name, datagrams: nil, client: nil, **options, &block)
87
+ expectation = StatsD::Instrument::Expectation.measure(metric_name, **options)
88
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
93
89
  end
94
90
 
95
91
  # Asserts that a given gauge metric occurred inside the provided block.
@@ -99,8 +95,9 @@ module StatsD::Instrument::Assertions
99
95
  # @yield (see #assert_statsd_increment)
100
96
  # @return [void]
101
97
  # @raise (see #assert_statsd_increment)
102
- def assert_statsd_gauge(metric_name, options = {}, &block)
103
- assert_statsd_call(:g, metric_name, options, &block)
98
+ def assert_statsd_gauge(metric_name, datagrams: nil, client: nil, **options, &block)
99
+ expectation = StatsD::Instrument::Expectation.gauge(metric_name, **options)
100
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
104
101
  end
105
102
 
106
103
  # Asserts that a given histogram metric occurred inside the provided block.
@@ -110,8 +107,9 @@ module StatsD::Instrument::Assertions
110
107
  # @yield (see #assert_statsd_increment)
111
108
  # @return [void]
112
109
  # @raise (see #assert_statsd_increment)
113
- def assert_statsd_histogram(metric_name, options = {}, &block)
114
- assert_statsd_call(:h, metric_name, options, &block)
110
+ def assert_statsd_histogram(metric_name, datagrams: nil, client: nil, **options, &block)
111
+ expectation = StatsD::Instrument::Expectation.histogram(metric_name, **options)
112
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
115
113
  end
116
114
 
117
115
  # Asserts that a given distribution metric occurred inside the provided block.
@@ -121,8 +119,9 @@ module StatsD::Instrument::Assertions
121
119
  # @yield (see #assert_statsd_increment)
122
120
  # @return [void]
123
121
  # @raise (see #assert_statsd_increment)
124
- def assert_statsd_distribution(metric_name, options = {}, &block)
125
- assert_statsd_call(:d, metric_name, options, &block)
122
+ def assert_statsd_distribution(metric_name, datagrams: nil, client: nil, **options, &block)
123
+ expectation = StatsD::Instrument::Expectation.distribution(metric_name, **options)
124
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
126
125
  end
127
126
 
128
127
  # Asserts that a given set metric occurred inside the provided block.
@@ -132,8 +131,9 @@ module StatsD::Instrument::Assertions
132
131
  # @yield (see #assert_statsd_increment)
133
132
  # @return [void]
134
133
  # @raise (see #assert_statsd_increment)
135
- def assert_statsd_set(metric_name, options = {}, &block)
136
- assert_statsd_call(:s, metric_name, options, &block)
134
+ def assert_statsd_set(metric_name, datagrams: nil, client: nil, **options, &block)
135
+ expectation = StatsD::Instrument::Expectation.set(metric_name, **options)
136
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
137
137
  end
138
138
 
139
139
  # Asserts that a given key/value metric occurred inside the provided block.
@@ -143,8 +143,9 @@ module StatsD::Instrument::Assertions
143
143
  # @yield (see #assert_statsd_increment)
144
144
  # @return [void]
145
145
  # @raise (see #assert_statsd_increment)
146
- def assert_statsd_key_value(metric_name, options = {}, &block)
147
- assert_statsd_call(:kv, metric_name, options, &block)
146
+ def assert_statsd_key_value(metric_name, datagrams: nil, client: nil, **options, &block)
147
+ expectation = StatsD::Instrument::Expectation.key_value(metric_name, **options)
148
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
148
149
  end
149
150
 
150
151
  # Asserts that the set of provided metric expectations came true.
@@ -153,89 +154,78 @@ module StatsD::Instrument::Assertions
153
154
  # {#assert_statsd_increment} and others.
154
155
  #
155
156
  # @private
156
- # @param [Array<StatsD::Instrument::MetricExpectation>] expected_metrics The set of
157
- # metric expectations to verify.
157
+ # @param [Array<StatsD::Instrument::Expectation>] expectations The set of
158
+ # expectations to verify.
158
159
  # @yield (see #assert_statsd_increment)
159
160
  # @return [void]
160
161
  # @raise (see #assert_statsd_increment)
161
- def assert_statsd_calls(expected_metrics)
162
- raise ArgumentError, "block must be given" unless block_given?
163
-
164
- capture_backend = StatsD::Instrument::Backends::CaptureBackend.new
165
- with_capture_backend(capture_backend) do
166
- begin
167
- yield
168
- rescue => exception
169
- flunk(<<~MESSAGE)
170
- An exception occurred in the block provided to the StatsD assertion.
171
-
172
- #{exception.class.name}: #{exception.message}
173
- \t#{exception.backtrace.join("\n\t")}
174
-
175
- If this exception is expected, make sure to handle it using `assert_raises`
176
- inside the block provided to the StatsD assertion.
177
- MESSAGE
178
- end
179
-
180
- metrics = capture_backend.collected_metrics
181
- matched_expected_metrics = []
182
- expected_metrics.each do |expected_metric|
183
- expected_metric_times = expected_metric.times
184
- expected_metric_times_remaining = expected_metric.times
185
- filtered_metrics = metrics.select { |m| m.type == expected_metric.type && m.name == expected_metric.name }
186
-
187
- if filtered_metrics.empty?
188
- flunk("No StatsD calls for metric #{expected_metric.name} of type #{expected_metric.type} were made.")
189
- end
162
+ def assert_statsd_expectations(expectations, datagrams: nil, client: nil, &block)
163
+ if datagrams.nil?
164
+ raise LocalJumpError, "assert_statsd_expectations requires a block" unless block_given?
165
+ datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
166
+ end
190
167
 
191
- filtered_metrics.each do |metric|
192
- next unless expected_metric.matches(metric)
168
+ expectations = Array(expectations)
169
+ matched_expectations = []
170
+ expectations.each do |expectation|
171
+ expectation_times = expectation.times
172
+ expectation_times_remaining = expectation.times
173
+ filtered_datagrams = datagrams.select { |m| m.type == expectation.type && m.name == expectation.name }
193
174
 
194
- assert(within_numeric_range?(metric.sample_rate),
195
- "Unexpected sample rate type for metric #{metric.name}, must be numeric")
175
+ if filtered_datagrams.empty?
176
+ flunk("No StatsD calls for metric #{expectation.name} of type #{expectation.type} were made.")
177
+ end
196
178
 
197
- if expected_metric_times_remaining == 0
198
- flunk("Unexpected StatsD call; number of times this metric " \
199
- "was expected exceeded: #{expected_metric.inspect}")
200
- end
179
+ filtered_datagrams.each do |datagram|
180
+ next unless expectation.matches(datagram)
201
181
 
202
- expected_metric_times_remaining -= 1
203
- metrics.delete(metric)
204
- if expected_metric_times_remaining == 0
205
- matched_expected_metrics << expected_metric
206
- end
182
+ if expectation_times_remaining == 0
183
+ flunk("Unexpected StatsD call; number of times this metric " \
184
+ "was expected exceeded: #{expectation.inspect}")
207
185
  end
208
186
 
209
- next if expected_metric_times_remaining == 0
210
-
211
- msg = +"Metric expected #{expected_metric_times} times but seen " \
212
- "#{expected_metric_times - expected_metric_times_remaining} " \
213
- "times: #{expected_metric.inspect}."
214
- msg << "\nCaptured metrics with the same key: #{filtered_metrics}" if filtered_metrics.any?
215
- flunk(msg)
187
+ expectation_times_remaining -= 1
188
+ datagrams.delete(datagram)
189
+ if expectation_times_remaining == 0
190
+ matched_expectations << expectation
191
+ end
216
192
  end
217
- expected_metrics -= matched_expected_metrics
218
193
 
219
- unless expected_metrics.empty?
220
- flunk("Unexpected StatsD calls; the following metric expectations " \
221
- "were not satisfied: #{expected_metrics.inspect}")
222
- end
194
+ next if expectation_times_remaining == 0
195
+
196
+ msg = +"Metric expected #{expectation_times} times but seen " \
197
+ "#{expectation_times - expectation_times_remaining} " \
198
+ "times: #{expectation.inspect}."
199
+ msg << "\nCaptured metrics with the same key: #{filtered_datagrams}" if filtered_datagrams.any?
200
+ flunk(msg)
201
+ end
202
+ expectations -= matched_expectations
223
203
 
224
- pass
204
+ unless expectations.empty?
205
+ flunk("Unexpected StatsD calls; the following metric expectations " \
206
+ "were not satisfied: #{expectations.inspect}")
225
207
  end
208
+
209
+ pass
226
210
  end
227
211
 
212
+ # For backwards compatibility
213
+ alias_method :assert_statsd_calls, :assert_statsd_expectations
214
+ alias_method :assert_statsd_expectation, :assert_statsd_expectations
215
+
228
216
  private
229
217
 
230
- def assert_statsd_call(metric_type, metric_name, options = {}, &block)
231
- options[:name] = metric_name
232
- options[:type] = metric_type
233
- options[:times] ||= 1
234
- expected_metric = StatsD::Instrument::MetricExpectation.new(options)
235
- assert_statsd_calls([expected_metric], &block)
236
- end
218
+ def capture_statsd_datagrams_with_exception_handling(client:, &block)
219
+ capture_statsd_datagrams(client: client, &block)
220
+ rescue => exception
221
+ flunk(<<~MESSAGE)
222
+ An exception occurred in the block provided to the StatsD assertion.
223
+
224
+ #{exception.class.name}: #{exception.message}
225
+ \t#{exception.backtrace.join("\n\t")}
237
226
 
238
- def within_numeric_range?(object)
239
- object.is_a?(Numeric) && (0.0..1.0).cover?(object)
227
+ If this exception is expected, make sure to handle it using `assert_raises`
228
+ inside the block provided to the StatsD assertion.
229
+ MESSAGE
240
230
  end
241
231
  end
@@ -11,8 +11,8 @@ require 'statsd/instrument/log_sink'
11
11
 
12
12
  # The Client is the main interface for using StatsD.
13
13
  #
14
- # @note This new new Client implementation that is intended to become the new default in
15
- # the next major release of this library. While this class may already be functional,
14
+ # @note This new Client implementation is intended to become the new default in the
15
+ # next major release of this library. While this class may already be functional,
16
16
  # we provide no guarantees about the API and the behavior may change.
17
17
  class StatsD::Instrument::Client
18
18
  attr_reader :sink, :datagram_builder_class, :prefix, :default_tags, :default_sample_rate
@@ -17,7 +17,7 @@ class StatsD::Instrument::Datagram
17
17
  end
18
18
 
19
19
  def type
20
- parsed_datagram[:type]
20
+ @type ||= parsed_datagram[:type].to_sym
21
21
  end
22
22
 
23
23
  def name
@@ -25,7 +25,16 @@ class StatsD::Instrument::Datagram
25
25
  end
26
26
 
27
27
  def value
28
- parsed_datagram[:value]
28
+ @value ||= case type
29
+ when :c
30
+ Integer(parsed_datagram[:value])
31
+ when :g, :h, :d, :kv, :ms
32
+ Float(parsed_datagram[:value])
33
+ when :s
34
+ String(parsed_datagram[:value])
35
+ else
36
+ parsed_datagram[:value]
37
+ end
29
38
  end
30
39
 
31
40
  def tags
@@ -59,7 +68,7 @@ class StatsD::Instrument::Datagram
59
68
  \A
60
69
  (?<name>[^\:\|\@]+)\:(?<value>[^\:\|\@]+)\|(?<type>c|ms|g|s|h|d)
61
70
  (?:\|\@(?<sample_rate>\d*(?:\.\d*)?))?
62
- (?:\|\#(?<tags>(?:[^\|\#,]+(?:,[^\|\#,]+)*)))?
71
+ (?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
63
72
  \n? # In some implementations, the datagram may include a trailing newline.
64
73
  \z
65
74
  }x
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'logger'
4
-
5
3
  # The environment module is used to detect, and initialize the environment in
6
4
  # which this library is active. It will use different default values based on the environment.
7
5
  class StatsD::Instrument::Environment
@@ -78,7 +76,7 @@ class StatsD::Instrument::Environment
78
76
  end
79
77
 
80
78
  def statsd_implementation
81
- env.fetch('STATSD_IMPLEMENTATION', 'statsd')
79
+ env.fetch('STATSD_IMPLEMENTATION', 'datadog')
82
80
  end
83
81
 
84
82
  def statsd_sample_rate
@@ -97,6 +95,14 @@ class StatsD::Instrument::Environment
97
95
  env.key?('STATSD_DEFAULT_TAGS') ? env.fetch('STATSD_DEFAULT_TAGS').split(',') : nil
98
96
  end
99
97
 
98
+ def client
99
+ if env.key?('STATSD_USE_NEW_CLIENT')
100
+ default_client
101
+ else
102
+ StatsD::Instrument::LegacyClient.singleton
103
+ end
104
+ end
105
+
100
106
  def default_client
101
107
  @default_client ||= StatsD::Instrument::Client.new(
102
108
  sink: default_sink_for_environment,
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @private
4
+ class StatsD::Instrument::Expectation
5
+ class << self
6
+ def increment(name, **options)
7
+ new(type: :c, name: name, **options)
8
+ end
9
+
10
+ def measure(name, **options)
11
+ new(type: :ms, name: name, **options)
12
+ end
13
+
14
+ def gauge(name, **options)
15
+ new(type: :g, name: name, **options)
16
+ end
17
+
18
+ def set(name, **options)
19
+ new(type: :s, name: name, **options)
20
+ end
21
+
22
+ def key_value(name, **options)
23
+ new(type: :kv, name: name, **options)
24
+ end
25
+
26
+ def distribution(name, **options)
27
+ new(type: :d, name: name, **options)
28
+ end
29
+
30
+ def histogram(name, **options)
31
+ new(type: :h, name: name, **options)
32
+ end
33
+ end
34
+
35
+ attr_accessor :times, :type, :name, :value, :sample_rate, :tags
36
+ attr_reader :ignore_tags
37
+
38
+ def initialize(type:, name:, value: nil, sample_rate: nil, tags: nil, ignore_tags: nil, no_prefix: false, times: 1)
39
+ @type = type
40
+ @name = StatsD.prefix ? "#{StatsD.prefix}.#{name}" : name unless no_prefix
41
+ @value = normalized_value_for_type(type, value) if value
42
+ @sample_rate = sample_rate
43
+ @tags = StatsD::Instrument::Metric.normalize_tags(tags)
44
+ @ignore_tags = StatsD::Instrument::Metric.normalize_tags(ignore_tags)
45
+ @times = times
46
+ end
47
+
48
+ def normalized_value_for_type(type, value)
49
+ case type
50
+ when :c then Integer(value)
51
+ when :g, :h, :d, :kv, :ms then Float(value)
52
+ when :s then String(value)
53
+ else value
54
+ end
55
+ end
56
+
57
+ def matches(actual_metric)
58
+ return false if sample_rate && sample_rate != actual_metric.sample_rate
59
+ return false if value && value != normalized_value_for_type(actual_metric.type, actual_metric.value)
60
+
61
+ if tags
62
+ expected_tags = Set.new(tags)
63
+ actual_tags = Set.new(actual_metric.tags)
64
+
65
+ if ignore_tags
66
+ ignored_tags = Set.new(ignore_tags) - expected_tags
67
+ actual_tags -= ignored_tags
68
+
69
+ if ignore_tags.is_a?(Array)
70
+ actual_tags.delete_if { |key| ignore_tags.include?(key.split(":").first) }
71
+ end
72
+ end
73
+
74
+ return expected_tags.subset?(actual_tags)
75
+ end
76
+ true
77
+ end
78
+
79
+ def to_s
80
+ str = +"#{name}:#{value || '<anything>'}|#{type}"
81
+ str << "|@#{sample_rate}" if sample_rate
82
+ str << "|#" << tags.join(',') if tags
83
+ str << " (expected #{times} times)" if times > 1
84
+ str
85
+ end
86
+
87
+ def inspect
88
+ "#<StatsD::Instrument::Expectation:\"#{self}\">"
89
+ end
90
+ end
91
+
92
+ # For backwards compatibility
93
+ StatsD::Instrument::MetricExpectation = StatsD::Instrument::Expectation
@@ -1,22 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StatsD::Instrument::Helpers
4
- def with_capture_backend(backend, &block)
5
- if StatsD.backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
6
- backend.parent = StatsD.backend
4
+ def capture_statsd_datagrams(client: nil, &block)
5
+ client ||= StatsD.singleton_client
6
+ case client
7
+ when StatsD.legacy_singleton_client
8
+ capture_statsd_metrics_on_legacy_client(&block)
9
+ when StatsD::Instrument::Client
10
+ client.capture(&block)
11
+ else
12
+ raise ArgumentError, "Don't know how to capture StatsD datagrams from #{client.inspect}!"
7
13
  end
8
-
9
- old_backend = StatsD.backend
10
- StatsD.backend = backend
11
-
12
- block.call
13
- ensure
14
- StatsD.backend = old_backend
15
14
  end
16
15
 
17
- def capture_statsd_calls(&block)
16
+ # For backwards compatibility
17
+ alias_method :capture_statsd_calls, :capture_statsd_datagrams
18
+
19
+ def capture_statsd_metrics_on_legacy_client(&block)
18
20
  capture_backend = StatsD::Instrument::Backends::CaptureBackend.new
19
21
  with_capture_backend(capture_backend, &block)
20
22
  capture_backend.collected_metrics
21
23
  end
24
+
25
+ private
26
+
27
+ def with_capture_backend(backend)
28
+ if StatsD.legacy_singleton_client.backend.is_a?(StatsD::Instrument::Backends::CaptureBackend)
29
+ backend.parent = StatsD.legacy_singleton_client.backend
30
+ end
31
+
32
+ old_backend = StatsD.legacy_singleton_client.backend
33
+ begin
34
+ StatsD.legacy_singleton_client.backend = backend
35
+ yield
36
+ ensure
37
+ StatsD.legacy_singleton_client.backend = old_backend
38
+ end
39
+ end
22
40
  end