statsd-instrument 3.1.2 → 3.3.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/benchmark.yml +4 -6
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/lint.yml +5 -7
- data/.github/workflows/tests.yml +7 -9
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +17 -1
- data/Gemfile +1 -1
- data/README.md +9 -1
- data/benchmark/send-metrics-to-local-udp-receiver +38 -11
- data/lib/statsd/instrument/assertions.rb +2 -0
- data/lib/statsd/instrument/batched_udp_sink.rb +133 -29
- data/lib/statsd/instrument/client.rb +6 -0
- data/lib/statsd/instrument/datagram_builder.rb +3 -0
- data/lib/statsd/instrument/environment.rb +15 -2
- data/lib/statsd/instrument/expectation.rb +4 -0
- data/lib/statsd/instrument/helpers.rb +16 -0
- data/lib/statsd/instrument/matchers.rb +1 -1
- data/lib/statsd/instrument/rubocop/positional_arguments.rb +1 -1
- data/lib/statsd/instrument/rubocop.rb +1 -0
- data/lib/statsd/instrument/statsd_datagram_builder.rb +1 -0
- data/lib/statsd/instrument/strict.rb +4 -0
- data/lib/statsd/instrument/udp_sink.rb +12 -9
- data/lib/statsd/instrument/version.rb +1 -1
- data/lib/statsd/instrument.rb +8 -3
- data/test/helpers_test.rb +40 -1
- data/test/statsd_instrumentation_test.rb +32 -0
- data/test/udp_sink_test.rb +87 -30
- metadata +4 -4
- data/.github/probots.yml +0 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 62f90038a90bccc54c0fe5ac2fe1a2449e8662183e9b9a0cac209e5ae22a07c7
|
4
|
+
data.tar.gz: e620363a10bff05710ce52f6869364e378a0979e2ef53cc86c56f8c7fe491d91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 18836250885562c7862db1e515c8f8433e43cf795b886800e8a40fae7e7ebead2a120656e6f7654d5c3b87c9f364d3861441593f42dfda7fa69479146b800842
|
7
|
+
data.tar.gz: 6915ee31b5bab72a8d52ef588f6fa90e1df4e821e1aaa96cf7523b1392d49bdc56003de4e3f09e818aee21e2d094e9671b2b2297e7b3f597ca193dafbf03824a
|
@@ -5,18 +5,16 @@ on: push
|
|
5
5
|
jobs:
|
6
6
|
test:
|
7
7
|
name: Send metric over UDP
|
8
|
-
runs-on: ubuntu-
|
8
|
+
runs-on: ubuntu-latest
|
9
9
|
|
10
10
|
steps:
|
11
11
|
- uses: actions/checkout@v1
|
12
12
|
|
13
|
-
- name:
|
14
|
-
uses:
|
13
|
+
- name: Set up Ruby
|
14
|
+
uses: ruby/setup-ruby@v1
|
15
15
|
with:
|
16
16
|
ruby-version: 2.6
|
17
|
-
|
18
|
-
- name: Install dependencies
|
19
|
-
run: gem install bundler && bundle install --jobs 4 --retry 3
|
17
|
+
bundler-cache: true
|
20
18
|
|
21
19
|
- name: Run benchmark on branch
|
22
20
|
run: benchmark/send-metrics-to-local-udp-receiver
|
@@ -0,0 +1,22 @@
|
|
1
|
+
name: Contributor License Agreement (CLA)
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request_target:
|
5
|
+
types: [opened, synchronize]
|
6
|
+
issue_comment:
|
7
|
+
types: [created]
|
8
|
+
|
9
|
+
jobs:
|
10
|
+
cla:
|
11
|
+
runs-on: ubuntu-latest
|
12
|
+
if: |
|
13
|
+
(github.event.issue.pull_request
|
14
|
+
&& !github.event.issue.pull_request.merged_at
|
15
|
+
&& contains(github.event.comment.body, 'signed')
|
16
|
+
)
|
17
|
+
|| (github.event.pull_request && !github.event.pull_request.merged)
|
18
|
+
steps:
|
19
|
+
- uses: Shopify/shopify-cla-action@v1
|
20
|
+
with:
|
21
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
22
|
+
cla-token: ${{ secrets.CLA_TOKEN }}
|
data/.github/workflows/lint.yml
CHANGED
@@ -5,18 +5,16 @@ on: push
|
|
5
5
|
jobs:
|
6
6
|
test:
|
7
7
|
name: Rubocop
|
8
|
-
runs-on: ubuntu-
|
8
|
+
runs-on: ubuntu-latest
|
9
9
|
|
10
10
|
steps:
|
11
11
|
- uses: actions/checkout@v1
|
12
12
|
|
13
|
-
- name:
|
14
|
-
uses:
|
13
|
+
- name: Set up Ruby
|
14
|
+
uses: ruby/setup-ruby@v1
|
15
15
|
with:
|
16
|
-
ruby-version: 2.
|
17
|
-
|
18
|
-
- name: Install dependencies
|
19
|
-
run: gem install bundler && bundle install --jobs 4 --retry 3
|
16
|
+
ruby-version: 2.6
|
17
|
+
bundler-cache: true
|
20
18
|
|
21
19
|
- name: Run Rubocop
|
22
20
|
run: bin/rubocop
|
data/.github/workflows/tests.yml
CHANGED
@@ -4,12 +4,12 @@ on: push
|
|
4
4
|
|
5
5
|
jobs:
|
6
6
|
test:
|
7
|
-
name: Ruby ${{ matrix.ruby }} on ubuntu-
|
8
|
-
runs-on: ubuntu-
|
7
|
+
name: Ruby ${{ matrix.ruby }} on ubuntu-latest
|
8
|
+
runs-on: ubuntu-latest
|
9
9
|
strategy:
|
10
10
|
fail-fast: false
|
11
11
|
matrix:
|
12
|
-
ruby: ['2.6', '2.7', '3.0']
|
12
|
+
ruby: ['2.6', '2.7', '3.0', '3.1']
|
13
13
|
|
14
14
|
# Windows on macOS builds started failing, so they are disabled for noew
|
15
15
|
# platform: [windows-2019, macOS-10.14, ubuntu-18.04]
|
@@ -19,13 +19,11 @@ jobs:
|
|
19
19
|
steps:
|
20
20
|
- uses: actions/checkout@v1
|
21
21
|
|
22
|
-
- name:
|
23
|
-
uses:
|
22
|
+
- name: Set up Ruby
|
23
|
+
uses: ruby/setup-ruby@v1
|
24
24
|
with:
|
25
25
|
ruby-version: ${{ matrix.ruby }}
|
26
|
-
|
27
|
-
- name: Install dependencies
|
28
|
-
run: gem install bundler && bundle install --jobs 4 --retry 3
|
26
|
+
bundler-cache: true
|
29
27
|
|
30
28
|
- name: Run test suite
|
31
|
-
run: rake test
|
29
|
+
run: bundle exec rake test
|
data/.rubocop.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -6,7 +6,23 @@ section below.
|
|
6
6
|
|
7
7
|
### Unreleased changes
|
8
8
|
|
9
|
-
|
9
|
+
## Version 3.3.0
|
10
|
+
|
11
|
+
- UDP Batching now has a max queue size and emitter threads will block if the queue
|
12
|
+
reaches the limit. This is to prevent the queue from growing unbounded.
|
13
|
+
More generally the UDP batching mode was optimized to improve throughput and to
|
14
|
+
flush the queue more eagerly (#309).
|
15
|
+
- Added `STATSD_BUFFER_CAPACITY` configuration.
|
16
|
+
- Added `STATSD_MAX_PACKET_SIZE` configuration.
|
17
|
+
- Require `set` explicitly, to avoid breaking tests for users of this library (#311)
|
18
|
+
|
19
|
+
## Version 3.2.1
|
20
|
+
|
21
|
+
- Fix a bug in UDP batching that could cause the Ruby process to be stuck on exit (#291).
|
22
|
+
|
23
|
+
## Version 3.2.0
|
24
|
+
|
25
|
+
- Add `tag_error_class` option to `statsd_count_success` which tags the class of a thrown error
|
10
26
|
|
11
27
|
## Version 3.1.2
|
12
28
|
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -27,7 +27,7 @@ The following environment variables are supported:
|
|
27
27
|
explicitly, this will be determined based on other environment variables,
|
28
28
|
like `RAILS_ENV` or `ENV`. The library will behave differently:
|
29
29
|
|
30
|
-
- In the **production** and **staging** environment,
|
30
|
+
- In the **production** and **staging** environment, the library will
|
31
31
|
actually send UDP packets.
|
32
32
|
- In the **test** environment, it will swallow all calls, but allows you to
|
33
33
|
capture them for testing purposes. See below for notes on writing tests.
|
@@ -45,6 +45,14 @@ The following environment variables are supported:
|
|
45
45
|
- `STATSD_FLUSH_INTERVAL`: (default: `1.0`) The interval in seconds at which
|
46
46
|
events are sent in batch. Only applicable to the UDP configuration. If set
|
47
47
|
to `0.0`, metrics are sent immediately.
|
48
|
+
- `STATSD_BUFFER_CAPACITY`: (default: `5000`) The maximum amount of events that
|
49
|
+
may be buffered before emitting threads will start to block. Increasing this
|
50
|
+
value may help for application generating spikes of events. However if the
|
51
|
+
application emit events faster than they can be sent, increasing it won't help.
|
52
|
+
- `STATSD_MAX_PACKET_SIZE`: (default: `1472`) The maximum size of UDP packets.
|
53
|
+
If your network is properly configured to handle larger packets you may try
|
54
|
+
to increase this value for better performance, but most network can't handle
|
55
|
+
larger packets.
|
48
56
|
|
49
57
|
## StatsD keys
|
50
58
|
|
@@ -16,28 +16,47 @@ else
|
|
16
16
|
end
|
17
17
|
|
18
18
|
intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}"
|
19
|
+
log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}.log"
|
19
20
|
FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
|
20
21
|
|
21
22
|
# Set up an UDP listener to which we can send StatsD packets
|
22
23
|
receiver = UDPSocket.new
|
23
24
|
receiver.bind("localhost", 0)
|
24
25
|
|
25
|
-
|
26
|
+
log_file = File.open(log_filename, "w+", level: Logger::WARN)
|
27
|
+
StatsD.logger = Logger.new(log_file)
|
28
|
+
|
29
|
+
udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
|
26
30
|
"STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
|
27
31
|
"STATSD_IMPLEMENTATION" => "dogstatsd",
|
28
32
|
"STATSD_ENV" => "production",
|
29
|
-
|
33
|
+
"STATSD_FLUSH_INTERVAL" => "0",
|
34
|
+
)).client
|
35
|
+
|
36
|
+
batched_udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
|
37
|
+
"STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
|
38
|
+
"STATSD_IMPLEMENTATION" => "dogstatsd",
|
39
|
+
"STATSD_ENV" => "production",
|
40
|
+
)).client
|
41
|
+
|
42
|
+
def send_metrics(client)
|
43
|
+
client.increment("StatsD.increment", 10)
|
44
|
+
client.measure("StatsD.measure") { 1 + 1 }
|
45
|
+
client.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
|
46
|
+
client.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
|
47
|
+
if client.datagram_builder_class == StatsD::Instrument::DogStatsDDatagramBuilder
|
48
|
+
client.event("StasD.event", "12345")
|
49
|
+
client.service_check("StatsD.service_check", "ok")
|
50
|
+
end
|
51
|
+
end
|
30
52
|
|
31
53
|
report = Benchmark.ips do |bench|
|
32
|
-
bench.report("
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
StatsD.event("StasD.event", "12345")
|
39
|
-
StatsD.service_check("StatsD.service_check", "ok")
|
40
|
-
end
|
54
|
+
bench.report("local UDP sync (branch: #{branch}, sha: #{revision[0, 7]})") do
|
55
|
+
send_metrics(udp_client)
|
56
|
+
end
|
57
|
+
|
58
|
+
bench.report("local UDP batched (branch: #{branch}, sha: #{revision[0, 7]})") do
|
59
|
+
send_metrics(batched_udp_client)
|
41
60
|
end
|
42
61
|
|
43
62
|
# Store the results in between runs
|
@@ -57,3 +76,11 @@ elsif ENV["KEEP_RESULTS"]
|
|
57
76
|
else
|
58
77
|
File.unlink(intermediate_results_filename)
|
59
78
|
end
|
79
|
+
|
80
|
+
log_file.close
|
81
|
+
logs = File.read(log_filename)
|
82
|
+
unless logs.empty?
|
83
|
+
puts
|
84
|
+
puts "==== logs ===="
|
85
|
+
puts logs
|
86
|
+
end
|
@@ -57,6 +57,7 @@ module StatsD
|
|
57
57
|
def assert_no_statsd_calls(*metric_names, datagrams: nil, client: nil, &block)
|
58
58
|
if datagrams.nil?
|
59
59
|
raise LocalJumpError, "assert_no_statsd_calls requires a block" unless block_given?
|
60
|
+
|
60
61
|
datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
|
61
62
|
end
|
62
63
|
|
@@ -152,6 +153,7 @@ module StatsD
|
|
152
153
|
def assert_statsd_expectations(expectations, datagrams: nil, client: nil, &block)
|
153
154
|
if datagrams.nil?
|
154
155
|
raise LocalJumpError, "assert_statsd_expectations requires a block" unless block_given?
|
156
|
+
|
155
157
|
datagrams = capture_statsd_datagrams_with_exception_handling(client: client, &block)
|
156
158
|
end
|
157
159
|
|
@@ -6,11 +6,15 @@ module StatsD
|
|
6
6
|
# to become the new default in the next major release of this library.
|
7
7
|
class BatchedUDPSink
|
8
8
|
DEFAULT_FLUSH_INTERVAL = 1.0
|
9
|
-
|
9
|
+
DEFAULT_THREAD_PRIORITY = 100
|
10
|
+
DEFAULT_FLUSH_THRESHOLD = 50
|
11
|
+
DEFAULT_BUFFER_CAPACITY = 5_000
|
12
|
+
# https://docs.datadoghq.com/developers/dogstatsd/high_throughput/?code-lang=ruby#ensure-proper-packet-sizes
|
13
|
+
DEFAULT_MAX_PACKET_SIZE = 1472
|
10
14
|
|
11
|
-
def self.for_addr(addr,
|
15
|
+
def self.for_addr(addr, **kwargs)
|
12
16
|
host, port_as_string = addr.split(":", 2)
|
13
|
-
new(host, Integer(port_as_string),
|
17
|
+
new(host, Integer(port_as_string), **kwargs)
|
14
18
|
end
|
15
19
|
|
16
20
|
attr_reader :host, :port
|
@@ -21,10 +25,26 @@ module StatsD
|
|
21
25
|
end
|
22
26
|
end
|
23
27
|
|
24
|
-
def initialize(
|
28
|
+
def initialize(
|
29
|
+
host,
|
30
|
+
port,
|
31
|
+
flush_interval: DEFAULT_FLUSH_INTERVAL,
|
32
|
+
thread_priority: DEFAULT_THREAD_PRIORITY,
|
33
|
+
flush_threshold: DEFAULT_FLUSH_THRESHOLD,
|
34
|
+
buffer_capacity: DEFAULT_BUFFER_CAPACITY,
|
35
|
+
max_packet_size: DEFAULT_MAX_PACKET_SIZE
|
36
|
+
)
|
25
37
|
@host = host
|
26
38
|
@port = port
|
27
|
-
@dispatcher = Dispatcher.new(
|
39
|
+
@dispatcher = Dispatcher.new(
|
40
|
+
host,
|
41
|
+
port,
|
42
|
+
flush_interval,
|
43
|
+
flush_threshold,
|
44
|
+
buffer_capacity,
|
45
|
+
thread_priority,
|
46
|
+
max_packet_size,
|
47
|
+
)
|
28
48
|
ObjectSpace.define_finalizer(self, self.class.finalize(@dispatcher))
|
29
49
|
end
|
30
50
|
|
@@ -37,6 +57,10 @@ module StatsD
|
|
37
57
|
self
|
38
58
|
end
|
39
59
|
|
60
|
+
def shutdown(*args)
|
61
|
+
@dispatcher.shutdown(*args)
|
62
|
+
end
|
63
|
+
|
40
64
|
class Dispatcher
|
41
65
|
BUFFER_CLASS = if !::Object.const_defined?(:RUBY_ENGINE) || RUBY_ENGINE == "ruby"
|
42
66
|
::Array
|
@@ -50,24 +74,54 @@ module StatsD
|
|
50
74
|
Concurrent::Array
|
51
75
|
end
|
52
76
|
|
53
|
-
def initialize(host, port, flush_interval)
|
77
|
+
def initialize(host, port, flush_interval, flush_threshold, buffer_capacity, thread_priority, max_packet_size)
|
54
78
|
@host = host
|
55
79
|
@port = port
|
56
80
|
@interrupted = false
|
57
81
|
@flush_interval = flush_interval
|
82
|
+
@flush_threshold = flush_threshold
|
83
|
+
@buffer_capacity = buffer_capacity
|
84
|
+
@thread_priority = thread_priority
|
85
|
+
@max_packet_size = max_packet_size
|
58
86
|
@buffer = BUFFER_CLASS.new
|
59
87
|
@dispatcher_thread = Thread.new { dispatch }
|
88
|
+
@pid = Process.pid
|
89
|
+
@monitor = Monitor.new
|
90
|
+
@condition = @monitor.new_cond
|
60
91
|
end
|
61
92
|
|
62
93
|
def <<(datagram)
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
#
|
67
|
-
|
68
|
-
@
|
94
|
+
if thread_healthcheck
|
95
|
+
@buffer << datagram
|
96
|
+
|
97
|
+
# To avoid sending too many signals when the thread is already flushing
|
98
|
+
# We only signal when the queue size is a multiple of `flush_threshold`
|
99
|
+
if @buffer.size % @flush_threshold == 0
|
100
|
+
wakeup_thread
|
101
|
+
end
|
102
|
+
|
103
|
+
# A SizedQueue would be perfect, except that it doesn't have a timeout
|
104
|
+
# Ref: https://bugs.ruby-lang.org/issues/18774
|
105
|
+
if @buffer.size >= @buffer_capacity
|
106
|
+
StatsD.logger.warn do
|
107
|
+
"[#{self.class.name}] Max buffer size reached (#{@buffer_capacity}), pausing " \
|
108
|
+
"thread##{Thread.current.object_id}"
|
109
|
+
end
|
110
|
+
before = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond)
|
111
|
+
@monitor.synchronize do
|
112
|
+
while @buffer.size >= @buffer_capacity && @dispatcher_thread.alive?
|
113
|
+
@condition.wait(0.01)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_millisecond) - before
|
117
|
+
StatsD.logger.warn do
|
118
|
+
"[#{self.class.name}] thread##{Thread.current.object_id} resumed after #{duration.round(2)}ms"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
else
|
122
|
+
flush
|
69
123
|
end
|
70
|
-
|
124
|
+
|
71
125
|
self
|
72
126
|
end
|
73
127
|
|
@@ -82,6 +136,24 @@ module StatsD
|
|
82
136
|
|
83
137
|
private
|
84
138
|
|
139
|
+
def wakeup_thread
|
140
|
+
begin
|
141
|
+
@monitor.synchronize do
|
142
|
+
@condition.signal
|
143
|
+
end
|
144
|
+
rescue ThreadError
|
145
|
+
# Can't synchronize from trap context
|
146
|
+
Thread.new { wakeup_thread }.join
|
147
|
+
return
|
148
|
+
end
|
149
|
+
|
150
|
+
begin
|
151
|
+
@dispatcher_thread&.run
|
152
|
+
rescue ThreadError # Somehow the thread just died
|
153
|
+
thread_healthcheck
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
85
157
|
NEWLINE = "\n".b.freeze
|
86
158
|
def flush
|
87
159
|
return if @buffer.empty?
|
@@ -89,24 +161,54 @@ module StatsD
|
|
89
161
|
datagrams = @buffer.shift(@buffer.size)
|
90
162
|
|
91
163
|
until datagrams.empty?
|
92
|
-
packet = String.new(datagrams.
|
164
|
+
packet = String.new(datagrams.shift, encoding: Encoding::BINARY, capacity: @max_packet_size)
|
93
165
|
|
94
|
-
until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 >
|
166
|
+
until datagrams.empty? || packet.bytesize + datagrams.first.bytesize + 1 > @max_packet_size
|
95
167
|
packet << NEWLINE << datagrams.shift
|
96
168
|
end
|
97
|
-
|
98
169
|
send_packet(packet)
|
99
170
|
end
|
100
171
|
end
|
101
172
|
|
173
|
+
def thread_healthcheck
|
174
|
+
# TODO: We have a race condition on JRuby / Truffle here. It could cause multiple
|
175
|
+
# dispatcher threads to be spawned, which would cause problems.
|
176
|
+
# However we can't simply lock here as we might be called from a trap context.
|
177
|
+
unless @dispatcher_thread&.alive?
|
178
|
+
# If the main the main thread is dead the VM is shutting down so we won't be able
|
179
|
+
# to spawn a new thread, so we fallback to sending our datagram directly.
|
180
|
+
return false unless Thread.main.alive?
|
181
|
+
|
182
|
+
# If the dispatcher thread is dead, it might be because the process was forked.
|
183
|
+
# So to avoid sending datagrams twice we clear the buffer.
|
184
|
+
if @pid != Process.pid
|
185
|
+
StatsD.logger.info { "[#{self.class.name}] Restarting the dispatcher thread after fork" }
|
186
|
+
@pid = Process.pid
|
187
|
+
@buffer.clear
|
188
|
+
else
|
189
|
+
StatsD.logger.info { "[#{self.class.name}] Restarting the dispatcher thread" }
|
190
|
+
end
|
191
|
+
@dispatcher_thread = Thread.new { dispatch }.tap { |t| t.priority = @thread_priority }
|
192
|
+
end
|
193
|
+
true
|
194
|
+
end
|
195
|
+
|
102
196
|
def dispatch
|
103
197
|
until @interrupted
|
104
198
|
begin
|
105
199
|
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
106
200
|
flush
|
201
|
+
|
202
|
+
# Other threads may have queued more events while we were doing IO
|
203
|
+
flush while @buffer.size > @flush_threshold
|
204
|
+
|
107
205
|
next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
|
108
206
|
|
109
|
-
|
207
|
+
if next_sleep_duration > 0
|
208
|
+
@monitor.synchronize do
|
209
|
+
@condition.wait(next_sleep_duration)
|
210
|
+
end
|
211
|
+
end
|
110
212
|
rescue => error
|
111
213
|
report_error(error)
|
112
214
|
end
|
@@ -124,19 +226,21 @@ module StatsD
|
|
124
226
|
|
125
227
|
def send_packet(packet)
|
126
228
|
retried = false
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
229
|
+
begin
|
230
|
+
socket.send(packet, 0)
|
231
|
+
rescue SocketError, IOError, SystemCallError => error
|
232
|
+
StatsD.logger.debug do
|
233
|
+
"[#{self.class.name}] Resetting connection because of #{error.class}: #{error.message}"
|
234
|
+
end
|
235
|
+
invalidate_socket
|
236
|
+
if retried
|
237
|
+
StatsD.logger.warn do
|
238
|
+
"[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
|
239
|
+
end
|
240
|
+
else
|
241
|
+
retried = true
|
242
|
+
retry
|
136
243
|
end
|
137
|
-
else
|
138
|
-
retried = true
|
139
|
-
retry
|
140
244
|
end
|
141
245
|
end
|
142
246
|
|
@@ -200,6 +200,7 @@ module StatsD
|
|
200
200
|
def increment(name, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
|
201
201
|
sample_rate ||= @default_sample_rate
|
202
202
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
203
|
+
|
203
204
|
emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
|
204
205
|
end
|
205
206
|
|
@@ -217,6 +218,7 @@ module StatsD
|
|
217
218
|
|
218
219
|
sample_rate ||= @default_sample_rate
|
219
220
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
221
|
+
|
220
222
|
emit(datagram_builder(no_prefix: no_prefix).ms(name, value, sample_rate, tags))
|
221
223
|
end
|
222
224
|
|
@@ -236,6 +238,7 @@ module StatsD
|
|
236
238
|
def gauge(name, value, sample_rate: nil, tags: nil, no_prefix: false)
|
237
239
|
sample_rate ||= @default_sample_rate
|
238
240
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
241
|
+
|
239
242
|
emit(datagram_builder(no_prefix: no_prefix).g(name, value, sample_rate, tags))
|
240
243
|
end
|
241
244
|
|
@@ -249,6 +252,7 @@ module StatsD
|
|
249
252
|
def set(name, value, sample_rate: nil, tags: nil, no_prefix: false)
|
250
253
|
sample_rate ||= @default_sample_rate
|
251
254
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
255
|
+
|
252
256
|
emit(datagram_builder(no_prefix: no_prefix).s(name, value, sample_rate, tags))
|
253
257
|
end
|
254
258
|
|
@@ -271,6 +275,7 @@ module StatsD
|
|
271
275
|
|
272
276
|
sample_rate ||= @default_sample_rate
|
273
277
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
278
|
+
|
274
279
|
emit(datagram_builder(no_prefix: no_prefix).d(name, value, sample_rate, tags))
|
275
280
|
end
|
276
281
|
|
@@ -288,6 +293,7 @@ module StatsD
|
|
288
293
|
def histogram(name, value, sample_rate: nil, tags: nil, no_prefix: false)
|
289
294
|
sample_rate ||= @default_sample_rate
|
290
295
|
return StatsD::Instrument::VOID unless sample?(sample_rate)
|
296
|
+
|
291
297
|
emit(datagram_builder(no_prefix: no_prefix).h(name, value, sample_rate, tags))
|
292
298
|
end
|
293
299
|
|
@@ -67,10 +67,12 @@ module StatsD
|
|
67
67
|
# @return [Array<String>, nil] the list of tags in canonical form.
|
68
68
|
def normalize_tags(tags)
|
69
69
|
return [] unless tags
|
70
|
+
|
70
71
|
tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
|
71
72
|
|
72
73
|
# Fast path when no string replacement is needed
|
73
74
|
return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
|
75
|
+
|
74
76
|
tags.map { |tag| tag.tr("|,", "") }
|
75
77
|
end
|
76
78
|
|
@@ -78,6 +80,7 @@ module StatsD
|
|
78
80
|
def normalize_name(name)
|
79
81
|
# Fast path when no normalization is needed to avoid copying the string
|
80
82
|
return name unless /[:|@]/.match?(name)
|
83
|
+
|
81
84
|
name.tr(":|@", "_")
|
82
85
|
end
|
83
86
|
|
@@ -79,7 +79,15 @@ module StatsD
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def statsd_flush_interval
|
82
|
-
Float(env.fetch("STATSD_FLUSH_INTERVAL",
|
82
|
+
Float(env.fetch("STATSD_FLUSH_INTERVAL", StatsD::Instrument::BatchedUDPSink::DEFAULT_FLUSH_INTERVAL))
|
83
|
+
end
|
84
|
+
|
85
|
+
def statsd_buffer_capacity
|
86
|
+
Float(env.fetch("STATSD_BUFFER_CAPACITY", StatsD::Instrument::BatchedUDPSink::DEFAULT_BUFFER_CAPACITY))
|
87
|
+
end
|
88
|
+
|
89
|
+
def statsd_max_packet_size
|
90
|
+
Float(env.fetch("STATSD_MAX_PACKET_SIZE", StatsD::Instrument::BatchedUDPSink::DEFAULT_MAX_PACKET_SIZE))
|
83
91
|
end
|
84
92
|
|
85
93
|
def client
|
@@ -90,7 +98,12 @@ module StatsD
|
|
90
98
|
case environment
|
91
99
|
when "production", "staging"
|
92
100
|
if statsd_flush_interval > 0.0
|
93
|
-
StatsD::Instrument::BatchedUDPSink.for_addr(
|
101
|
+
StatsD::Instrument::BatchedUDPSink.for_addr(
|
102
|
+
statsd_addr,
|
103
|
+
flush_interval: statsd_flush_interval,
|
104
|
+
buffer_capacity: statsd_buffer_capacity,
|
105
|
+
max_packet_size: statsd_max_packet_size,
|
106
|
+
)
|
94
107
|
else
|
95
108
|
StatsD::Instrument::UDPSink.for_addr(statsd_addr)
|
96
109
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
module StatsD
|
4
6
|
module Instrument
|
5
7
|
# @private
|
@@ -93,10 +95,12 @@ module StatsD
|
|
93
95
|
# to ensure that this logic matches the logic of the active datagram builder.
|
94
96
|
def normalize_tags(tags)
|
95
97
|
return [] unless tags
|
98
|
+
|
96
99
|
tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
|
97
100
|
|
98
101
|
# Fast path when no string replacement is needed
|
99
102
|
return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
|
103
|
+
|
100
104
|
tags.map { |tag| tag.tr("|,", "") }
|
101
105
|
end
|
102
106
|
end
|
@@ -10,6 +10,22 @@ module StatsD
|
|
10
10
|
|
11
11
|
# For backwards compatibility
|
12
12
|
alias_method :capture_statsd_calls, :capture_statsd_datagrams
|
13
|
+
|
14
|
+
def self.add_tag(tags, key, value)
|
15
|
+
tags = tags.dup || {}
|
16
|
+
|
17
|
+
if tags.is_a?(String)
|
18
|
+
tags = tags.empty? ? "#{key}:#{value}" : "#{tags},#{key}:#{value}"
|
19
|
+
elsif tags.is_a?(Array)
|
20
|
+
tags << "#{key}:#{value}"
|
21
|
+
elsif tags.is_a?(Hash)
|
22
|
+
tags[key] = value
|
23
|
+
else
|
24
|
+
raise ArgumentError, "add_tag only supports string, array or hash, #{tags.class} provided"
|
25
|
+
end
|
26
|
+
|
27
|
+
tags
|
28
|
+
end
|
13
29
|
end
|
14
30
|
end
|
15
31
|
end
|
@@ -49,7 +49,7 @@ module StatsD
|
|
49
49
|
raise RSpec::Expectations::ExpectationNotMetError, "No StatsD calls for metric #{metric_name} were made."
|
50
50
|
elsif options[:times] && options[:times] != metrics.length
|
51
51
|
raise RSpec::Expectations::ExpectationNotMetError, "The numbers of StatsD calls for metric " \
|
52
|
-
|
52
|
+
"#{metric_name} was unexpected. Expected #{options[:times].inspect}, got #{metrics.length}"
|
53
53
|
end
|
54
54
|
|
55
55
|
[:sample_rate, :value, :tags].each do |expectation|
|
@@ -27,6 +27,7 @@ module StatsD
|
|
27
27
|
def increment(key, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
|
28
28
|
raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
|
29
29
|
raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Integer)
|
30
|
+
|
30
31
|
check_tags_and_sample_rate(sample_rate, tags)
|
31
32
|
|
32
33
|
super
|
@@ -35,6 +36,7 @@ module StatsD
|
|
35
36
|
def gauge(key, value, sample_rate: nil, tags: nil, no_prefix: false)
|
36
37
|
raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
|
37
38
|
raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
|
39
|
+
|
38
40
|
check_tags_and_sample_rate(sample_rate, tags)
|
39
41
|
|
40
42
|
super
|
@@ -43,6 +45,7 @@ module StatsD
|
|
43
45
|
def histogram(key, value, sample_rate: nil, tags: nil, no_prefix: false)
|
44
46
|
raise ArgumentError, "StatsD.increment does not accept a block" if block_given?
|
45
47
|
raise ArgumentError, "The value argument should be an integer, got #{value.inspect}" unless value.is_a?(Numeric)
|
48
|
+
|
46
49
|
check_tags_and_sample_rate(sample_rate, tags)
|
47
50
|
|
48
51
|
super
|
@@ -50,6 +53,7 @@ module StatsD
|
|
50
53
|
|
51
54
|
def set(key, value, sample_rate: nil, tags: nil, no_prefix: false)
|
52
55
|
raise ArgumentError, "StatsD.set does not accept a block" if block_given?
|
56
|
+
|
53
57
|
check_tags_and_sample_rate(sample_rate, tags)
|
54
58
|
|
55
59
|
super
|
@@ -26,24 +26,27 @@ module StatsD
|
|
26
26
|
def <<(datagram)
|
27
27
|
with_socket { |socket| socket.send(datagram, 0) }
|
28
28
|
self
|
29
|
-
|
30
|
-
rescue ThreadError
|
31
|
-
# In cases where a TERM or KILL signal has been sent, and we send stats as
|
32
|
-
# part of a signal handler, locks cannot be acquired, so we do our best
|
33
|
-
# to try and send the datagram without a lock.
|
34
|
-
socket.send(datagram, 0) > 0
|
35
|
-
|
36
29
|
rescue SocketError, IOError, SystemCallError => error
|
37
30
|
StatsD.logger.debug do
|
38
31
|
"[StatsD::Instrument::UDPSink] Resetting connection because of #{error.class}: #{error.message}"
|
39
32
|
end
|
40
33
|
invalidate_socket
|
34
|
+
self
|
41
35
|
end
|
42
36
|
|
43
37
|
private
|
44
38
|
|
39
|
+
def synchronize(&block)
|
40
|
+
@mutex.synchronize(&block)
|
41
|
+
rescue ThreadError
|
42
|
+
# In cases where a TERM or KILL signal has been sent, and we send stats as
|
43
|
+
# part of a signal handler, locks cannot be acquired, so we do our best
|
44
|
+
# to try and send the datagram without a lock.
|
45
|
+
yield
|
46
|
+
end
|
47
|
+
|
45
48
|
def with_socket
|
46
|
-
|
49
|
+
synchronize { yield(socket) }
|
47
50
|
end
|
48
51
|
|
49
52
|
def socket
|
@@ -55,7 +58,7 @@ module StatsD
|
|
55
58
|
end
|
56
59
|
|
57
60
|
def invalidate_socket
|
58
|
-
|
61
|
+
synchronize do
|
59
62
|
@socket = nil
|
60
63
|
end
|
61
64
|
end
|
data/lib/statsd/instrument.rb
CHANGED
@@ -104,24 +104,26 @@ module StatsD
|
|
104
104
|
# @param method (see #statsd_measure)
|
105
105
|
# @param name (see #statsd_measure)
|
106
106
|
# @param metric_options (see #statsd_measure)
|
107
|
+
# @param tag_error_class add a <tt>error_class</tt> tag with the error class when an error is thrown
|
107
108
|
# @yield You can pass a block to this method if you want to define yourself what is a successful call
|
108
109
|
# based on the return value of the method.
|
109
110
|
# @yieldparam result The return value of the instrumented method.
|
110
111
|
# @yieldreturn [Boolean] Return true iff the return value is considered a success, false otherwise.
|
111
112
|
# @return [void]
|
112
113
|
# @see #statsd_count_if
|
113
|
-
def statsd_count_success(method, name, sample_rate: nil,
|
114
|
+
def statsd_count_success(method, name, sample_rate: nil,
|
115
|
+
tags: nil, no_prefix: false, client: nil, tag_error_class: false)
|
114
116
|
add_to_method(method, name, :count_success) do
|
115
117
|
define_method(method) do |*args, &block|
|
116
118
|
truthiness = result = super(*args, &block)
|
117
|
-
rescue
|
119
|
+
rescue => error
|
118
120
|
truthiness = false
|
119
121
|
raise
|
120
122
|
else
|
121
123
|
if block_given?
|
122
124
|
begin
|
123
125
|
truthiness = yield(result)
|
124
|
-
rescue
|
126
|
+
rescue => error
|
125
127
|
truthiness = false
|
126
128
|
end
|
127
129
|
end
|
@@ -130,6 +132,9 @@ module StatsD
|
|
130
132
|
client ||= StatsD.singleton_client
|
131
133
|
suffix = truthiness == false ? "failure" : "success"
|
132
134
|
key = StatsD::Instrument.generate_metric_name(name, self, *args)
|
135
|
+
|
136
|
+
tags = Helpers.add_tag(tags, :error_class, error.class.name) if tag_error_class && error
|
137
|
+
|
133
138
|
client.increment("#{key}.#{suffix}", sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
|
134
139
|
end
|
135
140
|
end
|
data/test/helpers_test.rb
CHANGED
@@ -35,8 +35,47 @@ class HelpersTest < Minitest::Test
|
|
35
35
|
StatsD.gauge("gauge", 15)
|
36
36
|
|
37
37
|
assert_equal(2, metrics.length)
|
38
|
-
|
39
38
|
ensure
|
40
39
|
StatsD.singleton_client = @old_client
|
41
40
|
end
|
41
|
+
|
42
|
+
def test_add_tag_works_for_nil
|
43
|
+
assert_equal({ key: 123 }, StatsD::Instrument::Helpers.add_tag(nil, :key, 123))
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_add_tag_works_for_hashes
|
47
|
+
assert_equal({ key: 123 }, StatsD::Instrument::Helpers.add_tag({}, :key, 123))
|
48
|
+
|
49
|
+
existing = { existing: 123 }
|
50
|
+
assert_equal({ existing: 123, new: 456 }, StatsD::Instrument::Helpers.add_tag(existing, :new, 456))
|
51
|
+
|
52
|
+
# ensure we do not modify the existing tags
|
53
|
+
assert_equal({ existing: 123 }, existing)
|
54
|
+
end
|
55
|
+
|
56
|
+
def test_add_tag_works_for_arrays
|
57
|
+
assert_equal(["key:123"], StatsD::Instrument::Helpers.add_tag([], :key, 123))
|
58
|
+
|
59
|
+
existing = ["existing:123"]
|
60
|
+
assert_equal(["existing:123", "new:456"], StatsD::Instrument::Helpers.add_tag(existing, :new, 456))
|
61
|
+
|
62
|
+
# ensure we do not modify the existing tags
|
63
|
+
assert_equal(["existing:123"], existing)
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_add_tag_works_for_strings
|
67
|
+
assert_equal("key:123", StatsD::Instrument::Helpers.add_tag("", :key, 123))
|
68
|
+
|
69
|
+
existing = "existing:123"
|
70
|
+
assert_equal("existing:123,new:456", StatsD::Instrument::Helpers.add_tag(existing, :new, 456))
|
71
|
+
|
72
|
+
# ensure we do not modify the existing tags
|
73
|
+
assert_equal("existing:123", existing)
|
74
|
+
end
|
75
|
+
|
76
|
+
def test_add_tags_raises_for_other
|
77
|
+
assert_raises(ArgumentError, "add_tag only supports string, array or hash, Integer provided") do
|
78
|
+
StatsD::Instrument::Helpers.add_tag(1, :key, 123)
|
79
|
+
end
|
80
|
+
end
|
42
81
|
end
|
@@ -159,6 +159,38 @@ class StatsDInstrumentationTest < Minitest::Test
|
|
159
159
|
ActiveMerchant::UniqueGateway.statsd_remove_count_success(:ssl_post, "ActiveMerchant.Gateway")
|
160
160
|
end
|
161
161
|
|
162
|
+
def test_statsd_count_success_tag_error_class
|
163
|
+
ActiveMerchant::Base.statsd_count_success(:ssl_post, "ActiveMerchant.Base", tag_error_class: true)
|
164
|
+
|
165
|
+
assert_statsd_increment("ActiveMerchant.Base.success", tags: nil) do
|
166
|
+
ActiveMerchant::Base.new.ssl_post(true)
|
167
|
+
end
|
168
|
+
|
169
|
+
assert_statsd_increment("ActiveMerchant.Base.failure", tags: ["error_class:RuntimeError"]) do
|
170
|
+
assert_raises(RuntimeError, "Not OK") do
|
171
|
+
ActiveMerchant::Base.new.ssl_post(false)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
ensure
|
175
|
+
ActiveMerchant::Base.statsd_remove_count_success(:ssl_post, "ActiveMerchant.Base")
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_statsd_count_success_tag_error_class_is_opt_in
|
179
|
+
ActiveMerchant::Base.statsd_count_success(:ssl_post, "ActiveMerchant.Base")
|
180
|
+
|
181
|
+
assert_statsd_increment("ActiveMerchant.Base.success", tags: nil) do
|
182
|
+
ActiveMerchant::Base.new.ssl_post(true)
|
183
|
+
end
|
184
|
+
|
185
|
+
assert_statsd_increment("ActiveMerchant.Base.failure", tags: nil) do
|
186
|
+
assert_raises(RuntimeError, "Not OK") do
|
187
|
+
ActiveMerchant::Base.new.ssl_post(false)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
ensure
|
191
|
+
ActiveMerchant::Base.statsd_remove_count_success(:ssl_post, "ActiveMerchant.Base")
|
192
|
+
end
|
193
|
+
|
162
194
|
def test_statsd_count
|
163
195
|
ActiveMerchant::Gateway.statsd_count(:ssl_post, "ActiveMerchant.Gateway.ssl_post")
|
164
196
|
|
data/test/udp_sink_test.rb
CHANGED
@@ -32,17 +32,19 @@ module UDPSinkTests
|
|
32
32
|
refute(udp_sink.sample?(0.5))
|
33
33
|
end
|
34
34
|
|
35
|
-
def
|
35
|
+
def test_concurrency
|
36
36
|
udp_sink = build_sink(@host, @port)
|
37
|
-
50.times.map { |i| Thread.new { udp_sink << "foo:#{i}|c" << "bar:#{i}|c" } }
|
37
|
+
threads = 50.times.map { |i| Thread.new { udp_sink << "foo:#{i}|c" << "bar:#{i}|c" } }
|
38
38
|
datagrams = []
|
39
39
|
|
40
40
|
while @receiver.wait_readable(2)
|
41
|
-
datagram, _source = @receiver.recvfrom(
|
41
|
+
datagram, _source = @receiver.recvfrom(4096)
|
42
42
|
datagrams += datagram.split("\n")
|
43
43
|
end
|
44
44
|
|
45
45
|
assert_equal(100, datagrams.size)
|
46
|
+
ensure
|
47
|
+
threads&.each(&:kill)
|
46
48
|
end
|
47
49
|
|
48
50
|
class SimpleFormatter < ::Logger::Formatter
|
@@ -53,31 +55,39 @@ module UDPSinkTests
|
|
53
55
|
|
54
56
|
def test_sends_datagram_in_signal_handler
|
55
57
|
udp_sink = build_sink(@host, @port)
|
56
|
-
Signal.trap("USR1")
|
57
|
-
|
58
|
-
|
59
|
-
sleep(5)
|
58
|
+
Signal.trap("USR1") do
|
59
|
+
udp_sink << "exiting:1|c"
|
60
|
+
udp_sink << "exiting:1|d"
|
60
61
|
end
|
61
62
|
|
63
|
+
Process.kill("USR1", Process.pid)
|
64
|
+
assert_equal(["exiting:1|c", "exiting:1|d"], read_datagrams(2))
|
65
|
+
ensure
|
62
66
|
Signal.trap("USR1", "DEFAULT")
|
63
|
-
|
64
|
-
Process.kill("USR1", pid)
|
65
|
-
@receiver.wait_readable(1)
|
66
|
-
assert_equal("exiting:1|c", @receiver.recvfrom_nonblock(100).first)
|
67
|
-
Process.kill("KILL", pid)
|
68
|
-
rescue NotImplementedError
|
69
|
-
pass("Fork is not implemented on #{RUBY_PLATFORM}")
|
70
67
|
end
|
71
68
|
|
72
69
|
def test_sends_datagram_before_exit
|
73
70
|
udp_sink = build_sink(@host, @port)
|
74
|
-
fork do
|
71
|
+
pid = fork do
|
75
72
|
udp_sink << "exiting:1|c"
|
76
|
-
|
73
|
+
udp_sink << "exiting:1|d"
|
77
74
|
end
|
75
|
+
Process.wait(pid)
|
76
|
+
assert_equal(["exiting:1|c", "exiting:1|d"], read_datagrams(2))
|
77
|
+
rescue NotImplementedError
|
78
|
+
pass("Fork is not implemented on #{RUBY_PLATFORM}")
|
79
|
+
end
|
78
80
|
|
79
|
-
|
80
|
-
|
81
|
+
def test_sends_datagram_in_at_exit_callback
|
82
|
+
udp_sink = build_sink(@host, @port)
|
83
|
+
pid = fork do
|
84
|
+
at_exit do
|
85
|
+
udp_sink << "exiting:1|c"
|
86
|
+
udp_sink << "exiting:1|d"
|
87
|
+
end
|
88
|
+
end
|
89
|
+
Process.wait(pid)
|
90
|
+
assert_equal(["exiting:1|c", "exiting:1|d"], read_datagrams(2))
|
81
91
|
rescue NotImplementedError
|
82
92
|
pass("Fork is not implemented on #{RUBY_PLATFORM}")
|
83
93
|
end
|
@@ -86,11 +96,11 @@ module UDPSinkTests
|
|
86
96
|
udp_sink = build_sink(@host, @port)
|
87
97
|
fork do
|
88
98
|
udp_sink << "exiting:1|c"
|
99
|
+
udp_sink << "exiting:1|d"
|
89
100
|
Process.kill("TERM", Process.pid)
|
90
101
|
end
|
91
102
|
|
92
|
-
|
93
|
-
assert_equal("exiting:1|c", @receiver.recvfrom_nonblock(100).first)
|
103
|
+
assert_equal(["exiting:1|c", "exiting:1|d"], read_datagrams(2))
|
94
104
|
rescue NotImplementedError
|
95
105
|
pass("Fork is not implemented on #{RUBY_PLATFORM}")
|
96
106
|
end
|
@@ -101,6 +111,18 @@ module UDPSinkTests
|
|
101
111
|
@sink_class.new(host, port)
|
102
112
|
end
|
103
113
|
|
114
|
+
def read_datagrams(count, timeout: 2)
|
115
|
+
datagrams = []
|
116
|
+
count.times do
|
117
|
+
if @receiver.wait_readable(timeout)
|
118
|
+
datagrams += @receiver.recvfrom_nonblock(1000).first.lines(chomp: true)
|
119
|
+
else
|
120
|
+
break
|
121
|
+
end
|
122
|
+
end
|
123
|
+
datagrams
|
124
|
+
end
|
125
|
+
|
104
126
|
class UDPSinkTest < Minitest::Test
|
105
127
|
include UDPSinkTests
|
106
128
|
|
@@ -136,7 +158,7 @@ module UDPSinkTests
|
|
136
158
|
|
137
159
|
assert_equal(
|
138
160
|
"[#{@sink_class}] Resetting connection because of " \
|
139
|
-
|
161
|
+
"Errno::EDESTADDRREQ: Destination address required\n",
|
140
162
|
logs.string,
|
141
163
|
)
|
142
164
|
ensure
|
@@ -145,7 +167,7 @@ module UDPSinkTests
|
|
145
167
|
end
|
146
168
|
end
|
147
169
|
|
148
|
-
|
170
|
+
module BatchedUDPSinkTests
|
149
171
|
include UDPSinkTests
|
150
172
|
|
151
173
|
def setup
|
@@ -154,28 +176,63 @@ module UDPSinkTests
|
|
154
176
|
@host = @receiver.addr[2]
|
155
177
|
@port = @receiver.addr[1]
|
156
178
|
@sink_class = StatsD::Instrument::BatchedUDPSink
|
179
|
+
@sinks = []
|
157
180
|
end
|
158
181
|
|
159
182
|
def teardown
|
160
183
|
@receiver.close
|
184
|
+
@sinks.each(&:shutdown)
|
161
185
|
end
|
162
186
|
|
163
|
-
|
187
|
+
private
|
188
|
+
|
189
|
+
def build_sink(host = @host, port = @port)
|
190
|
+
sink = @sink_class.new(host, port, flush_threshold: default_flush_threshold, buffer_capacity: 50)
|
191
|
+
@sinks << sink
|
192
|
+
sink
|
193
|
+
end
|
194
|
+
|
195
|
+
def default_flush_threshold
|
196
|
+
StatsD::Instrument::BatchedUDPSink::DEFAULT_FLUSH_THRESHOLD
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
class BatchedUDPSinkTest < Minitest::Test
|
201
|
+
include BatchedUDPSinkTests
|
202
|
+
|
203
|
+
def test_concurrency_buffering
|
164
204
|
udp_sink = build_sink(@host, @port)
|
165
|
-
50.times.map do |i|
|
205
|
+
threads = 50.times.map do |i|
|
166
206
|
Thread.new do
|
167
207
|
udp_sink << "foo:#{i}|c" << "bar:#{i}|c" << "baz:#{i}|c" << "plop:#{i}|c"
|
168
208
|
end
|
169
209
|
end
|
210
|
+
threads.each(&:join)
|
211
|
+
assert_equal(200, read_datagrams(10, timeout: 2).size)
|
212
|
+
ensure
|
213
|
+
threads&.each(&:kill)
|
214
|
+
end
|
215
|
+
end
|
170
216
|
|
171
|
-
|
217
|
+
class LowThresholdBatchedUDPSinkTest < Minitest::Test
|
218
|
+
include BatchedUDPSinkTests
|
172
219
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
220
|
+
def test_sends_datagram_when_termed
|
221
|
+
# When the main thread exit, the dispatcher thread is aborted
|
222
|
+
# and there's no exceptions or anything like that to rescue.
|
223
|
+
# So if the dispatcher thread poped some events from the buffer
|
224
|
+
# but didn't sent them yet, then they may be lost.
|
225
|
+
skip("Unfortunately this can't be guaranteed")
|
226
|
+
end
|
227
|
+
alias_method :test_sends_datagram_in_at_exit_callback, :test_sends_datagram_when_termed
|
228
|
+
alias_method :test_sends_datagram_before_exit, :test_sends_datagram_when_termed
|
229
|
+
|
230
|
+
private
|
177
231
|
|
178
|
-
|
232
|
+
# We run the same tests again, but this time we wake up the dispatcher
|
233
|
+
# thread on every call to make sure trap context is properly handled
|
234
|
+
def default_flush_threshold
|
235
|
+
1
|
179
236
|
end
|
180
237
|
end
|
181
238
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: statsd-instrument
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.
|
4
|
+
version: 3.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesse Storimer
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2022-07-26 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: concurrent-ruby
|
@@ -35,8 +35,8 @@ extensions: []
|
|
35
35
|
extra_rdoc_files: []
|
36
36
|
files:
|
37
37
|
- ".github/CODEOWNERS"
|
38
|
-
- ".github/probots.yml"
|
39
38
|
- ".github/workflows/benchmark.yml"
|
39
|
+
- ".github/workflows/cla.yml"
|
40
40
|
- ".github/workflows/lint.yml"
|
41
41
|
- ".github/workflows/tests.yml"
|
42
42
|
- ".gitignore"
|
@@ -134,7 +134,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
134
134
|
- !ruby/object:Gem::Version
|
135
135
|
version: '0'
|
136
136
|
requirements: []
|
137
|
-
rubygems_version: 3.
|
137
|
+
rubygems_version: 3.3.3
|
138
138
|
signing_key:
|
139
139
|
specification_version: 4
|
140
140
|
summary: A StatsD client for Ruby apps
|
data/.github/probots.yml
DELETED