dogstatsd-ruby 3.3.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 +197 -53
- data/lib/datadog/statsd/connection.rb +60 -0
- data/lib/datadog/statsd/connection_cfg.rb +125 -0
- data/lib/datadog/statsd/forwarder.rb +138 -0
- data/lib/datadog/statsd/message_buffer.rb +105 -0
- data/lib/datadog/statsd/sender.rb +184 -0
- data/lib/datadog/statsd/serialization/event_serializer.rb +71 -0
- data/lib/datadog/statsd/serialization/serializer.rb +41 -0
- data/lib/datadog/statsd/serialization/service_check_serializer.rb +60 -0
- data/lib/datadog/statsd/serialization/stat_serializer.rb +55 -0
- data/lib/datadog/statsd/serialization/tag_serializer.rb +96 -0
- data/lib/datadog/statsd/serialization.rb +15 -0
- data/lib/datadog/statsd/single_thread_sender.rb +82 -0
- data/lib/datadog/statsd/telemetry.rb +117 -0
- data/lib/datadog/statsd/timer.rb +61 -0
- data/lib/datadog/statsd/udp_connection.rb +48 -0
- data/lib/datadog/statsd/uds_connection.rb +49 -0
- data/lib/datadog/statsd/version.rb +9 -0
- data/lib/datadog/statsd.rb +233 -288
- metadata +37 -11
@@ -0,0 +1,82 @@
|
|
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, queue_size: 1)
|
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
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(message)
|
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
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def flush(*)
|
44
|
+
@mx.synchronize {
|
45
|
+
drain_message_queue
|
46
|
+
@message_buffer.flush()
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def start()
|
51
|
+
@flush_timer.start if @flush_timer
|
52
|
+
end
|
53
|
+
|
54
|
+
def stop()
|
55
|
+
@flush_timer.stop if @flush_timer
|
56
|
+
end
|
57
|
+
|
58
|
+
# Compatibility with `Sender`
|
59
|
+
def rendez_vous()
|
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
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
module Datadog
|
5
|
+
class Statsd
|
6
|
+
class Telemetry
|
7
|
+
attr_reader :metrics
|
8
|
+
attr_reader :events
|
9
|
+
attr_reader :service_checks
|
10
|
+
attr_reader :bytes_sent
|
11
|
+
attr_reader :bytes_dropped
|
12
|
+
attr_reader :bytes_dropped_queue
|
13
|
+
attr_reader :bytes_dropped_writer
|
14
|
+
attr_reader :packets_sent
|
15
|
+
attr_reader :packets_dropped
|
16
|
+
attr_reader :packets_dropped_queue
|
17
|
+
attr_reader :packets_dropped_writer
|
18
|
+
|
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)
|
23
|
+
@flush_interval = flush_interval
|
24
|
+
@global_tags = global_tags
|
25
|
+
@transport_type = transport_type
|
26
|
+
reset
|
27
|
+
|
28
|
+
# TODO: Karim: I don't know why but telemetry tags are serialized
|
29
|
+
# before global tags so by refactoring this, I am keeping the same behavior
|
30
|
+
@serialized_tags = Serialization::TagSerializer.new(
|
31
|
+
client: 'ruby',
|
32
|
+
client_version: VERSION,
|
33
|
+
client_transport: transport_type,
|
34
|
+
).format(global_tags)
|
35
|
+
end
|
36
|
+
|
37
|
+
def would_fit_in?(max_buffer_payload_size)
|
38
|
+
MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
|
39
|
+
end
|
40
|
+
|
41
|
+
def reset
|
42
|
+
@metrics = 0
|
43
|
+
@events = 0
|
44
|
+
@service_checks = 0
|
45
|
+
@bytes_sent = 0
|
46
|
+
@bytes_dropped = 0
|
47
|
+
@bytes_dropped_queue = 0
|
48
|
+
@bytes_dropped_writer = 0
|
49
|
+
@packets_sent = 0
|
50
|
+
@packets_dropped = 0
|
51
|
+
@packets_dropped_queue = 0
|
52
|
+
@packets_dropped_writer = 0
|
53
|
+
@next_flush_time = now_in_s + @flush_interval
|
54
|
+
end
|
55
|
+
|
56
|
+
def sent(metrics: 0, events: 0, service_checks: 0, bytes: 0, packets: 0)
|
57
|
+
@metrics += metrics
|
58
|
+
@events += events
|
59
|
+
@service_checks += service_checks
|
60
|
+
|
61
|
+
@bytes_sent += bytes
|
62
|
+
@packets_sent += packets
|
63
|
+
end
|
64
|
+
|
65
|
+
def dropped_queue(bytes: 0, packets: 0)
|
66
|
+
@bytes_dropped += bytes
|
67
|
+
@bytes_dropped_queue += bytes
|
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
|
77
|
+
end
|
78
|
+
|
79
|
+
def should_flush?
|
80
|
+
@next_flush_time < now_in_s
|
81
|
+
end
|
82
|
+
|
83
|
+
def flush
|
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
|
+
]
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
attr_reader :serialized_tags
|
101
|
+
|
102
|
+
def pattern
|
103
|
+
@pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
|
104
|
+
end
|
105
|
+
|
106
|
+
if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
|
107
|
+
def now_in_s
|
108
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
|
109
|
+
end
|
110
|
+
else
|
111
|
+
def now_in_s
|
112
|
+
Time.now.to_i
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -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
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'connection'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
class Statsd
|
7
|
+
class UDPConnection < Connection
|
8
|
+
# StatsD host.
|
9
|
+
attr_reader :host
|
10
|
+
|
11
|
+
# StatsD port.
|
12
|
+
attr_reader :port
|
13
|
+
|
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
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def connect
|
30
|
+
close if @socket
|
31
|
+
|
32
|
+
family = Addrinfo.udp(host, port).afamily
|
33
|
+
|
34
|
+
@socket = UDPSocket.new(family)
|
35
|
+
@socket.connect(host, port)
|
36
|
+
end
|
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.
|
42
|
+
def send_message(message)
|
43
|
+
connect unless @socket
|
44
|
+
@socket.send(message, 0)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'connection'
|
4
|
+
|
5
|
+
module Datadog
|
6
|
+
class Statsd
|
7
|
+
class UDSConnection < Connection
|
8
|
+
class BadSocketError < StandardError; end
|
9
|
+
|
10
|
+
# DogStatsd unix socket path
|
11
|
+
attr_reader :socket_path
|
12
|
+
|
13
|
+
def initialize(socket_path, **kwargs)
|
14
|
+
super(**kwargs)
|
15
|
+
|
16
|
+
@socket_path = socket_path
|
17
|
+
@socket = nil
|
18
|
+
end
|
19
|
+
|
20
|
+
def close
|
21
|
+
@socket.close if @socket
|
22
|
+
@socket = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def connect
|
28
|
+
close if @socket
|
29
|
+
|
30
|
+
@socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
|
31
|
+
@socket.connect(Socket.pack_sockaddr_un(@socket_path))
|
32
|
+
end
|
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.
|
38
|
+
def send_message(message)
|
39
|
+
connect unless @socket
|
40
|
+
@socket.sendmsg_nonblock(message)
|
41
|
+
rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
|
42
|
+
# TODO: FIXME: This error should be considered as a retryable error in the
|
43
|
+
# Connection class. An even better solution would be to make BadSocketError inherit
|
44
|
+
# from a specific retryable error class in the Connection class.
|
45
|
+
raise BadSocketError, "#{e.class}: #{e}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|