dogstatsd-ruby 5.2.0 → 5.6.1
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/README.md +89 -70
- data/lib/datadog/statsd/connection.rb +12 -11
- data/lib/datadog/statsd/connection_cfg.rb +125 -0
- data/lib/datadog/statsd/forwarder.rb +34 -18
- data/lib/datadog/statsd/message_buffer.rb +22 -5
- data/lib/datadog/statsd/sender.rb +80 -13
- data/lib/datadog/statsd/single_thread_sender.rb +56 -5
- data/lib/datadog/statsd/telemetry.rb +22 -1
- data/lib/datadog/statsd/timer.rb +61 -0
- data/lib/datadog/statsd/udp_connection.rb +22 -11
- data/lib/datadog/statsd/uds_connection.rb +16 -5
- data/lib/datadog/statsd/version.rb +1 -1
- data/lib/datadog/statsd.rb +67 -18
- metadata +15 -9
@@ -2,25 +2,56 @@
|
|
2
2
|
|
3
3
|
module Datadog
|
4
4
|
class Statsd
|
5
|
+
# Sender is using a companion thread to flush and pack messages
|
6
|
+
# in a `MessageBuffer`.
|
7
|
+
# The communication with this thread is done using a `Queue`.
|
8
|
+
# If the thread is dead, it is starting a new one to avoid having a blocked
|
9
|
+
# Sender with no companion thread to communicate with (most of the time, having
|
10
|
+
# a dead companion thread means that a fork just happened and that we are
|
11
|
+
# running in the child process).
|
5
12
|
class Sender
|
6
13
|
CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
|
7
14
|
|
8
|
-
def initialize(message_buffer)
|
15
|
+
def initialize(message_buffer, telemetry: nil, queue_size: UDP_DEFAULT_BUFFER_SIZE, logger: nil, flush_interval: nil, queue_class: Queue, thread_class: Thread)
|
9
16
|
@message_buffer = message_buffer
|
17
|
+
@telemetry = telemetry
|
18
|
+
@queue_size = queue_size
|
19
|
+
@logger = logger
|
20
|
+
@mx = Mutex.new
|
21
|
+
@queue_class = queue_class
|
22
|
+
@thread_class = thread_class
|
23
|
+
@flush_timer = if flush_interval
|
24
|
+
Datadog::Statsd::Timer.new(flush_interval) { flush(sync: true) }
|
25
|
+
else
|
26
|
+
nil
|
27
|
+
end
|
10
28
|
end
|
11
29
|
|
12
30
|
def flush(sync: false)
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
message_queue
|
31
|
+
# keep a copy around in case another thread is calling #stop while this method is running
|
32
|
+
current_message_queue = message_queue
|
33
|
+
|
34
|
+
# don't try to flush if there is no message_queue instantiated or
|
35
|
+
# no companion thread running
|
36
|
+
if !current_message_queue
|
37
|
+
@logger.debug { "Statsd: can't flush: no message queue ready" } if @logger
|
38
|
+
return
|
39
|
+
end
|
40
|
+
if !sender_thread.alive?
|
41
|
+
@logger.debug { "Statsd: can't flush: no sender_thread alive" } if @logger
|
42
|
+
return
|
43
|
+
end
|
17
44
|
|
45
|
+
current_message_queue.push(:flush)
|
18
46
|
rendez_vous if sync
|
19
47
|
end
|
20
48
|
|
21
49
|
def rendez_vous
|
50
|
+
# could happen if #start hasn't be called
|
51
|
+
return unless message_queue
|
52
|
+
|
22
53
|
# Initialize and get the thread's sync queue
|
23
|
-
queue = (
|
54
|
+
queue = (@thread_class.current[:statsd_sync_queue] ||= @queue_class.new)
|
24
55
|
# tell sender-thread to notify us in the current
|
25
56
|
# thread's queue
|
26
57
|
message_queue.push(queue)
|
@@ -32,20 +63,52 @@ module Datadog
|
|
32
63
|
def add(message)
|
33
64
|
raise ArgumentError, 'Start sender first' unless message_queue
|
34
65
|
|
35
|
-
|
66
|
+
# if the thread does not exist, we assume we are running in a forked process,
|
67
|
+
# empty the message queue and message buffers (these messages belong to
|
68
|
+
# the parent process) and spawn a new companion thread.
|
69
|
+
if !sender_thread.alive?
|
70
|
+
@mx.synchronize {
|
71
|
+
# a call from another thread has already re-created
|
72
|
+
# the companion thread before this one acquired the lock
|
73
|
+
break if sender_thread.alive?
|
74
|
+
@logger.debug { "Statsd: companion thread is dead, re-creating one" } if @logger
|
75
|
+
|
76
|
+
message_queue.close if CLOSEABLE_QUEUES
|
77
|
+
@message_queue = nil
|
78
|
+
message_buffer.reset
|
79
|
+
start
|
80
|
+
@flush_timer.start if @flush_timer && @flush_timer.stop?
|
81
|
+
}
|
82
|
+
end
|
83
|
+
|
84
|
+
if message_queue.length <= @queue_size
|
85
|
+
message_queue << message
|
86
|
+
else
|
87
|
+
if @telemetry
|
88
|
+
bytesize = message.respond_to?(:bytesize) ? message.bytesize : 0
|
89
|
+
@telemetry.dropped_queue(packets: 1, bytes: bytesize)
|
90
|
+
end
|
91
|
+
end
|
36
92
|
end
|
37
93
|
|
38
94
|
def start
|
39
95
|
raise ArgumentError, 'Sender already started' if message_queue
|
40
96
|
|
41
|
-
# initialize message queue for background thread
|
42
|
-
@message_queue =
|
97
|
+
# initialize a new message queue for the background thread
|
98
|
+
@message_queue = @queue_class.new
|
43
99
|
# start background thread
|
44
|
-
@sender_thread =
|
100
|
+
@sender_thread = @thread_class.new(&method(:send_loop))
|
101
|
+
@sender_thread.name = "Statsd Sender" unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
102
|
+
@flush_timer.start if @flush_timer
|
45
103
|
end
|
46
104
|
|
47
105
|
if CLOSEABLE_QUEUES
|
106
|
+
# when calling stop, make sure that no other threads is trying
|
107
|
+
# to close the sender nor trying to continue to `#add` more message
|
108
|
+
# into the sender.
|
48
109
|
def stop(join_worker: true)
|
110
|
+
@flush_timer.stop if @flush_timer
|
111
|
+
|
49
112
|
message_queue = @message_queue
|
50
113
|
message_queue.close if message_queue
|
51
114
|
|
@@ -53,7 +116,12 @@ module Datadog
|
|
53
116
|
sender_thread.join if sender_thread && join_worker
|
54
117
|
end
|
55
118
|
else
|
119
|
+
# when calling stop, make sure that no other threads is trying
|
120
|
+
# to close the sender nor trying to continue to `#add` more message
|
121
|
+
# into the sender.
|
56
122
|
def stop(join_worker: true)
|
123
|
+
@flush_timer.stop if @flush_timer
|
124
|
+
|
57
125
|
message_queue = @message_queue
|
58
126
|
message_queue << :close if message_queue
|
59
127
|
|
@@ -65,7 +133,6 @@ module Datadog
|
|
65
133
|
private
|
66
134
|
|
67
135
|
attr_reader :message_buffer
|
68
|
-
|
69
136
|
attr_reader :message_queue
|
70
137
|
attr_reader :sender_thread
|
71
138
|
|
@@ -79,7 +146,7 @@ module Datadog
|
|
79
146
|
case message
|
80
147
|
when :flush
|
81
148
|
message_buffer.flush
|
82
|
-
when
|
149
|
+
when @queue_class
|
83
150
|
message.push(:go_on)
|
84
151
|
else
|
85
152
|
message_buffer.add(message)
|
@@ -101,7 +168,7 @@ module Datadog
|
|
101
168
|
break
|
102
169
|
when :flush
|
103
170
|
message_buffer.flush
|
104
|
-
when
|
171
|
+
when @queue_class
|
105
172
|
message.push(:go_on)
|
106
173
|
else
|
107
174
|
message_buffer.add(message)
|
@@ -2,30 +2,81 @@
|
|
2
2
|
|
3
3
|
module Datadog
|
4
4
|
class Statsd
|
5
|
+
# The SingleThreadSender is a sender synchronously buffering messages
|
6
|
+
# in a `MessageBuffer`.
|
7
|
+
# It is using current Process.PID to check it is the result of a recent fork
|
8
|
+
# and it is reseting the MessageBuffer if that's the case.
|
5
9
|
class SingleThreadSender
|
6
|
-
def initialize(message_buffer)
|
10
|
+
def initialize(message_buffer, logger: nil, flush_interval: nil, queue_size: 1)
|
7
11
|
@message_buffer = message_buffer
|
12
|
+
@logger = logger
|
13
|
+
@mx = Mutex.new
|
14
|
+
@message_queue_size = queue_size
|
15
|
+
@message_queue = []
|
16
|
+
@flush_timer = if flush_interval
|
17
|
+
Datadog::Statsd::Timer.new(flush_interval) { flush }
|
18
|
+
else
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
# store the pid for which this sender has been created
|
22
|
+
update_fork_pid
|
8
23
|
end
|
9
24
|
|
10
25
|
def add(message)
|
11
|
-
@
|
26
|
+
@mx.synchronize {
|
27
|
+
# we have just forked, meaning we have messages in the buffer that we should
|
28
|
+
# not send, they belong to the parent process, let's clear the buffer.
|
29
|
+
if forked?
|
30
|
+
@message_buffer.reset
|
31
|
+
@message_queue.clear
|
32
|
+
@flush_timer.start if @flush_timer && @flush_timer.stop?
|
33
|
+
update_fork_pid
|
34
|
+
end
|
35
|
+
|
36
|
+
@message_queue << message
|
37
|
+
if @message_queue.size >= @message_queue_size
|
38
|
+
drain_message_queue
|
39
|
+
end
|
40
|
+
}
|
12
41
|
end
|
13
42
|
|
14
43
|
def flush(*)
|
15
|
-
@
|
44
|
+
@mx.synchronize {
|
45
|
+
drain_message_queue
|
46
|
+
@message_buffer.flush()
|
47
|
+
}
|
16
48
|
end
|
17
49
|
|
18
|
-
# Compatibility with `Sender`
|
19
50
|
def start()
|
51
|
+
@flush_timer.start if @flush_timer
|
20
52
|
end
|
21
53
|
|
22
|
-
# Compatibility with `Sender`
|
23
54
|
def stop()
|
55
|
+
@flush_timer.stop if @flush_timer
|
24
56
|
end
|
25
57
|
|
26
58
|
# Compatibility with `Sender`
|
27
59
|
def rendez_vous()
|
28
60
|
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def drain_message_queue
|
65
|
+
while msg = @message_queue.shift
|
66
|
+
@message_buffer.add(msg)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# below are "fork management" methods to be able to clean the MessageBuffer
|
71
|
+
# if it detects that it is running in a unknown PID.
|
72
|
+
|
73
|
+
def forked?
|
74
|
+
Process.pid != @fork_pid
|
75
|
+
end
|
76
|
+
|
77
|
+
def update_fork_pid
|
78
|
+
@fork_pid = Process.pid
|
79
|
+
end
|
29
80
|
end
|
30
81
|
end
|
31
82
|
end
|
@@ -9,8 +9,12 @@ module Datadog
|
|
9
9
|
attr_reader :service_checks
|
10
10
|
attr_reader :bytes_sent
|
11
11
|
attr_reader :bytes_dropped
|
12
|
+
attr_reader :bytes_dropped_queue
|
13
|
+
attr_reader :bytes_dropped_writer
|
12
14
|
attr_reader :packets_sent
|
13
15
|
attr_reader :packets_dropped
|
16
|
+
attr_reader :packets_dropped_queue
|
17
|
+
attr_reader :packets_dropped_writer
|
14
18
|
|
15
19
|
# Rough estimation of maximum telemetry message size without tags
|
16
20
|
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
|
@@ -40,8 +44,12 @@ module Datadog
|
|
40
44
|
@service_checks = 0
|
41
45
|
@bytes_sent = 0
|
42
46
|
@bytes_dropped = 0
|
47
|
+
@bytes_dropped_queue = 0
|
48
|
+
@bytes_dropped_writer = 0
|
43
49
|
@packets_sent = 0
|
44
50
|
@packets_dropped = 0
|
51
|
+
@packets_dropped_queue = 0
|
52
|
+
@packets_dropped_writer = 0
|
45
53
|
@next_flush_time = now_in_s + @flush_interval
|
46
54
|
end
|
47
55
|
|
@@ -54,9 +62,18 @@ module Datadog
|
|
54
62
|
@packets_sent += packets
|
55
63
|
end
|
56
64
|
|
57
|
-
def
|
65
|
+
def dropped_queue(bytes: 0, packets: 0)
|
58
66
|
@bytes_dropped += bytes
|
67
|
+
@bytes_dropped_queue += bytes
|
59
68
|
@packets_dropped += packets
|
69
|
+
@packets_dropped_queue += packets
|
70
|
+
end
|
71
|
+
|
72
|
+
def dropped_writer(bytes: 0, packets: 0)
|
73
|
+
@bytes_dropped += bytes
|
74
|
+
@bytes_dropped_writer += bytes
|
75
|
+
@packets_dropped += packets
|
76
|
+
@packets_dropped_writer += packets
|
60
77
|
end
|
61
78
|
|
62
79
|
def should_flush?
|
@@ -70,8 +87,12 @@ module Datadog
|
|
70
87
|
sprintf(pattern, 'service_checks', @service_checks),
|
71
88
|
sprintf(pattern, 'bytes_sent', @bytes_sent),
|
72
89
|
sprintf(pattern, 'bytes_dropped', @bytes_dropped),
|
90
|
+
sprintf(pattern, 'bytes_dropped_queue', @bytes_dropped_queue),
|
91
|
+
sprintf(pattern, 'bytes_dropped_writer', @bytes_dropped_writer),
|
73
92
|
sprintf(pattern, 'packets_sent', @packets_sent),
|
74
93
|
sprintf(pattern, 'packets_dropped', @packets_dropped),
|
94
|
+
sprintf(pattern, 'packets_dropped_queue', @packets_dropped_queue),
|
95
|
+
sprintf(pattern, 'packets_dropped_writer', @packets_dropped_writer),
|
75
96
|
]
|
76
97
|
end
|
77
98
|
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Timer
|
6
|
+
def initialize(interval, &callback)
|
7
|
+
@mx = Mutex.new
|
8
|
+
@cv = ConditionVariable.new
|
9
|
+
@interval = interval
|
10
|
+
@callback = callback
|
11
|
+
@stop = true
|
12
|
+
@thread = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
return unless stop?
|
17
|
+
|
18
|
+
@stop = false
|
19
|
+
@thread = Thread.new do
|
20
|
+
last_execution_time = current_time
|
21
|
+
@mx.synchronize do
|
22
|
+
until @stop
|
23
|
+
timeout = @interval - (current_time - last_execution_time)
|
24
|
+
@cv.wait(@mx, timeout > 0 ? timeout : 0)
|
25
|
+
last_execution_time = current_time
|
26
|
+
@callback.call
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
@thread.name = 'Statsd Timer' unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
31
|
+
end
|
32
|
+
|
33
|
+
def stop
|
34
|
+
return if @thread.nil?
|
35
|
+
|
36
|
+
@stop = true
|
37
|
+
@mx.synchronize do
|
38
|
+
@cv.signal
|
39
|
+
end
|
40
|
+
@thread.join
|
41
|
+
@thread = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop?
|
45
|
+
@thread.nil? || @thread.stop?
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
if Process.const_defined?(:CLOCK_MONOTONIC)
|
51
|
+
def current_time
|
52
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
53
|
+
end
|
54
|
+
else
|
55
|
+
def current_time
|
56
|
+
Time.now
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -5,32 +5,43 @@ require_relative 'connection'
|
|
5
5
|
module Datadog
|
6
6
|
class Statsd
|
7
7
|
class UDPConnection < Connection
|
8
|
-
|
9
|
-
DEFAULT_PORT = 8125
|
10
|
-
|
11
|
-
# StatsD host. Defaults to 127.0.0.1.
|
8
|
+
# StatsD host.
|
12
9
|
attr_reader :host
|
13
10
|
|
14
|
-
# StatsD port.
|
11
|
+
# StatsD port.
|
15
12
|
attr_reader :port
|
16
13
|
|
17
14
|
def initialize(host, port, **kwargs)
|
18
15
|
super(**kwargs)
|
19
16
|
|
20
|
-
@host = host
|
21
|
-
@port = port
|
17
|
+
@host = host
|
18
|
+
@port = port
|
19
|
+
@socket = nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def close
|
23
|
+
@socket.close if @socket
|
24
|
+
@socket = nil
|
22
25
|
end
|
23
26
|
|
24
27
|
private
|
25
28
|
|
26
29
|
def connect
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
+
close if @socket
|
31
|
+
|
32
|
+
family = Addrinfo.udp(host, port).afamily
|
33
|
+
|
34
|
+
@socket = UDPSocket.new(family)
|
35
|
+
@socket.connect(host, port)
|
30
36
|
end
|
31
37
|
|
38
|
+
# send_message is writing the message in the socket, it may create the socket if nil
|
39
|
+
# It is not thread-safe but since it is called by either the Sender bg thread or the
|
40
|
+
# SingleThreadSender (which is using a mutex while Flushing), only one thread must call
|
41
|
+
# it at a time.
|
32
42
|
def send_message(message)
|
33
|
-
socket
|
43
|
+
connect unless @socket
|
44
|
+
@socket.send(message, 0)
|
34
45
|
end
|
35
46
|
end
|
36
47
|
end
|
@@ -14,20 +14,31 @@ module Datadog
|
|
14
14
|
super(**kwargs)
|
15
15
|
|
16
16
|
@socket_path = socket_path
|
17
|
+
@socket = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def close
|
21
|
+
@socket.close if @socket
|
22
|
+
@socket = nil
|
17
23
|
end
|
18
24
|
|
19
25
|
private
|
20
26
|
|
21
27
|
def connect
|
22
|
-
|
23
|
-
|
24
|
-
socket
|
28
|
+
close if @socket
|
29
|
+
|
30
|
+
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
31
|
+
@socket.connect(Socket.pack_sockaddr_un(@socket_path))
|
25
32
|
end
|
26
33
|
|
34
|
+
# send_message is writing the message in the socket, it may create the socket if nil
|
35
|
+
# It is not thread-safe but since it is called by either the Sender bg thread or the
|
36
|
+
# SingleThreadSender (which is using a mutex while Flushing), only one thread must call
|
37
|
+
# it at a time.
|
27
38
|
def send_message(message)
|
28
|
-
socket
|
39
|
+
connect unless @socket
|
40
|
+
@socket.sendmsg_nonblock(message)
|
29
41
|
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
|
30
|
-
@socket = nil
|
31
42
|
# TODO: FIXME: This error should be considered as a retryable error in the
|
32
43
|
# Connection class. An even better solution would be to make BadSocketError inherit
|
33
44
|
# from a specific retryable error class in the Connection class.
|