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.
@@ -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