dogstatsd-ruby 4.0.0 → 5.2.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/README.md +176 -56
- data/lib/datadog/statsd.rb +169 -333
- data/lib/datadog/statsd/connection.rb +59 -0
- data/lib/datadog/statsd/forwarder.rb +122 -0
- data/lib/datadog/statsd/message_buffer.rb +88 -0
- data/lib/datadog/statsd/sender.rb +117 -0
- data/lib/datadog/statsd/serialization.rb +15 -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/single_thread_sender.rb +31 -0
- data/lib/datadog/statsd/telemetry.rb +96 -0
- data/lib/datadog/statsd/udp_connection.rb +37 -0
- data/lib/datadog/statsd/uds_connection.rb +38 -0
- data/lib/datadog/statsd/version.rb +9 -0
- metadata +30 -10
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Connection
|
6
|
+
def initialize(telemetry: nil, logger: nil)
|
7
|
+
@telemetry = telemetry
|
8
|
+
@logger = logger
|
9
|
+
end
|
10
|
+
|
11
|
+
# Close the underlying socket
|
12
|
+
def close
|
13
|
+
begin
|
14
|
+
@socket && @socket.close if instance_variable_defined?(:@socket)
|
15
|
+
rescue StandardError => boom
|
16
|
+
logger.error { "Statsd: #{boom.class} #{boom}" } if logger
|
17
|
+
end
|
18
|
+
@socket = nil
|
19
|
+
end
|
20
|
+
|
21
|
+
def write(payload)
|
22
|
+
logger.debug { "Statsd: #{payload}" } if logger
|
23
|
+
|
24
|
+
send_message(payload)
|
25
|
+
|
26
|
+
telemetry.sent(packets: 1, bytes: payload.length) if telemetry
|
27
|
+
|
28
|
+
true
|
29
|
+
rescue StandardError => boom
|
30
|
+
# Try once to reconnect if the socket has been closed
|
31
|
+
retries ||= 1
|
32
|
+
if retries <= 1 &&
|
33
|
+
(boom.is_a?(Errno::ENOTCONN) or
|
34
|
+
boom.is_a?(Errno::ECONNREFUSED) or
|
35
|
+
boom.is_a?(IOError) && boom.message =~ /closed stream/i)
|
36
|
+
retries += 1
|
37
|
+
begin
|
38
|
+
close
|
39
|
+
retry
|
40
|
+
rescue StandardError => e
|
41
|
+
boom = e
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
telemetry.dropped(packets: 1, bytes: payload.length) if telemetry
|
46
|
+
logger.error { "Statsd: #{boom.class} #{boom}" } if logger
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
attr_reader :telemetry
|
52
|
+
attr_reader :logger
|
53
|
+
|
54
|
+
def socket
|
55
|
+
@socket ||= connect
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Forwarder
|
6
|
+
attr_reader :telemetry
|
7
|
+
attr_reader :transport_type
|
8
|
+
|
9
|
+
def initialize(
|
10
|
+
host: nil,
|
11
|
+
port: nil,
|
12
|
+
socket_path: nil,
|
13
|
+
|
14
|
+
buffer_max_payload_size: nil,
|
15
|
+
buffer_max_pool_size: nil,
|
16
|
+
buffer_overflowing_stategy: :drop,
|
17
|
+
|
18
|
+
telemetry_flush_interval: nil,
|
19
|
+
global_tags: [],
|
20
|
+
|
21
|
+
single_thread: false,
|
22
|
+
|
23
|
+
logger: nil
|
24
|
+
)
|
25
|
+
@transport_type = socket_path.nil? ? :udp : :uds
|
26
|
+
|
27
|
+
if telemetry_flush_interval
|
28
|
+
@telemetry = Telemetry.new(telemetry_flush_interval,
|
29
|
+
global_tags: global_tags,
|
30
|
+
transport_type: transport_type
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
@connection = case transport_type
|
35
|
+
when :udp
|
36
|
+
UDPConnection.new(host, port, logger: logger, telemetry: telemetry)
|
37
|
+
when :uds
|
38
|
+
UDSConnection.new(socket_path, logger: logger, telemetry: telemetry)
|
39
|
+
end
|
40
|
+
|
41
|
+
# Initialize buffer
|
42
|
+
buffer_max_payload_size ||= (transport_type == :udp ? UDP_DEFAULT_BUFFER_SIZE : UDS_DEFAULT_BUFFER_SIZE)
|
43
|
+
|
44
|
+
if buffer_max_payload_size <= 0
|
45
|
+
raise ArgumentError, 'buffer_max_payload_size cannot be <= 0'
|
46
|
+
end
|
47
|
+
|
48
|
+
unless telemetry.nil? || telemetry.would_fit_in?(buffer_max_payload_size)
|
49
|
+
raise ArgumentError, "buffer_max_payload_size is not high enough to use telemetry (tags=(#{global_tags.inspect}))"
|
50
|
+
end
|
51
|
+
|
52
|
+
@buffer = MessageBuffer.new(@connection,
|
53
|
+
max_payload_size: buffer_max_payload_size,
|
54
|
+
max_pool_size: buffer_max_pool_size || DEFAULT_BUFFER_POOL_SIZE,
|
55
|
+
overflowing_stategy: buffer_overflowing_stategy,
|
56
|
+
)
|
57
|
+
|
58
|
+
@sender = single_thread ? SingleThreadSender.new(buffer) : Sender.new(buffer)
|
59
|
+
@sender.start
|
60
|
+
end
|
61
|
+
|
62
|
+
def send_message(message)
|
63
|
+
sender.add(message)
|
64
|
+
|
65
|
+
tick_telemetry
|
66
|
+
end
|
67
|
+
|
68
|
+
def sync_with_outbound_io
|
69
|
+
sender.rendez_vous
|
70
|
+
end
|
71
|
+
|
72
|
+
def flush(flush_telemetry: false, sync: false)
|
73
|
+
do_flush_telemetry if telemetry && flush_telemetry
|
74
|
+
|
75
|
+
sender.flush(sync: sync)
|
76
|
+
end
|
77
|
+
|
78
|
+
def host
|
79
|
+
return nil unless transport_type == :udp
|
80
|
+
|
81
|
+
connection.host
|
82
|
+
end
|
83
|
+
|
84
|
+
def port
|
85
|
+
return nil unless transport_type == :udp
|
86
|
+
|
87
|
+
connection.port
|
88
|
+
end
|
89
|
+
|
90
|
+
def socket_path
|
91
|
+
return nil unless transport_type == :uds
|
92
|
+
|
93
|
+
connection.socket_path
|
94
|
+
end
|
95
|
+
|
96
|
+
def close
|
97
|
+
sender.stop
|
98
|
+
connection.close
|
99
|
+
end
|
100
|
+
|
101
|
+
private
|
102
|
+
attr_reader :buffer
|
103
|
+
attr_reader :sender
|
104
|
+
attr_reader :connection
|
105
|
+
|
106
|
+
def do_flush_telemetry
|
107
|
+
telemetry_snapshot = telemetry.flush
|
108
|
+
telemetry.reset
|
109
|
+
|
110
|
+
telemetry_snapshot.each do |message|
|
111
|
+
sender.add(message)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def tick_telemetry
|
116
|
+
return nil unless telemetry
|
117
|
+
|
118
|
+
do_flush_telemetry if telemetry.should_flush?
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class MessageBuffer
|
6
|
+
PAYLOAD_SIZE_TOLERANCE = 0.05
|
7
|
+
|
8
|
+
def initialize(connection,
|
9
|
+
max_payload_size: nil,
|
10
|
+
max_pool_size: DEFAULT_BUFFER_POOL_SIZE,
|
11
|
+
overflowing_stategy: :drop
|
12
|
+
)
|
13
|
+
raise ArgumentError, 'max_payload_size keyword argument must be provided' unless max_payload_size
|
14
|
+
raise ArgumentError, 'max_pool_size keyword argument must be provided' unless max_pool_size
|
15
|
+
|
16
|
+
@connection = connection
|
17
|
+
@max_payload_size = max_payload_size
|
18
|
+
@max_pool_size = max_pool_size
|
19
|
+
@overflowing_stategy = overflowing_stategy
|
20
|
+
|
21
|
+
@buffer = String.new
|
22
|
+
@message_count = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
def add(message)
|
26
|
+
message_size = message.bytesize
|
27
|
+
|
28
|
+
return nil unless message_size > 0 # to avoid adding empty messages to the buffer
|
29
|
+
return nil unless ensure_sendable!(message_size)
|
30
|
+
|
31
|
+
flush if should_flush?(message_size)
|
32
|
+
|
33
|
+
buffer << "\n" unless buffer.empty?
|
34
|
+
buffer << message
|
35
|
+
|
36
|
+
@message_count += 1
|
37
|
+
|
38
|
+
# flush when we're pretty sure that we won't be able
|
39
|
+
# to add another message to the buffer
|
40
|
+
flush if preemptive_flush?
|
41
|
+
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def flush
|
46
|
+
return if buffer.empty?
|
47
|
+
|
48
|
+
connection.write(buffer)
|
49
|
+
|
50
|
+
buffer.clear
|
51
|
+
@message_count = 0
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
attr :max_payload_size
|
56
|
+
attr :max_pool_size
|
57
|
+
|
58
|
+
attr :overflowing_stategy
|
59
|
+
|
60
|
+
attr :connection
|
61
|
+
attr :buffer
|
62
|
+
|
63
|
+
def should_flush?(message_size)
|
64
|
+
return true if buffer.bytesize + 1 + message_size >= max_payload_size
|
65
|
+
|
66
|
+
false
|
67
|
+
end
|
68
|
+
|
69
|
+
def preemptive_flush?
|
70
|
+
@message_count == max_pool_size || buffer.bytesize > bytesize_threshold
|
71
|
+
end
|
72
|
+
|
73
|
+
def ensure_sendable!(message_size)
|
74
|
+
return true if message_size <= max_payload_size
|
75
|
+
|
76
|
+
if overflowing_stategy == :raise
|
77
|
+
raise Error, 'Message too big for payload limit'
|
78
|
+
end
|
79
|
+
|
80
|
+
false
|
81
|
+
end
|
82
|
+
|
83
|
+
def bytesize_threshold
|
84
|
+
@bytesize_threshold ||= (max_payload_size - PAYLOAD_SIZE_TOLERANCE * max_payload_size).to_i
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
class Sender
|
6
|
+
CLOSEABLE_QUEUES = Queue.instance_methods.include?(:close)
|
7
|
+
|
8
|
+
def initialize(message_buffer)
|
9
|
+
@message_buffer = message_buffer
|
10
|
+
end
|
11
|
+
|
12
|
+
def flush(sync: false)
|
13
|
+
# don't try to flush if there is no message_queue instantiated
|
14
|
+
return unless message_queue
|
15
|
+
|
16
|
+
message_queue.push(:flush)
|
17
|
+
|
18
|
+
rendez_vous if sync
|
19
|
+
end
|
20
|
+
|
21
|
+
def rendez_vous
|
22
|
+
# Initialize and get the thread's sync queue
|
23
|
+
queue = (Thread.current[:statsd_sync_queue] ||= Queue.new)
|
24
|
+
# tell sender-thread to notify us in the current
|
25
|
+
# thread's queue
|
26
|
+
message_queue.push(queue)
|
27
|
+
# wait for the sender thread to send a message
|
28
|
+
# once the flush is done
|
29
|
+
queue.pop
|
30
|
+
end
|
31
|
+
|
32
|
+
def add(message)
|
33
|
+
raise ArgumentError, 'Start sender first' unless message_queue
|
34
|
+
|
35
|
+
message_queue << message
|
36
|
+
end
|
37
|
+
|
38
|
+
def start
|
39
|
+
raise ArgumentError, 'Sender already started' if message_queue
|
40
|
+
|
41
|
+
# initialize message queue for background thread
|
42
|
+
@message_queue = Queue.new
|
43
|
+
# start background thread
|
44
|
+
@sender_thread = Thread.new(&method(:send_loop))
|
45
|
+
end
|
46
|
+
|
47
|
+
if CLOSEABLE_QUEUES
|
48
|
+
def stop(join_worker: true)
|
49
|
+
message_queue = @message_queue
|
50
|
+
message_queue.close if message_queue
|
51
|
+
|
52
|
+
sender_thread = @sender_thread
|
53
|
+
sender_thread.join if sender_thread && join_worker
|
54
|
+
end
|
55
|
+
else
|
56
|
+
def stop(join_worker: true)
|
57
|
+
message_queue = @message_queue
|
58
|
+
message_queue << :close if message_queue
|
59
|
+
|
60
|
+
sender_thread = @sender_thread
|
61
|
+
sender_thread.join if sender_thread && join_worker
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
attr_reader :message_buffer
|
68
|
+
|
69
|
+
attr_reader :message_queue
|
70
|
+
attr_reader :sender_thread
|
71
|
+
|
72
|
+
if CLOSEABLE_QUEUES
|
73
|
+
def send_loop
|
74
|
+
until (message = message_queue.pop).nil? && message_queue.closed?
|
75
|
+
# skip if message is nil, e.g. when message_queue
|
76
|
+
# is empty and closed
|
77
|
+
next unless message
|
78
|
+
|
79
|
+
case message
|
80
|
+
when :flush
|
81
|
+
message_buffer.flush
|
82
|
+
when Queue
|
83
|
+
message.push(:go_on)
|
84
|
+
else
|
85
|
+
message_buffer.add(message)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
@message_queue = nil
|
90
|
+
@sender_thread = nil
|
91
|
+
end
|
92
|
+
else
|
93
|
+
def send_loop
|
94
|
+
loop do
|
95
|
+
message = message_queue.pop
|
96
|
+
|
97
|
+
next unless message
|
98
|
+
|
99
|
+
case message
|
100
|
+
when :close
|
101
|
+
break
|
102
|
+
when :flush
|
103
|
+
message_buffer.flush
|
104
|
+
when Queue
|
105
|
+
message.push(:go_on)
|
106
|
+
else
|
107
|
+
message_buffer.add(message)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
@message_queue = nil
|
112
|
+
@sender_thread = nil
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
end
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
require_relative 'serialization/tag_serializer'
|
11
|
+
require_relative 'serialization/service_check_serializer'
|
12
|
+
require_relative 'serialization/event_serializer'
|
13
|
+
require_relative 'serialization/stat_serializer'
|
14
|
+
|
15
|
+
require_relative 'serialization/serializer'
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Datadog
|
4
|
+
class Statsd
|
5
|
+
module Serialization
|
6
|
+
class EventSerializer
|
7
|
+
EVENT_BASIC_OPTIONS = {
|
8
|
+
date_happened: 'd:',
|
9
|
+
hostname: 'h:',
|
10
|
+
aggregation_key: 'k:',
|
11
|
+
priority: 'p:',
|
12
|
+
source_type_name: 's:',
|
13
|
+
alert_type: 't:',
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
def initialize(global_tags: [])
|
17
|
+
@tag_serializer = TagSerializer.new(global_tags)
|
18
|
+
end
|
19
|
+
|
20
|
+
def format(title, text, options = EMPTY_OPTIONS)
|
21
|
+
title = escape(title)
|
22
|
+
text = escape(text)
|
23
|
+
|
24
|
+
String.new.tap do |event|
|
25
|
+
event << '_e{'
|
26
|
+
event << title.bytesize.to_s
|
27
|
+
event << ','
|
28
|
+
event << text.bytesize.to_s
|
29
|
+
event << '}:'
|
30
|
+
event << title
|
31
|
+
event << '|'
|
32
|
+
event << text
|
33
|
+
|
34
|
+
# we are serializing the generic service check options
|
35
|
+
# before serializing specialized options that need edge-cases
|
36
|
+
EVENT_BASIC_OPTIONS.each do |option_key, shortcut|
|
37
|
+
if value = options[option_key]
|
38
|
+
event << '|'
|
39
|
+
event << shortcut
|
40
|
+
event << value.to_s.delete('|')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# also returns the global tags from serializer
|
45
|
+
if tags = tag_serializer.format(options[:tags])
|
46
|
+
event << '|#'
|
47
|
+
event << tags
|
48
|
+
end
|
49
|
+
|
50
|
+
if event.bytesize > MAX_EVENT_SIZE
|
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
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
protected
|
61
|
+
attr_reader :tag_serializer
|
62
|
+
|
63
|
+
def escape(text)
|
64
|
+
text.delete('|').tap do |t|
|
65
|
+
t.gsub!("\n", '\n')
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|