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.
- checksums.yaml +4 -4
- data/.github/workflows/lint.yml +22 -0
- data/.github/workflows/tests.yml +31 -0
- data/.rubocop.yml +3 -13
- data/CHANGELOG.md +50 -0
- data/Gemfile +8 -2
- data/README.md +6 -3
- data/Rakefile +7 -7
- data/benchmark/send-metrics-to-dev-null-log +12 -11
- data/benchmark/send-metrics-to-local-udp-receiver +16 -15
- data/bin/rake +29 -0
- data/bin/rubocop +29 -0
- data/lib/statsd-instrument.rb +1 -1
- data/lib/statsd/instrument.rb +112 -145
- data/lib/statsd/instrument/assertions.rb +200 -208
- data/lib/statsd/instrument/batched_udp_sink.rb +154 -0
- data/lib/statsd/instrument/capture_sink.rb +23 -19
- data/lib/statsd/instrument/client.rb +410 -306
- data/lib/statsd/instrument/datagram.rb +69 -65
- data/lib/statsd/instrument/datagram_builder.rb +81 -77
- data/lib/statsd/instrument/dogstatsd_datagram.rb +76 -72
- data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +68 -64
- data/lib/statsd/instrument/environment.rb +88 -77
- data/lib/statsd/instrument/expectation.rb +96 -96
- data/lib/statsd/instrument/helpers.rb +11 -7
- data/lib/statsd/instrument/log_sink.rb +20 -16
- data/lib/statsd/instrument/matchers.rb +93 -74
- data/lib/statsd/instrument/null_sink.rb +12 -8
- data/lib/statsd/instrument/railtie.rb +11 -7
- data/lib/statsd/instrument/rubocop.rb +8 -8
- data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +1 -1
- data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +2 -2
- data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +1 -1
- data/lib/statsd/instrument/rubocop/metric_return_value.rb +2 -2
- data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +1 -1
- data/lib/statsd/instrument/rubocop/positional_arguments.rb +4 -4
- data/lib/statsd/instrument/rubocop/singleton_configuration.rb +1 -1
- data/lib/statsd/instrument/rubocop/splat_arguments.rb +2 -2
- data/lib/statsd/instrument/statsd_datagram_builder.rb +12 -8
- data/lib/statsd/instrument/strict.rb +1 -6
- data/lib/statsd/instrument/udp_sink.rb +49 -47
- data/lib/statsd/instrument/version.rb +1 -1
- data/statsd-instrument.gemspec +4 -8
- data/test/assertions_test.rb +205 -161
- data/test/benchmark/clock_gettime.rb +1 -1
- data/test/benchmark/default_tags.rb +9 -9
- data/test/benchmark/metrics.rb +8 -8
- data/test/benchmark/tags.rb +4 -4
- data/test/capture_sink_test.rb +14 -14
- data/test/client_test.rb +96 -96
- data/test/datagram_builder_test.rb +55 -55
- data/test/datagram_test.rb +5 -5
- data/test/dogstatsd_datagram_builder_test.rb +37 -37
- data/test/environment_test.rb +30 -21
- data/test/helpers/rubocop_helper.rb +12 -9
- data/test/helpers_test.rb +15 -15
- data/test/integration_test.rb +7 -7
- data/test/log_sink_test.rb +4 -4
- data/test/matchers_test.rb +54 -54
- data/test/null_sink_test.rb +4 -4
- data/test/rubocop/measure_as_dist_argument_test.rb +2 -2
- data/test/rubocop/metaprogramming_positional_arguments_test.rb +2 -2
- data/test/rubocop/metric_prefix_argument_test.rb +2 -2
- data/test/rubocop/metric_return_value_test.rb +6 -6
- data/test/rubocop/metric_value_keyword_argument_test.rb +3 -3
- data/test/rubocop/positional_arguments_test.rb +12 -12
- data/test/rubocop/singleton_configuration_test.rb +8 -8
- data/test/rubocop/splat_arguments_test.rb +2 -2
- data/test/statsd_datagram_builder_test.rb +6 -6
- data/test/statsd_instrumentation_test.rb +122 -122
- data/test/statsd_test.rb +69 -67
- data/test/test_helper.rb +19 -10
- data/test/udp_sink_test.rb +122 -50
- metadata +12 -92
- data/.github/workflows/ci.yml +0 -56
- 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
|
-
|
4
|
-
|
5
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# metric
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
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
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
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
|
-
|
176
|
-
|
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
|
-
|
180
|
-
|
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
|
-
|
183
|
-
|
184
|
-
|
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
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
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
|
-
|
193
|
-
|
194
|
-
next if expectation_times_remaining == 0
|
192
|
+
expectations -= matched_expectations
|
195
193
|
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
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
|
-
|
210
|
-
|
199
|
+
pass
|
200
|
+
end
|
211
201
|
|
212
|
-
|
213
|
-
|
214
|
-
|
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
|
-
|
206
|
+
private
|
217
207
|
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
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
|
-
|
225
|
-
|
214
|
+
#{exception.class.name}: #{exception.message}
|
215
|
+
\t#{(exception.backtrace || []).join("\n\t")}
|
226
216
|
|
227
|
-
|
228
|
-
|
229
|
-
|
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
|