statsd-instrument 3.0.0.pre1 → 3.1.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/lint.yml +22 -0
  3. data/.github/workflows/tests.yml +31 -0
  4. data/.rubocop.yml +3 -13
  5. data/CHANGELOG.md +50 -0
  6. data/Gemfile +8 -2
  7. data/README.md +6 -3
  8. data/Rakefile +7 -7
  9. data/benchmark/send-metrics-to-dev-null-log +12 -11
  10. data/benchmark/send-metrics-to-local-udp-receiver +16 -15
  11. data/bin/rake +29 -0
  12. data/bin/rubocop +29 -0
  13. data/lib/statsd-instrument.rb +1 -1
  14. data/lib/statsd/instrument.rb +112 -145
  15. data/lib/statsd/instrument/assertions.rb +200 -208
  16. data/lib/statsd/instrument/batched_udp_sink.rb +154 -0
  17. data/lib/statsd/instrument/capture_sink.rb +23 -19
  18. data/lib/statsd/instrument/client.rb +410 -306
  19. data/lib/statsd/instrument/datagram.rb +69 -65
  20. data/lib/statsd/instrument/datagram_builder.rb +81 -77
  21. data/lib/statsd/instrument/dogstatsd_datagram.rb +76 -72
  22. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +68 -64
  23. data/lib/statsd/instrument/environment.rb +88 -77
  24. data/lib/statsd/instrument/expectation.rb +96 -96
  25. data/lib/statsd/instrument/helpers.rb +11 -7
  26. data/lib/statsd/instrument/log_sink.rb +20 -16
  27. data/lib/statsd/instrument/matchers.rb +93 -74
  28. data/lib/statsd/instrument/null_sink.rb +12 -8
  29. data/lib/statsd/instrument/railtie.rb +11 -7
  30. data/lib/statsd/instrument/rubocop.rb +8 -8
  31. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +1 -1
  32. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +2 -2
  33. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +1 -1
  34. data/lib/statsd/instrument/rubocop/metric_return_value.rb +2 -2
  35. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +1 -1
  36. data/lib/statsd/instrument/rubocop/positional_arguments.rb +4 -4
  37. data/lib/statsd/instrument/rubocop/singleton_configuration.rb +1 -1
  38. data/lib/statsd/instrument/rubocop/splat_arguments.rb +2 -2
  39. data/lib/statsd/instrument/statsd_datagram_builder.rb +12 -8
  40. data/lib/statsd/instrument/strict.rb +1 -6
  41. data/lib/statsd/instrument/udp_sink.rb +49 -47
  42. data/lib/statsd/instrument/version.rb +1 -1
  43. data/statsd-instrument.gemspec +4 -8
  44. data/test/assertions_test.rb +205 -161
  45. data/test/benchmark/clock_gettime.rb +1 -1
  46. data/test/benchmark/default_tags.rb +9 -9
  47. data/test/benchmark/metrics.rb +8 -8
  48. data/test/benchmark/tags.rb +4 -4
  49. data/test/capture_sink_test.rb +14 -14
  50. data/test/client_test.rb +96 -96
  51. data/test/datagram_builder_test.rb +55 -55
  52. data/test/datagram_test.rb +5 -5
  53. data/test/dogstatsd_datagram_builder_test.rb +37 -37
  54. data/test/environment_test.rb +30 -21
  55. data/test/helpers/rubocop_helper.rb +12 -9
  56. data/test/helpers_test.rb +15 -15
  57. data/test/integration_test.rb +7 -7
  58. data/test/log_sink_test.rb +4 -4
  59. data/test/matchers_test.rb +54 -54
  60. data/test/null_sink_test.rb +4 -4
  61. data/test/rubocop/measure_as_dist_argument_test.rb +2 -2
  62. data/test/rubocop/metaprogramming_positional_arguments_test.rb +2 -2
  63. data/test/rubocop/metric_prefix_argument_test.rb +2 -2
  64. data/test/rubocop/metric_return_value_test.rb +6 -6
  65. data/test/rubocop/metric_value_keyword_argument_test.rb +3 -3
  66. data/test/rubocop/positional_arguments_test.rb +12 -12
  67. data/test/rubocop/singleton_configuration_test.rb +8 -8
  68. data/test/rubocop/splat_arguments_test.rb +2 -2
  69. data/test/statsd_datagram_builder_test.rb +6 -6
  70. data/test/statsd_instrumentation_test.rb +122 -122
  71. data/test/statsd_test.rb +69 -67
  72. data/test/test_helper.rb +19 -10
  73. data/test/udp_sink_test.rb +122 -50
  74. metadata +12 -92
  75. data/.github/workflows/ci.yml +0 -56
  76. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +0 -1027
@@ -1,231 +1,223 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # This module defines several assertion methods that can be used to verify that
4
- # your application is emitting the right StatsD metrics.
5
- #
6
- # Every metric type has its own assertion method, like {#assert_statsd_increment}
7
- # to assert `StatsD.increment` calls. You can also assert other properties of the
8
- # metric that was emitted, like the sample rate or presence of tags.
9
- # To check for the absence of metrics, use {#assert_no_statsd_calls}.
10
- #
11
- # @example Check for metric properties:
12
- # assert_statsd_measure('foo', sample_rate: 0.1, tags: ["bar"]) do
13
- # StatsD.measure('foo', sample_rate: 0.5, tags: ['bar','baz']) do
14
- # some_code_to_measure
15
- # end
16
- # end
17
- #
18
- # @example Check for multiple occurrences:
19
- # assert_statsd_increment('foo', times: 2) do
20
- # StatsD.increment('foo')
21
- # StatsD.increment('foo')
22
- # end
23
- #
24
- # @example Absence of metrics
25
- # assert_no_statsd_calls do
26
- # foo
27
- # end
28
- #
29
- # @example Handling exceptions
30
- # assert_statsd_increment('foo.error') do
31
- # # If we expect exceptions to occur, we have to handle them inside
32
- # # the block we pass to assert_statsd_increment.
33
- # assert_raises(RuntimeError) do
34
- # begin
35
- # attempt_foo
36
- # rescue
37
- # StatsD.increment('foo.error')
38
- # raise 'foo failed'
39
- # end
40
- # end
41
- # end
42
- module StatsD::Instrument::Assertions
43
- include StatsD::Instrument::Helpers
44
-
45
- # Asserts no metric occurred during the execution of the provided block.
46
- #
47
- # @param [Array<String>] metric_names (default: []) The metric names that are not
48
- # allowed to happen inside the block. If this is set to `[]`, the assertion
49
- # will fail if any metric occurs.
50
- # @yield A block in which the specified metric should not occur. This block
51
- # should not raise any exceptions.
52
- # @return [void]
53
- # @raise [Minitest::Assertion] If an exception occurs, or if any metric (with the
54
- # provided names, or any), occurred during the execution of the provided block.
55
- def assert_no_statsd_calls(*metric_names, 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
60
-
61
- datagrams.select! { |metric| metric_names.include?(metric.name) } unless metric_names.empty?
62
- assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(', ')} expected.")
63
- end
64
-
65
- # Asserts that a given counter metric occurred inside the provided block.
66
- #
67
- # @param [String] metric_name The name of the metric that should occur.
68
- # @param [Hash] options (see StatsD::Instrument::MetricExpectation.new)
69
- # @yield A block in which the specified metric should occur. This block
70
- # should not raise any exceptions.
71
- # @return [void]
72
- # @raise [Minitest::Assertion] If an exception occurs, or if the metric did
73
- # not occur as specified during the execution the 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)
77
- end
78
-
79
- # Asserts that a given timing metric occurred inside the provided block.
80
- #
81
- # @param metric_name (see #assert_statsd_increment)
82
- # @param options (see #assert_statsd_increment)
83
- # @yield (see #assert_statsd_increment)
84
- # @return [void]
85
- # @raise (see #assert_statsd_increment)
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)
89
- end
90
-
91
- # Asserts that a given gauge metric occurred inside the provided block.
92
- #
93
- # @param metric_name (see #assert_statsd_increment)
94
- # @param options (see #assert_statsd_increment)
95
- # @yield (see #assert_statsd_increment)
96
- # @return [void]
97
- # @raise (see #assert_statsd_increment)
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)
101
- end
102
-
103
- # Asserts that a given histogram metric occurred inside the provided block.
104
- #
105
- # @param metric_name (see #assert_statsd_increment)
106
- # @param options (see #assert_statsd_increment)
107
- # @yield (see #assert_statsd_increment)
108
- # @return [void]
109
- # @raise (see #assert_statsd_increment)
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)
113
- end
3
+ module StatsD
4
+ module Instrument
5
+ # This module defines several assertion methods that can be used to verify that
6
+ # your application is emitting the right StatsD metrics.
7
+ #
8
+ # Every metric type has its own assertion method, like {#assert_statsd_increment}
9
+ # to assert `StatsD.increment` calls. You can also assert other properties of the
10
+ # metric that was emitted, like the sample rate or presence of tags.
11
+ # To check for the absence of metrics, use {#assert_no_statsd_calls}.
12
+ #
13
+ # @example Check for metric properties:
14
+ # assert_statsd_measure('foo', sample_rate: 0.1, tags: ["bar"]) do
15
+ # StatsD.measure('foo', sample_rate: 0.5, tags: ['bar','baz']) do
16
+ # some_code_to_measure
17
+ # end
18
+ # end
19
+ #
20
+ # @example Check for multiple occurrences:
21
+ # assert_statsd_increment('foo', times: 2) do
22
+ # StatsD.increment('foo')
23
+ # StatsD.increment('foo')
24
+ # end
25
+ #
26
+ # @example Absence of metrics
27
+ # assert_no_statsd_calls do
28
+ # foo
29
+ # end
30
+ #
31
+ # @example Handling exceptions
32
+ # assert_statsd_increment('foo.error') do
33
+ # # If we expect exceptions to occur, we have to handle them inside
34
+ # # the block we pass to assert_statsd_increment.
35
+ # assert_raises(RuntimeError) do
36
+ # begin
37
+ # attempt_foo
38
+ # rescue
39
+ # StatsD.increment('foo.error')
40
+ # raise 'foo failed'
41
+ # end
42
+ # end
43
+ # end
44
+ module Assertions
45
+ include StatsD::Instrument::Helpers
46
+
47
+ # Asserts no metric occurred during the execution of the provided block.
48
+ #
49
+ # @param [Array<String>] metric_names (default: []) The metric names that are not
50
+ # allowed to happen inside the block. If this is set to `[]`, the assertion
51
+ # will fail if any metric occurs.
52
+ # @yield A block in which the specified metric should not occur. This block
53
+ # should not raise any exceptions.
54
+ # @return [void]
55
+ # @raise [Minitest::Assertion] If an exception occurs, or if any metric (with the
56
+ # provided names, or any), occurred during the execution of the provided block.
57
+ def assert_no_statsd_calls(*metric_names, datagrams: nil, client: nil, &block)
58
+ if datagrams.nil?
59
+ raise LocalJumpError, "assert_no_statsd_calls requires a block" unless block_given?
60
+ datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
61
+ end
114
62
 
115
- # Asserts that a given distribution metric occurred inside the provided block.
116
- #
117
- # @param metric_name (see #assert_statsd_increment)
118
- # @param options (see #assert_statsd_increment)
119
- # @yield (see #assert_statsd_increment)
120
- # @return [void]
121
- # @raise (see #assert_statsd_increment)
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)
125
- end
63
+ datagrams.select! { |metric| metric_names.include?(metric.name) } unless metric_names.empty?
64
+ assert(datagrams.empty?, "No StatsD calls for metric #{datagrams.map(&:name).join(", ")} expected.")
65
+ end
126
66
 
127
- # Asserts that a given set metric occurred inside the provided block.
128
- #
129
- # @param metric_name (see #assert_statsd_increment)
130
- # @param options (see #assert_statsd_increment)
131
- # @yield (see #assert_statsd_increment)
132
- # @return [void]
133
- # @raise (see #assert_statsd_increment)
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
- end
67
+ # Asserts that a given counter metric occurred inside the provided block.
68
+ #
69
+ # @param [String] metric_name The name of the metric that should occur.
70
+ # @param [Hash] options (see StatsD::Instrument::MetricExpectation.new)
71
+ # @yield A block in which the specified metric should occur. This block
72
+ # should not raise any exceptions.
73
+ # @return [void]
74
+ # @raise [Minitest::Assertion] If an exception occurs, or if the metric did
75
+ # not occur as specified during the execution the block.
76
+ def assert_statsd_increment(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
77
+ expectation = StatsD::Instrument::Expectation.increment(metric_name, value, **options)
78
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
79
+ end
138
80
 
139
- # Asserts that a given key/value metric occurred inside the provided block.
140
- #
141
- # @param metric_name (see #assert_statsd_increment)
142
- # @param options (see #assert_statsd_increment)
143
- # @yield (see #assert_statsd_increment)
144
- # @return [void]
145
- # @raise (see #assert_statsd_increment)
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)
149
- end
81
+ # Asserts that a given timing metric occurred inside the provided block.
82
+ #
83
+ # @param metric_name (see #assert_statsd_increment)
84
+ # @param options (see #assert_statsd_increment)
85
+ # @yield (see #assert_statsd_increment)
86
+ # @return [void]
87
+ # @raise (see #assert_statsd_increment)
88
+ def assert_statsd_measure(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
89
+ expectation = StatsD::Instrument::Expectation.measure(metric_name, value, **options)
90
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
91
+ end
150
92
 
151
- # Asserts that the set of provided metric expectations came true.
152
- #
153
- # Generally, it's recommended to use more specific assertion methods, like
154
- # {#assert_statsd_increment} and others.
155
- #
156
- # @private
157
- # @param [Array<StatsD::Instrument::Expectation>] expectations The set of
158
- # expectations to verify.
159
- # @yield (see #assert_statsd_increment)
160
- # @return [void]
161
- # @raise (see #assert_statsd_increment)
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
93
+ # Asserts that a given gauge metric occurred inside the provided block.
94
+ #
95
+ # @param metric_name (see #assert_statsd_increment)
96
+ # @param options (see #assert_statsd_increment)
97
+ # @yield (see #assert_statsd_increment)
98
+ # @return [void]
99
+ # @raise (see #assert_statsd_increment)
100
+ def assert_statsd_gauge(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
101
+ expectation = StatsD::Instrument::Expectation.gauge(metric_name, value, **options)
102
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
103
+ end
167
104
 
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 }
105
+ # Asserts that a given histogram metric occurred inside the provided block.
106
+ #
107
+ # @param metric_name (see #assert_statsd_increment)
108
+ # @param options (see #assert_statsd_increment)
109
+ # @yield (see #assert_statsd_increment)
110
+ # @return [void]
111
+ # @raise (see #assert_statsd_increment)
112
+ def assert_statsd_histogram(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
113
+ expectation = StatsD::Instrument::Expectation.histogram(metric_name, value, **options)
114
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
115
+ end
174
116
 
175
- if filtered_datagrams.empty?
176
- flunk("No StatsD calls for metric #{expectation.name} of type #{expectation.type} were made.")
117
+ # Asserts that a given distribution metric occurred inside the provided block.
118
+ #
119
+ # @param metric_name (see #assert_statsd_increment)
120
+ # @param options (see #assert_statsd_increment)
121
+ # @yield (see #assert_statsd_increment)
122
+ # @return [void]
123
+ # @raise (see #assert_statsd_increment)
124
+ def assert_statsd_distribution(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
125
+ expectation = StatsD::Instrument::Expectation.distribution(metric_name, value, **options)
126
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
177
127
  end
178
128
 
179
- filtered_datagrams.each do |datagram|
180
- next unless expectation.matches(datagram)
129
+ # Asserts that a given set metric occurred inside the provided block.
130
+ #
131
+ # @param metric_name (see #assert_statsd_increment)
132
+ # @param options (see #assert_statsd_increment)
133
+ # @yield (see #assert_statsd_increment)
134
+ # @return [void]
135
+ # @raise (see #assert_statsd_increment)
136
+ def assert_statsd_set(metric_name, value = nil, datagrams: nil, client: nil, **options, &block)
137
+ expectation = StatsD::Instrument::Expectation.set(metric_name, value, **options)
138
+ assert_statsd_expectation(expectation, datagrams: datagrams, client: client, &block)
139
+ end
181
140
 
182
- if expectation_times_remaining == 0
183
- flunk("Unexpected StatsD call; number of times this metric " \
184
- "was expected exceeded: #{expectation.inspect}")
141
+ # Asserts that the set of provided metric expectations came true.
142
+ #
143
+ # Generally, it's recommended to use more specific assertion methods, like
144
+ # {#assert_statsd_increment} and others.
145
+ #
146
+ # @private
147
+ # @param [Array<StatsD::Instrument::Expectation>] expectations The set of
148
+ # expectations to verify.
149
+ # @yield (see #assert_statsd_increment)
150
+ # @return [void]
151
+ # @raise (see #assert_statsd_increment)
152
+ def assert_statsd_expectations(expectations, datagrams: nil, client: nil, &block)
153
+ if datagrams.nil?
154
+ raise LocalJumpError, "assert_statsd_expectations requires a block" unless block_given?
155
+ datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
185
156
  end
186
157
 
187
- expectation_times_remaining -= 1
188
- datagrams.delete(datagram)
189
- if expectation_times_remaining == 0
190
- matched_expectations << expectation
158
+ expectations = Array(expectations)
159
+ matched_expectations = []
160
+ expectations.each do |expectation|
161
+ expectation_times = expectation.times
162
+ expectation_times_remaining = expectation.times
163
+ filtered_datagrams = datagrams.select { |m| m.type == expectation.type && m.name == expectation.name }
164
+
165
+ if filtered_datagrams.empty?
166
+ flunk("No StatsD calls for metric #{expectation.name} of type #{expectation.type} were made.")
167
+ end
168
+
169
+ filtered_datagrams.each do |datagram|
170
+ next unless expectation.matches(datagram)
171
+
172
+ if expectation_times_remaining == 0
173
+ flunk("Unexpected StatsD call; number of times this metric " \
174
+ "was expected exceeded: #{expectation.inspect}")
175
+ end
176
+
177
+ expectation_times_remaining -= 1
178
+ datagrams.delete(datagram)
179
+ if expectation_times_remaining == 0
180
+ matched_expectations << expectation
181
+ end
182
+ end
183
+
184
+ next if expectation_times_remaining == 0
185
+
186
+ msg = +"Metric expected #{expectation_times} times but seen " \
187
+ "#{expectation_times - expectation_times_remaining} " \
188
+ "times: #{expectation.inspect}."
189
+ msg << "\nCaptured metrics with the same key: #{filtered_datagrams}" if filtered_datagrams.any?
190
+ flunk(msg)
191
191
  end
192
- end
193
-
194
- next if expectation_times_remaining == 0
192
+ expectations -= matched_expectations
195
193
 
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
203
-
204
- unless expectations.empty?
205
- flunk("Unexpected StatsD calls; the following metric expectations " \
206
- "were not satisfied: #{expectations.inspect}")
207
- end
194
+ unless expectations.empty?
195
+ flunk("Unexpected StatsD calls; the following metric expectations " \
196
+ "were not satisfied: #{expectations.inspect}")
197
+ end
208
198
 
209
- pass
210
- end
199
+ pass
200
+ end
211
201
 
212
- # For backwards compatibility
213
- alias_method :assert_statsd_calls, :assert_statsd_expectations
214
- alias_method :assert_statsd_expectation, :assert_statsd_expectations
202
+ # For backwards compatibility
203
+ alias_method :assert_statsd_calls, :assert_statsd_expectations
204
+ alias_method :assert_statsd_expectation, :assert_statsd_expectations
215
205
 
216
- private
206
+ private
217
207
 
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.
208
+ def capture_statsd_datagrams_with_exception_handling(client:, &block)
209
+ capture_statsd_datagrams(client: client, &block)
210
+ rescue => exception
211
+ flunk(<<~MESSAGE)
212
+ An exception occurred in the block provided to the StatsD assertion.
223
213
 
224
- #{exception.class.name}: #{exception.message}
225
- \t#{exception.backtrace.join("\n\t")}
214
+ #{exception.class.name}: #{exception.message}
215
+ \t#{(exception.backtrace || []).join("\n\t")}
226
216
 
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
217
+ If this exception is expected, make sure to handle it using `assert_raises`
218
+ inside the block provided to the StatsD assertion.
219
+ MESSAGE
220
+ end
221
+ end
230
222
  end
231
223
  end
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StatsD
4
+ module Instrument
5
+ # @note This class is part of the new Client implementation that is intended
6
+ # to become the new default in the next major release of this library.
7
+ class BatchedUDPSink
8
+ DEFAULT_FLUSH_INTERVAL = 1.0
9
+ MAX_PACKET_SIZE = 508
10
+
11
+ def self.for_addr(addr, flush_interval: DEFAULT_FLUSH_INTERVAL)
12
+ host, port_as_string = addr.split(":", 2)
13
+ new(host, Integer(port_as_string), flush_interval: flush_interval)
14
+ end
15
+
16
+ attr_reader :host, :port
17
+
18
+ class << self
19
+ def finalize(dispatcher)
20
+ proc { dispatcher.shutdown }
21
+ end
22
+ end
23
+
24
+ def initialize(host, port, flush_interval: DEFAULT_FLUSH_INTERVAL)
25
+ @host = host
26
+ @port = port
27
+ @dispatcher = Dispatcher.new(host, port, flush_interval)
28
+ ObjectSpace.define_finalizer(self, self.class.finalize(@dispatcher))
29
+ end
30
+
31
+ def sample?(sample_rate)
32
+ sample_rate == 1 || rand < sample_rate
33
+ end
34
+
35
+ def <<(datagram)
36
+ @dispatcher << datagram
37
+ self
38
+ end
39
+
40
+ class Dispatcher
41
+ BUFFER_CLASS = if !::Object.const_defined?(:RUBY_ENGINE) || RUBY_ENGINE == "ruby"
42
+ ::Array
43
+ else
44
+ begin
45
+ gem("concurrent-ruby")
46
+ rescue Gem::MissingSpecError
47
+ raise Gem::MissingSpecError, "statsd-instrument depends on `concurrent-ruby` on #{RUBY_ENGINE}"
48
+ end
49
+ require "concurrent/array"
50
+ Concurrent::Array
51
+ end
52
+
53
+ def initialize(host, port, flush_interval)
54
+ @host = host
55
+ @port = port
56
+ @interrupted = false
57
+ @flush_interval = flush_interval
58
+ @buffer = BUFFER_CLASS.new
59
+ @dispatcher_thread = Thread.new { dispatch }
60
+ end
61
+
62
+ def <<(datagram)
63
+ unless @dispatcher_thread&.alive?
64
+ # If the dispatcher thread is dead, we assume it is because
65
+ # the process was forked. So to avoid ending datagrams twice
66
+ # we clear the buffer.
67
+ @buffer.clear
68
+ @dispatcher_thread = Thread.new { dispatch }
69
+ end
70
+ @buffer << datagram
71
+ self
72
+ end
73
+
74
+ def shutdown
75
+ @interrupted = true
76
+ end
77
+
78
+ private
79
+
80
+ NEWLINE = "\n".b.freeze
81
+ def flush
82
+ return if @buffer.empty?
83
+
84
+ datagrams = @buffer.shift(@buffer.size)
85
+
86
+ until datagrams.empty?
87
+ packet = String.new(datagrams.pop, encoding: Encoding::BINARY, capacity: MAX_PACKET_SIZE)
88
+
89
+ until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 > MAX_PACKET_SIZE
90
+ packet << NEWLINE << datagrams.shift
91
+ end
92
+
93
+ send_packet(packet)
94
+ end
95
+ end
96
+
97
+ def dispatch
98
+ until @interrupted
99
+ begin
100
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
101
+ flush
102
+ next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
103
+
104
+ sleep(next_sleep_duration) if next_sleep_duration > 0
105
+ rescue => error
106
+ report_error(error)
107
+ end
108
+ end
109
+
110
+ flush
111
+ invalidate_socket
112
+ end
113
+
114
+ def report_error(error)
115
+ StatsD.logger.error do
116
+ "[#{self.class.name}] The dispatcher thread encountered an error #{error.class}: #{error.message}"
117
+ end
118
+ end
119
+
120
+ def send_packet(packet)
121
+ retried = false
122
+ socket.send(packet, 0)
123
+ rescue SocketError, IOError, SystemCallError => error
124
+ StatsD.logger.debug do
125
+ "[#{self.class.name}] Resetting connection because of #{error.class}: #{error.message}"
126
+ end
127
+ invalidate_socket
128
+ if retried
129
+ StatsD.logger.warning do
130
+ "[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
131
+ end
132
+ else
133
+ retried = true
134
+ retry
135
+ end
136
+ end
137
+
138
+ def socket
139
+ @socket ||= begin
140
+ socket = UDPSocket.new
141
+ socket.connect(@host, @port)
142
+ socket
143
+ end
144
+ end
145
+
146
+ def invalidate_socket
147
+ @socket&.close
148
+ ensure
149
+ @socket = nil
150
+ end
151
+ end
152
+ end
153
+ end
154
+ end