statsd-instrument 3.3.0 → 3.4.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 +7 -1
- data/.github/workflows/tests.yml +2 -3
- data/.rubocop.yml +6 -0
- data/CHANGELOG.md +6 -0
- data/README.md +4 -3
- data/benchmark/local-udp-throughput +59 -0
- data/benchmark/send-metrics-to-local-udp-receiver +55 -58
- data/lib/statsd/instrument/batched_udp_sink.rb +54 -134
- data/lib/statsd/instrument/environment.rb +13 -6
- data/lib/statsd/instrument/udp_sink.rb +31 -24
- data/lib/statsd/instrument/version.rb +1 -1
- data/statsd-instrument.gemspec +0 -2
- data/test/environment_test.rb +9 -0
- data/test/udp_sink_test.rb +13 -51
- metadata +4 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bcbaac2cd4178c61bfcb484c45931bb387d81dfa632fbd114bba126c393beb75
|
4
|
+
data.tar.gz: d1101fbc534b6612ff76a282ab45d37aad3a0c185b3ccedf6dcf2ef78ce85bb1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4cd62e31fe1dc59ae49345e598de791ba1295a18d527d88f840de7349d31e8d325cf00543e59eddf02c3fed56a58f9020ace88ca7554de2340d193e8e62cb1d4
|
7
|
+
data.tar.gz: 98fcae4a9b924ab432d745ad2c02f4ac5ed6d0e94ad1f002e80afc0f4f741f01412dbf605151813432dc828ad57f9c1747e26a7127c32950df9ffddeb90e9523
|
@@ -13,12 +13,15 @@ jobs:
|
|
13
13
|
- name: Set up Ruby
|
14
14
|
uses: ruby/setup-ruby@v1
|
15
15
|
with:
|
16
|
-
ruby-version:
|
16
|
+
ruby-version: 3.1
|
17
17
|
bundler-cache: true
|
18
18
|
|
19
19
|
- name: Run benchmark on branch
|
20
20
|
run: benchmark/send-metrics-to-local-udp-receiver
|
21
21
|
|
22
|
+
- name: Run throughput benchmark on branch
|
23
|
+
run: benchmark/local-udp-throughput
|
24
|
+
|
22
25
|
- uses: actions/checkout@v1
|
23
26
|
with:
|
24
27
|
ref: 'master'
|
@@ -28,3 +31,6 @@ jobs:
|
|
28
31
|
|
29
32
|
- name: Run benchmark on master
|
30
33
|
run: benchmark/send-metrics-to-local-udp-receiver
|
34
|
+
|
35
|
+
- name: Run throughput benchmark on master
|
36
|
+
run: benchmark/local-udp-throughput
|
data/.github/workflows/tests.yml
CHANGED
@@ -9,9 +9,8 @@ jobs:
|
|
9
9
|
strategy:
|
10
10
|
fail-fast: false
|
11
11
|
matrix:
|
12
|
-
ruby: ['2.6', '2.7', '3.0', '3.1']
|
13
|
-
|
14
|
-
# Windows on macOS builds started failing, so they are disabled for noew
|
12
|
+
ruby: ['2.6', '2.7', '3.0', '3.1', 'ruby-head', 'jruby-9.3.7.0', 'truffleruby-22.2.0']
|
13
|
+
# Windows on macOS builds started failing, so they are disabled for now
|
15
14
|
# platform: [windows-2019, macOS-10.14, ubuntu-18.04]
|
16
15
|
# exclude:
|
17
16
|
# ...
|
data/.rubocop.yml
CHANGED
@@ -20,9 +20,15 @@ Naming/FileName:
|
|
20
20
|
Metrics/ParameterLists:
|
21
21
|
Enabled: false
|
22
22
|
|
23
|
+
Metrics/BlockNesting:
|
24
|
+
Enabled: false
|
25
|
+
|
23
26
|
Style/WhileUntilModifier:
|
24
27
|
Enabled: false
|
25
28
|
|
29
|
+
Style/IdenticalConditionalBranches:
|
30
|
+
Enabled: false
|
31
|
+
|
26
32
|
# Enable our own cops on our own repo
|
27
33
|
|
28
34
|
StatsD/MetricReturnValue:
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,12 @@ section below.
|
|
6
6
|
|
7
7
|
### Unreleased changes
|
8
8
|
|
9
|
+
- UDP Batching has been largely refactored again. The `STATSD_FLUSH_INTERVAL` environment variable
|
10
|
+
is deprecated. It still disable batching if set to `0`, but other than that is has no effect.
|
11
|
+
Setting `STATSD_BUFFER_CAPACITY` to `0` is now the recommended way to disable batching.
|
12
|
+
- The synchronous UDP sink now use one socket per thread, instead of a single socket
|
13
|
+
protected by a mutex.
|
14
|
+
|
9
15
|
## Version 3.3.0
|
10
16
|
|
11
17
|
- UDP Batching now has a max queue size and emitter threads will block if the queue
|
data/README.md
CHANGED
@@ -42,13 +42,14 @@ The following environment variables are supported:
|
|
42
42
|
overridden in a metric method call.
|
43
43
|
- `STATSD_DEFAULT_TAGS`: A comma-separated list of tags to apply to all metrics.
|
44
44
|
(Note: tags are not supported by all implementations.)
|
45
|
-
- `STATSD_FLUSH_INTERVAL`: (default: `1.0`) The interval in seconds at which
|
46
|
-
events are sent in batch. Only applicable to the UDP configuration. If set
|
47
|
-
to `0.0`, metrics are sent immediately.
|
48
45
|
- `STATSD_BUFFER_CAPACITY`: (default: `5000`) The maximum amount of events that
|
49
46
|
may be buffered before emitting threads will start to block. Increasing this
|
50
47
|
value may help for application generating spikes of events. However if the
|
51
48
|
application emit events faster than they can be sent, increasing it won't help.
|
49
|
+
If set to `0`, batching will be disabled, and events will be sent in individual
|
50
|
+
UDP packets, which is much slower.
|
51
|
+
- `STATSD_FLUSH_INTERVAL`: (default: `1`) Deprecated. Setting this to `0` is
|
52
|
+
equivalent to setting `STATSD_BUFFER_CAPACITY` to `0`.
|
52
53
|
- `STATSD_MAX_PACKET_SIZE`: (default: `1472`) The maximum size of UDP packets.
|
53
54
|
If your network is properly configured to handle larger packets you may try
|
54
55
|
to increase this value for better performance, but most network can't handle
|
@@ -0,0 +1,59 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "benchmark/ips"
|
6
|
+
require "tmpdir"
|
7
|
+
require "socket"
|
8
|
+
require "statsd-instrument"
|
9
|
+
|
10
|
+
def send_metrics(client)
|
11
|
+
client.increment("StatsD.increment", 10)
|
12
|
+
client.measure("StatsD.measure") { 1 + 1 }
|
13
|
+
client.gauge("StatsD.gauge", 12.0, tags: ["foo:bar", "quc"])
|
14
|
+
client.set("StatsD.set", "value", tags: { foo: "bar", baz: "quc" })
|
15
|
+
client.event("StasD.event", "12345")
|
16
|
+
client.service_check("StatsD.service_check", "ok")
|
17
|
+
end
|
18
|
+
|
19
|
+
THREAD_COUNT = Integer(ENV.fetch("THREAD_COUNT", 5))
|
20
|
+
EVENTS_PER_ITERATION = 6
|
21
|
+
ITERATIONS = 50_000
|
22
|
+
def benchmark_implementation(name, env = {})
|
23
|
+
intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/"
|
24
|
+
log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
|
25
|
+
FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
|
26
|
+
|
27
|
+
# Set up an UDP listener to which we can send StatsD packets
|
28
|
+
receiver = UDPSocket.new
|
29
|
+
receiver.bind("localhost", 0)
|
30
|
+
|
31
|
+
log_file = File.open(log_filename, "w+", level: Logger::WARN)
|
32
|
+
StatsD.logger = Logger.new(log_file)
|
33
|
+
|
34
|
+
udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
|
35
|
+
"STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
|
36
|
+
"STATSD_IMPLEMENTATION" => "dogstatsd",
|
37
|
+
"STATSD_ENV" => "production",
|
38
|
+
).merge(env)).client
|
39
|
+
|
40
|
+
puts "===== #{name} throughtput (#{THREAD_COUNT} threads) ====="
|
41
|
+
threads = THREAD_COUNT.times.map do
|
42
|
+
Thread.new do
|
43
|
+
count = ITERATIONS
|
44
|
+
while (count -= 1) > 0
|
45
|
+
send_metrics(udp_client)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
50
|
+
threads.each(&:join)
|
51
|
+
duration = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
52
|
+
events_sent = THREAD_COUNT * EVENTS_PER_ITERATION * ITERATIONS
|
53
|
+
puts "events: #{(events_sent / duration).round(1)}/s"
|
54
|
+
receiver.close
|
55
|
+
udp_client.shutdown if udp_client.respond_to?(:shutdown)
|
56
|
+
end
|
57
|
+
|
58
|
+
benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
|
59
|
+
benchmark_implementation("UDP batched")
|
@@ -7,38 +7,6 @@ require "tmpdir"
|
|
7
7
|
require "socket"
|
8
8
|
require "statsd-instrument"
|
9
9
|
|
10
|
-
revision = %x(git rev-parse HEAD).rstrip
|
11
|
-
base_revision = %x(git rev-parse origin/master).rstrip
|
12
|
-
branch = if revision == base_revision
|
13
|
-
"master"
|
14
|
-
else
|
15
|
-
%x(git rev-parse --abbrev-ref HEAD).rstrip
|
16
|
-
end
|
17
|
-
|
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"
|
20
|
-
FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
|
21
|
-
|
22
|
-
# Set up an UDP listener to which we can send StatsD packets
|
23
|
-
receiver = UDPSocket.new
|
24
|
-
receiver.bind("localhost", 0)
|
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(
|
30
|
-
"STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
|
31
|
-
"STATSD_IMPLEMENTATION" => "dogstatsd",
|
32
|
-
"STATSD_ENV" => "production",
|
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
10
|
def send_metrics(client)
|
43
11
|
client.increment("StatsD.increment", 10)
|
44
12
|
client.measure("StatsD.measure") { 1 + 1 }
|
@@ -50,37 +18,66 @@ def send_metrics(client)
|
|
50
18
|
end
|
51
19
|
end
|
52
20
|
|
53
|
-
|
54
|
-
|
55
|
-
|
21
|
+
def benchmark_implementation(name, env = {})
|
22
|
+
revision = %x(git rev-parse HEAD).rstrip
|
23
|
+
base_revision = %x(git rev-parse origin/master).rstrip
|
24
|
+
branch = if revision == base_revision
|
25
|
+
"master"
|
26
|
+
else
|
27
|
+
%x(git rev-parse --abbrev-ref HEAD).rstrip
|
56
28
|
end
|
57
29
|
|
58
|
-
|
59
|
-
|
30
|
+
intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}"
|
31
|
+
log_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}-#{name}.log"
|
32
|
+
FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
|
33
|
+
|
34
|
+
# Set up an UDP listener to which we can send StatsD packets
|
35
|
+
receiver = UDPSocket.new
|
36
|
+
receiver.bind("localhost", 0)
|
37
|
+
|
38
|
+
log_file = File.open(log_filename, "w+", level: Logger::WARN)
|
39
|
+
StatsD.logger = Logger.new(log_file)
|
40
|
+
|
41
|
+
udp_client = StatsD::Instrument::Environment.new(ENV.to_h.merge(
|
42
|
+
"STATSD_ADDR" => "#{receiver.addr[2]}:#{receiver.addr[1]}",
|
43
|
+
"STATSD_IMPLEMENTATION" => "dogstatsd",
|
44
|
+
"STATSD_ENV" => "production",
|
45
|
+
).merge(env)).client
|
46
|
+
|
47
|
+
puts "===== #{name} ====="
|
48
|
+
report = Benchmark.ips do |bench|
|
49
|
+
bench.report("#{name} (branch: #{branch}, sha: #{revision[0, 7]})") do
|
50
|
+
send_metrics(udp_client)
|
51
|
+
end
|
52
|
+
|
53
|
+
# Store the results in between runs
|
54
|
+
bench.save!(intermediate_results_filename)
|
55
|
+
bench.compare!
|
60
56
|
end
|
61
57
|
|
62
|
-
|
63
|
-
|
64
|
-
bench.compare!
|
65
|
-
end
|
58
|
+
receiver.close
|
59
|
+
udp_client.shutdown if udp_client.respond_to?(:shutdown)
|
66
60
|
|
67
|
-
|
61
|
+
if report.entries.length == 1
|
62
|
+
puts
|
63
|
+
puts "To compare the performance of this revision against another revision (e.g. master),"
|
64
|
+
puts "check out a different branch and run this benchmark script again."
|
65
|
+
elsif ENV["KEEP_RESULTS"]
|
66
|
+
puts
|
67
|
+
puts "The intermediate results have been stored in #{intermediate_results_filename}"
|
68
|
+
else
|
69
|
+
File.unlink(intermediate_results_filename)
|
70
|
+
end
|
68
71
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
File.unlink(intermediate_results_filename)
|
72
|
+
log_file.close
|
73
|
+
logs = File.read(log_filename)
|
74
|
+
unless logs.empty?
|
75
|
+
puts
|
76
|
+
puts "==== logs ===="
|
77
|
+
puts logs
|
78
|
+
end
|
79
|
+
puts "================"
|
78
80
|
end
|
79
81
|
|
80
|
-
|
81
|
-
|
82
|
-
unless logs.empty?
|
83
|
-
puts
|
84
|
-
puts "==== logs ===="
|
85
|
-
puts logs
|
86
|
-
end
|
82
|
+
benchmark_implementation("UDP sync", "STATSD_BUFFER_CAPACITY" => "0")
|
83
|
+
benchmark_implementation("UDP batched")
|
@@ -5,9 +5,7 @@ module StatsD
|
|
5
5
|
# @note This class is part of the new Client implementation that is intended
|
6
6
|
# to become the new default in the next major release of this library.
|
7
7
|
class BatchedUDPSink
|
8
|
-
DEFAULT_FLUSH_INTERVAL = 1.0
|
9
8
|
DEFAULT_THREAD_PRIORITY = 100
|
10
|
-
DEFAULT_FLUSH_THRESHOLD = 50
|
11
9
|
DEFAULT_BUFFER_CAPACITY = 5_000
|
12
10
|
# https://docs.datadoghq.com/developers/dogstatsd/high_throughput/?code-lang=ruby#ensure-proper-packet-sizes
|
13
11
|
DEFAULT_MAX_PACKET_SIZE = 1472
|
@@ -28,9 +26,7 @@ module StatsD
|
|
28
26
|
def initialize(
|
29
27
|
host,
|
30
28
|
port,
|
31
|
-
flush_interval: DEFAULT_FLUSH_INTERVAL,
|
32
29
|
thread_priority: DEFAULT_THREAD_PRIORITY,
|
33
|
-
flush_threshold: DEFAULT_FLUSH_THRESHOLD,
|
34
30
|
buffer_capacity: DEFAULT_BUFFER_CAPACITY,
|
35
31
|
max_packet_size: DEFAULT_MAX_PACKET_SIZE
|
36
32
|
)
|
@@ -39,8 +35,6 @@ module StatsD
|
|
39
35
|
@dispatcher = Dispatcher.new(
|
40
36
|
host,
|
41
37
|
port,
|
42
|
-
flush_interval,
|
43
|
-
flush_threshold,
|
44
38
|
buffer_capacity,
|
45
39
|
thread_priority,
|
46
40
|
max_packet_size,
|
@@ -61,112 +55,85 @@ module StatsD
|
|
61
55
|
@dispatcher.shutdown(*args)
|
62
56
|
end
|
63
57
|
|
64
|
-
class
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
58
|
+
class Buffer < SizedQueue
|
59
|
+
def push_nonblock(item)
|
60
|
+
push(item, true)
|
61
|
+
rescue ThreadError, ClosedQueueError
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def inspect
|
66
|
+
"<#{self.class.name}:#{object_id} capacity=#{max} size=#{size}>"
|
67
|
+
end
|
68
|
+
|
69
|
+
def pop_nonblock
|
70
|
+
pop(true)
|
71
|
+
rescue ThreadError
|
72
|
+
nil
|
75
73
|
end
|
74
|
+
end
|
76
75
|
|
77
|
-
|
78
|
-
|
79
|
-
@
|
76
|
+
class Dispatcher
|
77
|
+
def initialize(host, port, buffer_capacity, thread_priority, max_packet_size)
|
78
|
+
@udp_sink = UDPSink.new(host, port)
|
80
79
|
@interrupted = false
|
81
|
-
@flush_interval = flush_interval
|
82
|
-
@flush_threshold = flush_threshold
|
83
|
-
@buffer_capacity = buffer_capacity
|
84
80
|
@thread_priority = thread_priority
|
85
81
|
@max_packet_size = max_packet_size
|
86
|
-
@
|
82
|
+
@buffer_capacity = buffer_capacity
|
83
|
+
@buffer = Buffer.new(buffer_capacity)
|
87
84
|
@dispatcher_thread = Thread.new { dispatch }
|
88
85
|
@pid = Process.pid
|
89
|
-
@monitor = Monitor.new
|
90
|
-
@condition = @monitor.new_cond
|
91
86
|
end
|
92
87
|
|
93
88
|
def <<(datagram)
|
94
|
-
if thread_healthcheck
|
95
|
-
|
96
|
-
|
97
|
-
|
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
|
89
|
+
if !thread_healthcheck || !@buffer.push_nonblock(datagram)
|
90
|
+
# The buffer is full or the thread can't be respaned,
|
91
|
+
# we'll send the datagram synchronously
|
92
|
+
@udp_sink << datagram
|
123
93
|
end
|
124
94
|
|
125
95
|
self
|
126
96
|
end
|
127
97
|
|
128
|
-
def shutdown(wait =
|
98
|
+
def shutdown(wait = 2)
|
129
99
|
@interrupted = true
|
100
|
+
@buffer.close
|
130
101
|
if @dispatcher_thread&.alive?
|
131
102
|
@dispatcher_thread.join(wait)
|
132
|
-
else
|
133
|
-
flush
|
134
103
|
end
|
104
|
+
flush(blocking: false)
|
135
105
|
end
|
136
106
|
|
137
107
|
private
|
138
108
|
|
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
|
-
|
157
109
|
NEWLINE = "\n".b.freeze
|
158
|
-
def flush
|
159
|
-
return if @buffer.empty?
|
160
110
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
111
|
+
def flush(blocking:)
|
112
|
+
packet = "".b
|
113
|
+
next_datagram = nil
|
114
|
+
until @buffer.closed? && @buffer.empty? && next_datagram.nil?
|
115
|
+
if blocking
|
116
|
+
next_datagram ||= @buffer.pop
|
117
|
+
break if next_datagram.nil? # queue was closed
|
118
|
+
else
|
119
|
+
next_datagram ||= @buffer.pop_nonblock
|
120
|
+
break if next_datagram.nil? # no datagram in buffer
|
121
|
+
end
|
165
122
|
|
166
|
-
|
167
|
-
|
123
|
+
packet << next_datagram
|
124
|
+
next_datagram = nil
|
125
|
+
unless packet.bytesize > @max_packet_size
|
126
|
+
while (next_datagram = @buffer.pop_nonblock)
|
127
|
+
if @max_packet_size - packet.bytesize - 1 > next_datagram.bytesize
|
128
|
+
packet << NEWLINE << next_datagram
|
129
|
+
else
|
130
|
+
break
|
131
|
+
end
|
132
|
+
end
|
168
133
|
end
|
169
|
-
|
134
|
+
|
135
|
+
@udp_sink << packet
|
136
|
+
packet.clear
|
170
137
|
end
|
171
138
|
end
|
172
139
|
|
@@ -196,26 +163,13 @@ module StatsD
|
|
196
163
|
def dispatch
|
197
164
|
until @interrupted
|
198
165
|
begin
|
199
|
-
|
200
|
-
flush
|
201
|
-
|
202
|
-
# Other threads may have queued more events while we were doing IO
|
203
|
-
flush while @buffer.size > @flush_threshold
|
204
|
-
|
205
|
-
next_sleep_duration = @flush_interval - (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start)
|
206
|
-
|
207
|
-
if next_sleep_duration > 0
|
208
|
-
@monitor.synchronize do
|
209
|
-
@condition.wait(next_sleep_duration)
|
210
|
-
end
|
211
|
-
end
|
166
|
+
flush(blocking: true)
|
212
167
|
rescue => error
|
213
168
|
report_error(error)
|
214
169
|
end
|
215
170
|
end
|
216
171
|
|
217
|
-
flush
|
218
|
-
invalidate_socket
|
172
|
+
flush(blocking: false)
|
219
173
|
end
|
220
174
|
|
221
175
|
def report_error(error)
|
@@ -223,40 +177,6 @@ module StatsD
|
|
223
177
|
"[#{self.class.name}] The dispatcher thread encountered an error #{error.class}: #{error.message}"
|
224
178
|
end
|
225
179
|
end
|
226
|
-
|
227
|
-
def send_packet(packet)
|
228
|
-
retried = false
|
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
|
243
|
-
end
|
244
|
-
end
|
245
|
-
end
|
246
|
-
|
247
|
-
def socket
|
248
|
-
@socket ||= begin
|
249
|
-
socket = UDPSocket.new
|
250
|
-
socket.connect(@host, @port)
|
251
|
-
socket
|
252
|
-
end
|
253
|
-
end
|
254
|
-
|
255
|
-
def invalidate_socket
|
256
|
-
@socket&.close
|
257
|
-
ensure
|
258
|
-
@socket = nil
|
259
|
-
end
|
260
180
|
end
|
261
181
|
end
|
262
182
|
end
|
@@ -35,6 +35,14 @@ module StatsD
|
|
35
35
|
|
36
36
|
def initialize(env)
|
37
37
|
@env = env
|
38
|
+
if env.key?("STATSD_FLUSH_INTERVAL")
|
39
|
+
value = env["STATSD_FLUSH_INTERVAL"]
|
40
|
+
if Float(value) == 0.0
|
41
|
+
warn("STATSD_FLUSH_INTERVAL=#{value} is deprecated, please set STATSD_BUFFER_CAPACITY=0 instead.")
|
42
|
+
else
|
43
|
+
warn("STATSD_FLUSH_INTERVAL=#{value} is deprecated and has no effect, please remove it.")
|
44
|
+
end
|
45
|
+
end
|
38
46
|
end
|
39
47
|
|
40
48
|
# Detects the current environment, either by asking Rails, or by inspecting environment variables.
|
@@ -78,12 +86,12 @@ module StatsD
|
|
78
86
|
env.key?("STATSD_DEFAULT_TAGS") ? env.fetch("STATSD_DEFAULT_TAGS").split(",") : nil
|
79
87
|
end
|
80
88
|
|
81
|
-
def
|
82
|
-
|
89
|
+
def statsd_buffer_capacity
|
90
|
+
Integer(env.fetch("STATSD_BUFFER_CAPACITY", StatsD::Instrument::BatchedUDPSink::DEFAULT_BUFFER_CAPACITY))
|
83
91
|
end
|
84
92
|
|
85
|
-
def
|
86
|
-
Float(env.fetch("
|
93
|
+
def statsd_batching?
|
94
|
+
statsd_buffer_capacity > 0 && Float(env.fetch("STATSD_FLUSH_INTERVAL", 1.0)) > 0.0
|
87
95
|
end
|
88
96
|
|
89
97
|
def statsd_max_packet_size
|
@@ -97,10 +105,9 @@ module StatsD
|
|
97
105
|
def default_sink_for_environment
|
98
106
|
case environment
|
99
107
|
when "production", "staging"
|
100
|
-
if
|
108
|
+
if statsd_batching?
|
101
109
|
StatsD::Instrument::BatchedUDPSink.for_addr(
|
102
110
|
statsd_addr,
|
103
|
-
flush_interval: statsd_flush_interval,
|
104
111
|
buffer_capacity: statsd_buffer_capacity,
|
105
112
|
max_packet_size: statsd_max_packet_size,
|
106
113
|
)
|
@@ -12,11 +12,18 @@ module StatsD
|
|
12
12
|
|
13
13
|
attr_reader :host, :port
|
14
14
|
|
15
|
+
FINALIZER = ->(object_id) do
|
16
|
+
Thread.list.each do |thread|
|
17
|
+
if (store = thread["StatsD::UDPSink"])
|
18
|
+
store.delete(object_id)&.close
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
15
23
|
def initialize(host, port)
|
24
|
+
ObjectSpace.define_finalizer(self, FINALIZER)
|
16
25
|
@host = host
|
17
26
|
@port = port
|
18
|
-
@mutex = Mutex.new
|
19
|
-
@socket = nil
|
20
27
|
end
|
21
28
|
|
22
29
|
def sample?(sample_rate)
|
@@ -24,43 +31,43 @@ module StatsD
|
|
24
31
|
end
|
25
32
|
|
26
33
|
def <<(datagram)
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
34
|
+
retried = false
|
35
|
+
begin
|
36
|
+
socket.send(datagram, 0)
|
37
|
+
rescue SocketError, IOError, SystemCallError => error
|
38
|
+
StatsD.logger.debug do
|
39
|
+
"[StatsD::Instrument::UDPSink] Resetting connection because of #{error.class}: #{error.message}"
|
40
|
+
end
|
41
|
+
invalidate_socket
|
42
|
+
if retried
|
43
|
+
StatsD.logger.warn do
|
44
|
+
"[#{self.class.name}] Events were dropped because of #{error.class}: #{error.message}"
|
45
|
+
end
|
46
|
+
else
|
47
|
+
retried = true
|
48
|
+
retry
|
49
|
+
end
|
32
50
|
end
|
33
|
-
invalidate_socket
|
34
51
|
self
|
35
52
|
end
|
36
53
|
|
37
54
|
private
|
38
55
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
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
|
-
|
48
|
-
def with_socket
|
49
|
-
synchronize { yield(socket) }
|
56
|
+
def invalidate_socket
|
57
|
+
socket = thread_store.delete(object_id)
|
58
|
+
socket&.close
|
50
59
|
end
|
51
60
|
|
52
61
|
def socket
|
53
|
-
|
62
|
+
thread_store[object_id] ||= begin
|
54
63
|
socket = UDPSocket.new
|
55
64
|
socket.connect(@host, @port)
|
56
65
|
socket
|
57
66
|
end
|
58
67
|
end
|
59
68
|
|
60
|
-
def
|
61
|
-
|
62
|
-
@socket = nil
|
63
|
-
end
|
69
|
+
def thread_store
|
70
|
+
Thread.current["StatsD::UDPSink"] ||= {}
|
64
71
|
end
|
65
72
|
end
|
66
73
|
end
|
data/statsd-instrument.gemspec
CHANGED
data/test/environment_test.rb
CHANGED
@@ -64,4 +64,13 @@ class EnvironmentTest < Minitest::Test
|
|
64
64
|
)
|
65
65
|
assert_kind_of(StatsD::Instrument::UDPSink, env.client.sink)
|
66
66
|
end
|
67
|
+
|
68
|
+
def test_client_from_env_uses_regular_udp_sink_when_buffer_capacity_is_0
|
69
|
+
env = StatsD::Instrument::Environment.new(
|
70
|
+
"STATSD_USE_NEW_CLIENT" => "1",
|
71
|
+
"STATSD_ENV" => "staging",
|
72
|
+
"STATSD_BUFFER_CAPACITY" => "0",
|
73
|
+
)
|
74
|
+
assert_kind_of(StatsD::Instrument::UDPSink, env.client.sink)
|
75
|
+
end
|
67
76
|
end
|
data/test/udp_sink_test.rb
CHANGED
@@ -34,15 +34,14 @@ module UDPSinkTests
|
|
34
34
|
|
35
35
|
def test_concurrency
|
36
36
|
udp_sink = build_sink(@host, @port)
|
37
|
-
threads =
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
datagram, _source = @receiver.recvfrom(4096)
|
42
|
-
datagrams += datagram.split("\n")
|
37
|
+
threads = 10.times.map do |i|
|
38
|
+
Thread.new do
|
39
|
+
udp_sink << "foo:#{i}|c" << "bar:#{i}|c" << "baz:#{i}|c" << "plop:#{i}|c"
|
40
|
+
end
|
43
41
|
end
|
44
|
-
|
45
|
-
|
42
|
+
threads.each(&:join)
|
43
|
+
udp_sink.shutdown if udp_sink.respond_to?(:shutdown)
|
44
|
+
assert_equal(40, read_datagrams(40).size)
|
46
45
|
ensure
|
47
46
|
threads&.each(&:kill)
|
48
47
|
end
|
@@ -111,11 +110,12 @@ module UDPSinkTests
|
|
111
110
|
@sink_class.new(host, port)
|
112
111
|
end
|
113
112
|
|
114
|
-
def read_datagrams(count, timeout:
|
113
|
+
def read_datagrams(count, timeout: ENV["CI"] ? 5 : 1)
|
115
114
|
datagrams = []
|
116
115
|
count.times do
|
117
116
|
if @receiver.wait_readable(timeout)
|
118
|
-
datagrams += @receiver.
|
117
|
+
datagrams += @receiver.recvfrom(2000).first.lines(chomp: true)
|
118
|
+
break if datagrams.size >= count
|
119
119
|
else
|
120
120
|
break
|
121
121
|
end
|
@@ -149,8 +149,9 @@ module UDPSinkTests
|
|
149
149
|
seq = sequence("connect_fail_connect_succeed")
|
150
150
|
socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
|
151
151
|
socket.expects(:send).raises(Errno::EDESTADDRREQ).in_sequence(seq)
|
152
|
+
socket.expects(:close).in_sequence(seq)
|
152
153
|
socket.expects(:connect).with("localhost", 8125).in_sequence(seq)
|
153
|
-
socket.expects(:send).returns(1).in_sequence(seq)
|
154
|
+
socket.expects(:send).twice.returns(1).in_sequence(seq)
|
154
155
|
|
155
156
|
udp_sink = build_sink("localhost", 8125)
|
156
157
|
udp_sink << "foo:1|c"
|
@@ -187,52 +188,13 @@ module UDPSinkTests
|
|
187
188
|
private
|
188
189
|
|
189
190
|
def build_sink(host = @host, port = @port)
|
190
|
-
sink = @sink_class.new(host, port,
|
191
|
+
sink = @sink_class.new(host, port, buffer_capacity: 50)
|
191
192
|
@sinks << sink
|
192
193
|
sink
|
193
194
|
end
|
194
|
-
|
195
|
-
def default_flush_threshold
|
196
|
-
StatsD::Instrument::BatchedUDPSink::DEFAULT_FLUSH_THRESHOLD
|
197
|
-
end
|
198
195
|
end
|
199
196
|
|
200
197
|
class BatchedUDPSinkTest < Minitest::Test
|
201
198
|
include BatchedUDPSinkTests
|
202
|
-
|
203
|
-
def test_concurrency_buffering
|
204
|
-
udp_sink = build_sink(@host, @port)
|
205
|
-
threads = 50.times.map do |i|
|
206
|
-
Thread.new do
|
207
|
-
udp_sink << "foo:#{i}|c" << "bar:#{i}|c" << "baz:#{i}|c" << "plop:#{i}|c"
|
208
|
-
end
|
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
|
216
|
-
|
217
|
-
class LowThresholdBatchedUDPSinkTest < Minitest::Test
|
218
|
-
include BatchedUDPSinkTests
|
219
|
-
|
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
|
231
|
-
|
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
|
236
|
-
end
|
237
199
|
end
|
238
200
|
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jesse Storimer
|
@@ -10,22 +10,8 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2022-
|
14
|
-
dependencies:
|
15
|
-
- !ruby/object:Gem::Dependency
|
16
|
-
name: concurrent-ruby
|
17
|
-
requirement: !ruby/object:Gem::Requirement
|
18
|
-
requirements:
|
19
|
-
- - ">="
|
20
|
-
- !ruby/object:Gem::Version
|
21
|
-
version: '0'
|
22
|
-
type: :development
|
23
|
-
prerelease: false
|
24
|
-
version_requirements: !ruby/object:Gem::Requirement
|
25
|
-
requirements:
|
26
|
-
- - ">="
|
27
|
-
- !ruby/object:Gem::Version
|
28
|
-
version: '0'
|
13
|
+
date: 2022-08-29 00:00:00.000000000 Z
|
14
|
+
dependencies: []
|
29
15
|
description: A StatsD client for Ruby apps. Provides metaprogramming methods to inject
|
30
16
|
StatsD instrumentation into your code.
|
31
17
|
email:
|
@@ -49,6 +35,7 @@ files:
|
|
49
35
|
- README.md
|
50
36
|
- Rakefile
|
51
37
|
- benchmark/README.md
|
38
|
+
- benchmark/local-udp-throughput
|
52
39
|
- benchmark/send-metrics-to-dev-null-log
|
53
40
|
- benchmark/send-metrics-to-local-udp-receiver
|
54
41
|
- bin/rake
|