dogstatsd-ruby 4.8.2 → 5.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +157 -21
- data/lib/datadog/statsd/connection.rb +16 -18
- data/lib/datadog/statsd/connection_cfg.rb +76 -0
- data/lib/datadog/statsd/forwarder.rb +131 -0
- data/lib/datadog/statsd/message_buffer.rb +97 -0
- data/lib/datadog/statsd/sender.rb +177 -0
- data/lib/datadog/statsd/serialization/event_serializer.rb +5 -1
- data/lib/datadog/statsd/serialization/tag_serializer.rb +5 -1
- data/lib/datadog/statsd/single_thread_sender.rb +66 -0
- data/lib/datadog/statsd/telemetry.rb +43 -24
- data/lib/datadog/statsd/timer.rb +60 -0
- data/lib/datadog/statsd/udp_connection.rb +23 -14
- data/lib/datadog/statsd/uds_connection.rb +19 -8
- data/lib/datadog/statsd/version.rb +1 -1
- data/lib/datadog/statsd.rb +124 -68
- metadata +22 -13
- data/lib/datadog/statsd/batch.rb +0 -56
@@ -0,0 +1,177 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
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).
|
12
|
+
class Sender
|
13
|
+
CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
|
14
|
+
|
15
|
+
def initialize(message_buffer, telemetry: nil, queue_size: UDP_DEFAULT_BUFFER_SIZE, logger: nil, flush_interval: nil, queue_class: Queue, thread_class: Thread)
|
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
|
+
if flush_interval
|
24
|
+
@flush_timer = Datadog::Statsd::Timer.new(flush_interval) { flush(sync: true) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def flush(sync: false)
|
29
|
+
# keep a copy around in case another thread is calling #stop while this method is running
|
30
|
+
current_message_queue = message_queue
|
31
|
+
|
32
|
+
# don't try to flush if there is no message_queue instantiated or
|
33
|
+
# no companion thread running
|
34
|
+
if !current_message_queue
|
35
|
+
@logger.debug { "Statsd: can't flush: no message queue ready" } if @logger
|
36
|
+
return
|
37
|
+
end
|
38
|
+
if !sender_thread.alive?
|
39
|
+
@logger.debug { "Statsd: can't flush: no sender_thread alive" } if @logger
|
40
|
+
return
|
41
|
+
end
|
42
|
+
|
43
|
+
current_message_queue.push(:flush)
|
44
|
+
rendez_vous if sync
|
45
|
+
end
|
46
|
+
|
47
|
+
def rendez_vous
|
48
|
+
# could happen if #start hasn't be called
|
49
|
+
return unless message_queue
|
50
|
+
|
51
|
+
# Initialize and get the thread's sync queue
|
52
|
+
queue = (@thread_class.current[:statsd_sync_queue] ||= @queue_class.new)
|
53
|
+
# tell sender-thread to notify us in the current
|
54
|
+
# thread's queue
|
55
|
+
message_queue.push(queue)
|
56
|
+
# wait for the sender thread to send a message
|
57
|
+
# once the flush is done
|
58
|
+
queue.pop
|
59
|
+
end
|
60
|
+
|
61
|
+
def add(message)
|
62
|
+
raise ArgumentError, 'Start sender first' unless message_queue
|
63
|
+
|
64
|
+
# if the thread does not exist, we assume we are running in a forked process,
|
65
|
+
# empty the message queue and message buffers (these messages belong to
|
66
|
+
# the parent process) and spawn a new companion thread.
|
67
|
+
if !sender_thread.alive?
|
68
|
+
@mx.synchronize {
|
69
|
+
# a call from another thread has already re-created
|
70
|
+
# the companion thread before this one acquired the lock
|
71
|
+
break if sender_thread.alive?
|
72
|
+
@logger.debug { "Statsd: companion thread is dead, re-creating one" } if @logger
|
73
|
+
|
74
|
+
message_queue.close if CLOSEABLE_QUEUES
|
75
|
+
@message_queue = nil
|
76
|
+
message_buffer.reset
|
77
|
+
start
|
78
|
+
@flush_timer.start if @flush_timer && @flush_timer.stop?
|
79
|
+
}
|
80
|
+
end
|
81
|
+
|
82
|
+
if message_queue.length <= @queue_size
|
83
|
+
message_queue << message
|
84
|
+
else
|
85
|
+
@telemetry.dropped_queue(packets: 1, bytes: message.bytesize) if @telemetry
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def start
|
90
|
+
raise ArgumentError, 'Sender already started' if message_queue
|
91
|
+
|
92
|
+
# initialize a new message queue for the background thread
|
93
|
+
@message_queue = @queue_class.new
|
94
|
+
# start background thread
|
95
|
+
@sender_thread = @thread_class.new(&method(:send_loop))
|
96
|
+
@sender_thread.name = "Statsd Sender" unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
97
|
+
@flush_timer.start if @flush_timer
|
98
|
+
end
|
99
|
+
|
100
|
+
if CLOSEABLE_QUEUES
|
101
|
+
# when calling stop, make sure that no other threads is trying
|
102
|
+
# to close the sender nor trying to continue to `#add` more message
|
103
|
+
# into the sender.
|
104
|
+
def stop(join_worker: true)
|
105
|
+
message_queue = @message_queue
|
106
|
+
message_queue.close if message_queue
|
107
|
+
|
108
|
+
@flush_timer.stop if @flush_timer
|
109
|
+
sender_thread = @sender_thread
|
110
|
+
sender_thread.join if sender_thread && join_worker
|
111
|
+
end
|
112
|
+
else
|
113
|
+
# when calling stop, make sure that no other threads is trying
|
114
|
+
# to close the sender nor trying to continue to `#add` more message
|
115
|
+
# into the sender.
|
116
|
+
def stop(join_worker: true)
|
117
|
+
message_queue = @message_queue
|
118
|
+
message_queue << :close if message_queue
|
119
|
+
|
120
|
+
@flush_timer.stop if @flush_timer
|
121
|
+
sender_thread = @sender_thread
|
122
|
+
sender_thread.join if sender_thread && join_worker
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
attr_reader :message_buffer
|
129
|
+
attr_reader :message_queue
|
130
|
+
attr_reader :sender_thread
|
131
|
+
|
132
|
+
if CLOSEABLE_QUEUES
|
133
|
+
def send_loop
|
134
|
+
until (message = message_queue.pop).nil? && message_queue.closed?
|
135
|
+
# skip if message is nil, e.g. when message_queue
|
136
|
+
# is empty and closed
|
137
|
+
next unless message
|
138
|
+
|
139
|
+
case message
|
140
|
+
when :flush
|
141
|
+
message_buffer.flush
|
142
|
+
when @queue_class
|
143
|
+
message.push(:go_on)
|
144
|
+
else
|
145
|
+
message_buffer.add(message)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
@message_queue = nil
|
150
|
+
@sender_thread = nil
|
151
|
+
end
|
152
|
+
else
|
153
|
+
def send_loop
|
154
|
+
loop do
|
155
|
+
message = message_queue.pop
|
156
|
+
|
157
|
+
next unless message
|
158
|
+
|
159
|
+
case message
|
160
|
+
when :close
|
161
|
+
break
|
162
|
+
when :flush
|
163
|
+
message_buffer.flush
|
164
|
+
when @queue_class
|
165
|
+
message.push(:go_on)
|
166
|
+
else
|
167
|
+
message_buffer.add(message)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
@message_queue = nil
|
172
|
+
@sender_thread = nil
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -48,7 +48,11 @@ module Datadog
|
|
48
48
|
end
|
49
49
|
|
50
50
|
if event.bytesize > MAX_EVENT_SIZE
|
51
|
-
|
51
|
+
if options[:truncate_if_too_long]
|
52
|
+
event.slice!(MAX_EVENT_SIZE..event.length)
|
53
|
+
else
|
54
|
+
raise "Event #{title} payload is too big (more that 8KB), event discarded"
|
55
|
+
end
|
52
56
|
end
|
53
57
|
end
|
54
58
|
end
|
@@ -13,7 +13,11 @@ module Datadog
|
|
13
13
|
|
14
14
|
# Convert to tag list and set
|
15
15
|
@global_tags = to_tags_list(global_tags)
|
16
|
-
|
16
|
+
if @global_tags.any?
|
17
|
+
@global_tags_formatted = @global_tags.join(',')
|
18
|
+
else
|
19
|
+
@global_tags_formatted = nil
|
20
|
+
end
|
17
21
|
end
|
18
22
|
|
19
23
|
def format(message_tags)
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
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.
|
9
|
+
class SingleThreadSender
|
10
|
+
def initialize(message_buffer, logger: nil, flush_interval: nil)
|
11
|
+
@message_buffer = message_buffer
|
12
|
+
@logger = logger
|
13
|
+
@mx = Mutex.new
|
14
|
+
if flush_interval
|
15
|
+
@flush_timer = Datadog::Statsd::Timer.new(flush_interval) { flush }
|
16
|
+
end
|
17
|
+
# store the pid for which this sender has been created
|
18
|
+
update_fork_pid
|
19
|
+
end
|
20
|
+
|
21
|
+
def add(message)
|
22
|
+
@mx.synchronize {
|
23
|
+
# we have just forked, meaning we have messages in the buffer that we should
|
24
|
+
# not send, they belong to the parent process, let's clear the buffer.
|
25
|
+
if forked?
|
26
|
+
@message_buffer.reset
|
27
|
+
@flush_timer.start if @flush_timer && @flush_timer.stop?
|
28
|
+
update_fork_pid
|
29
|
+
end
|
30
|
+
@message_buffer.add(message)
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def flush(*)
|
35
|
+
@mx.synchronize {
|
36
|
+
@message_buffer.flush()
|
37
|
+
}
|
38
|
+
end
|
39
|
+
|
40
|
+
def start()
|
41
|
+
@flush_timer.start if @flush_timer
|
42
|
+
end
|
43
|
+
|
44
|
+
def stop()
|
45
|
+
@flush_timer.stop if @flush_timer
|
46
|
+
end
|
47
|
+
|
48
|
+
# Compatibility with `Sender`
|
49
|
+
def rendez_vous()
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# below are "fork management" methods to be able to clean the MessageBuffer
|
55
|
+
# if it detects that it is running in a unknown PID.
|
56
|
+
|
57
|
+
def forked?
|
58
|
+
Process.pid != @fork_pid
|
59
|
+
end
|
60
|
+
|
61
|
+
def update_fork_pid
|
62
|
+
@fork_pid = Process.pid
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -9,12 +9,17 @@ 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
|
14
|
-
attr_reader :
|
16
|
+
attr_reader :packets_dropped_queue
|
17
|
+
attr_reader :packets_dropped_writer
|
15
18
|
|
16
|
-
|
17
|
-
|
19
|
+
# Rough estimation of maximum telemetry message size without tags
|
20
|
+
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
|
21
|
+
|
22
|
+
def initialize(flush_interval, global_tags: [], transport_type: :udp)
|
18
23
|
@flush_interval = flush_interval
|
19
24
|
@global_tags = global_tags
|
20
25
|
@transport_type = transport_type
|
@@ -27,15 +32,10 @@ module Datadog
|
|
27
32
|
client_version: VERSION,
|
28
33
|
client_transport: transport_type,
|
29
34
|
).format(global_tags)
|
35
|
+
end
|
30
36
|
|
31
|
-
|
32
|
-
|
33
|
-
# 'max_buffer_bytes', we have to adjust with the size of the telemetry
|
34
|
-
# (and any tags used). The telemetry payload size will change depending
|
35
|
-
# on the actual value of metrics: metrics received, packet dropped,
|
36
|
-
# etc. This is why we add a 63bytes margin: 9 bytes for each of the 7
|
37
|
-
# telemetry metrics.
|
38
|
-
@estimate_max_size = disabled ? 0 : flush.length + 9 * 7
|
37
|
+
def would_fit_in?(max_buffer_payload_size)
|
38
|
+
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
|
39
39
|
end
|
40
40
|
|
41
41
|
def reset
|
@@ -44,8 +44,12 @@ module Datadog
|
|
44
44
|
@service_checks = 0
|
45
45
|
@bytes_sent = 0
|
46
46
|
@bytes_dropped = 0
|
47
|
+
@bytes_dropped_queue = 0
|
48
|
+
@bytes_dropped_writer = 0
|
47
49
|
@packets_sent = 0
|
48
50
|
@packets_dropped = 0
|
51
|
+
@packets_dropped_queue = 0
|
52
|
+
@packets_dropped_writer = 0
|
49
53
|
@next_flush_time = now_in_s + @flush_interval
|
50
54
|
end
|
51
55
|
|
@@ -58,32 +62,47 @@ module Datadog
|
|
58
62
|
@packets_sent += packets
|
59
63
|
end
|
60
64
|
|
61
|
-
def
|
65
|
+
def dropped_queue(bytes: 0, packets: 0)
|
62
66
|
@bytes_dropped += bytes
|
67
|
+
@bytes_dropped_queue += bytes
|
63
68
|
@packets_dropped += packets
|
69
|
+
@packets_dropped_queue += packets
|
64
70
|
end
|
65
71
|
|
66
|
-
def
|
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
|
77
|
+
end
|
78
|
+
|
79
|
+
def should_flush?
|
67
80
|
@next_flush_time < now_in_s
|
68
81
|
end
|
69
82
|
|
70
83
|
def flush
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
84
|
+
[
|
85
|
+
sprintf(pattern, 'metrics', @metrics),
|
86
|
+
sprintf(pattern, 'events', @events),
|
87
|
+
sprintf(pattern, 'service_checks', @service_checks),
|
88
|
+
sprintf(pattern, 'bytes_sent', @bytes_sent),
|
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),
|
92
|
+
sprintf(pattern, 'packets_sent', @packets_sent),
|
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),
|
96
|
+
]
|
82
97
|
end
|
83
98
|
|
84
99
|
private
|
85
100
|
attr_reader :serialized_tags
|
86
101
|
|
102
|
+
def pattern
|
103
|
+
@pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
|
104
|
+
end
|
105
|
+
|
87
106
|
if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
|
88
107
|
def now_in_s
|
89
108
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
@@ -0,0 +1,60 @@
|
|
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
|
+
end
|
13
|
+
|
14
|
+
def start
|
15
|
+
return unless stop?
|
16
|
+
|
17
|
+
@stop = false
|
18
|
+
@thread = Thread.new do
|
19
|
+
last_execution_time = current_time
|
20
|
+
@mx.synchronize do
|
21
|
+
until @stop
|
22
|
+
timeout = @interval - (current_time - last_execution_time)
|
23
|
+
@cv.wait(@mx, timeout > 0 ? timeout : 0)
|
24
|
+
last_execution_time = current_time
|
25
|
+
@callback.call
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@thread.name = 'Statsd Timer' unless Gem::Version.new(RUBY_VERSION) < Gem::Version.new('2.3')
|
30
|
+
end
|
31
|
+
|
32
|
+
def stop
|
33
|
+
return if @thread.nil?
|
34
|
+
|
35
|
+
@stop = true
|
36
|
+
@mx.synchronize do
|
37
|
+
@cv.signal
|
38
|
+
end
|
39
|
+
@thread.join
|
40
|
+
@thread = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def stop?
|
44
|
+
@thread.nil? || @thread.stop?
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
if Process.const_defined?(:CLOCK_MONOTONIC)
|
50
|
+
def current_time
|
51
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
def current_time
|
55
|
+
Time.now
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -5,32 +5,41 @@ 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
|
-
def initialize(host, port,
|
18
|
-
super(
|
19
|
-
|
20
|
-
@
|
21
|
-
@
|
14
|
+
def initialize(host, port, **kwargs)
|
15
|
+
super(**kwargs)
|
16
|
+
|
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
|
+
@socket = UDPSocket.new
|
33
|
+
@socket.connect(host, port)
|
30
34
|
end
|
31
35
|
|
36
|
+
# send_message is writing the message in the socket, it may create the socket if nil
|
37
|
+
# It is not thread-safe but since it is called by either the Sender bg thread or the
|
38
|
+
# SingleThreadSender (which is using a mutex while Flushing), only one thread must call
|
39
|
+
# it at a time.
|
32
40
|
def send_message(message)
|
33
|
-
socket
|
41
|
+
connect unless @socket
|
42
|
+
@socket.send(message, 0)
|
34
43
|
end
|
35
44
|
end
|
36
45
|
end
|
@@ -10,24 +10,35 @@ module Datadog
|
|
10
10
|
# DogStatsd unix socket path
|
11
11
|
attr_reader :socket_path
|
12
12
|
|
13
|
-
def initialize(socket_path,
|
14
|
-
super(
|
13
|
+
def initialize(socket_path, **kwargs)
|
14
|
+
super(**kwargs)
|
15
|
+
|
15
16
|
@socket_path = socket_path
|
16
|
-
@
|
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.
|