dogstatsd-ruby 4.7.0 → 5.0.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.
@@ -3,30 +3,29 @@
3
3
  module Datadog
4
4
  class Statsd
5
5
  class Connection
6
- def initialize(telemetry)
6
+ def initialize(telemetry: nil, logger: nil)
7
7
  @telemetry = telemetry
8
+ @logger = logger
8
9
  end
9
10
 
10
11
  # Close the underlying socket
11
12
  def close
12
- @socket && @socket.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
13
19
  end
14
20
 
15
21
  def write(payload)
16
22
  logger.debug { "Statsd: #{payload}" } if logger
17
- flush_telemetry = @telemetry.flush?
18
- if flush_telemetry
19
- payload += @telemetry.flush()
20
- end
21
23
 
22
24
  send_message(payload)
23
25
 
24
- if flush_telemetry
25
- @telemetry.reset
26
- end
26
+ telemetry.sent(packets: 1, bytes: payload.length) if telemetry
27
27
 
28
- telemetry.bytes_sent += payload.length
29
- telemetry.packets_sent += 1
28
+ true
30
29
  rescue StandardError => boom
31
30
  # Try once to reconnect if the socket has been closed
32
31
  retries ||= 1
@@ -36,21 +35,19 @@ module Datadog
36
35
  boom.is_a?(IOError) && boom.message =~ /closed stream/i)
37
36
  retries += 1
38
37
  begin
39
- @socket = connect
38
+ close
40
39
  retry
41
40
  rescue StandardError => e
42
41
  boom = e
43
42
  end
44
43
  end
45
44
 
46
- telemetry.bytes_dropped += payload.length
47
- telemetry.packets_dropped += 1
45
+ telemetry.dropped(packets: 1, bytes: payload.length) if telemetry
48
46
  logger.error { "Statsd: #{boom.class} #{boom}" } if logger
49
47
  nil
50
48
  end
51
49
 
52
50
  private
53
-
54
51
  attr_reader :telemetry
55
52
  attr_reader :logger
56
53
 
@@ -0,0 +1,120 @@
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
+ logger: nil
22
+ )
23
+ @transport_type = socket_path.nil? ? :udp : :uds
24
+
25
+ if telemetry_flush_interval
26
+ @telemetry = Telemetry.new(telemetry_flush_interval,
27
+ global_tags: global_tags,
28
+ transport_type: transport_type
29
+ )
30
+ end
31
+
32
+ @connection = case transport_type
33
+ when :udp
34
+ UDPConnection.new(host, port, logger: logger, telemetry: telemetry)
35
+ when :uds
36
+ UDSConnection.new(socket_path, logger: logger, telemetry: telemetry)
37
+ end
38
+
39
+ # Initialize buffer
40
+ buffer_max_payload_size ||= (transport_type == :udp ? UDP_DEFAULT_BUFFER_SIZE : UDS_DEFAULT_BUFFER_SIZE)
41
+
42
+ if buffer_max_payload_size <= 0
43
+ raise ArgumentError, 'buffer_max_payload_size cannot be <= 0'
44
+ end
45
+
46
+ unless telemetry.nil? || telemetry.would_fit_in?(buffer_max_payload_size)
47
+ raise ArgumentError, "buffer_max_payload_size is not high enough to use telemetry (tags=(#{global_tags.inspect}))"
48
+ end
49
+
50
+ @buffer = MessageBuffer.new(@connection,
51
+ max_payload_size: buffer_max_payload_size,
52
+ max_pool_size: buffer_max_pool_size || DEFAULT_BUFFER_POOL_SIZE,
53
+ overflowing_stategy: buffer_overflowing_stategy,
54
+ )
55
+
56
+ @sender = Sender.new(buffer)
57
+ @sender.start
58
+ end
59
+
60
+ def send_message(message)
61
+ sender.add(message)
62
+
63
+ tick_telemetry
64
+ end
65
+
66
+ def sync_with_outbound_io
67
+ sender.rendez_vous
68
+ end
69
+
70
+ def flush(flush_telemetry: false, sync: false)
71
+ do_flush_telemetry if telemetry && flush_telemetry
72
+
73
+ sender.flush(sync: sync)
74
+ end
75
+
76
+ def host
77
+ return nil unless transport_type == :udp
78
+
79
+ connection.host
80
+ end
81
+
82
+ def port
83
+ return nil unless transport_type == :udp
84
+
85
+ connection.port
86
+ end
87
+
88
+ def socket_path
89
+ return nil unless transport_type == :uds
90
+
91
+ connection.socket_path
92
+ end
93
+
94
+ def close
95
+ sender.stop
96
+ connection.close
97
+ end
98
+
99
+ private
100
+ attr_reader :buffer
101
+ attr_reader :sender
102
+ attr_reader :connection
103
+
104
+ def do_flush_telemetry
105
+ telemetry_snapshot = telemetry.flush
106
+ telemetry.reset
107
+
108
+ telemetry_snapshot.each do |message|
109
+ sender.add(message)
110
+ end
111
+ end
112
+
113
+ def tick_telemetry
114
+ return nil unless telemetry
115
+
116
+ do_flush_telemetry if telemetry.should_flush?
117
+ end
118
+ end
119
+ end
120
+ 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,110 @@
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
+ raise ArgumentError, 'Start sender first' unless message_queue
14
+
15
+ message_queue.push(:flush)
16
+
17
+ rendez_vous if sync
18
+ end
19
+
20
+ def rendez_vous
21
+ # Initialize and get the thread's sync queue
22
+ queue = (Thread.current[:statsd_sync_queue] ||= Queue.new)
23
+ # tell sender-thread to notify us in the current
24
+ # thread's queue
25
+ message_queue.push(queue)
26
+ # wait for the sender thread to send a message
27
+ # once the flush is done
28
+ queue.pop
29
+ end
30
+
31
+ def add(message)
32
+ raise ArgumentError, 'Start sender first' unless message_queue
33
+
34
+ message_queue << message
35
+ end
36
+
37
+ def start
38
+ raise ArgumentError, 'Sender already started' if message_queue
39
+
40
+ # initialize message queue for background thread
41
+ @message_queue = Queue.new
42
+ # start background thread
43
+ @sender_thread = Thread.new(&method(:send_loop))
44
+ end
45
+
46
+ if CLOSEABLE_QUEUES
47
+ def stop(join_worker: true)
48
+ message_queue.close if message_queue
49
+ sender_thread.join if sender_thread && join_worker
50
+ end
51
+ else
52
+ def stop(join_worker: true)
53
+ message_queue << :close if message_queue
54
+ sender_thread.join if sender_thread && join_worker
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ attr_reader :message_buffer
61
+
62
+ attr_reader :message_queue
63
+ attr_reader :sender_thread
64
+
65
+ if CLOSEABLE_QUEUES
66
+ def send_loop
67
+ until (message = message_queue.pop).nil? && message_queue.closed?
68
+ # skip if message is nil, e.g. when message_queue
69
+ # is empty and closed
70
+ next unless message
71
+
72
+ case message
73
+ when :flush
74
+ message_buffer.flush
75
+ when Queue
76
+ message.push(:go_on)
77
+ else
78
+ message_buffer.add(message)
79
+ end
80
+ end
81
+
82
+ @message_queue = nil
83
+ @sender_thread = nil
84
+ end
85
+ else
86
+ def send_loop
87
+ loop do
88
+ message = message_queue.pop
89
+
90
+ next unless message
91
+
92
+ case message
93
+ when :close
94
+ break
95
+ when :flush
96
+ message_buffer.flush
97
+ when Queue
98
+ message.push(:go_on)
99
+ else
100
+ message_buffer.add(message)
101
+ end
102
+ end
103
+
104
+ @message_queue = nil
105
+ @sender_thread = nil
106
+ end
107
+ end
108
+ end
109
+ end
110
+ 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