dogstatsd-ruby 4.8.2 → 5.4.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,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
- raise "Event #{title} payload is too big (more that 8KB), event discarded"
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
- @global_tags_formatted = @global_tags.join(',') if @global_tags.any?
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 :estimate_max_size
16
+ attr_reader :packets_dropped_queue
17
+ attr_reader :packets_dropped_writer
15
18
 
16
- def initialize(disabled, flush_interval, global_tags: [], transport_type: :udp)
17
- @disabled = disabled
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
- # estimate_max_size is an estimation or the maximum size of the
32
- # telemetry payload. Since we don't want our packet to go over
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 dropped(bytes: 0, packets: 0)
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 flush?
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
- return '' if @disabled
72
-
73
- # using shorthand syntax to reduce the garbage collection
74
- %Q(
75
- datadog.dogstatsd.client.metrics:#{@metrics}|#{COUNTER_TYPE}|##{serialized_tags}
76
- datadog.dogstatsd.client.events:#{@events}|#{COUNTER_TYPE}|##{serialized_tags}
77
- datadog.dogstatsd.client.service_checks:#{@service_checks}|#{COUNTER_TYPE}|##{serialized_tags}
78
- datadog.dogstatsd.client.bytes_sent:#{@bytes_sent}|#{COUNTER_TYPE}|##{serialized_tags}
79
- datadog.dogstatsd.client.bytes_dropped:#{@bytes_dropped}|#{COUNTER_TYPE}|##{serialized_tags}
80
- datadog.dogstatsd.client.packets_sent:#{@packets_sent}|#{COUNTER_TYPE}|##{serialized_tags}
81
- datadog.dogstatsd.client.packets_dropped:#{@packets_dropped}|#{COUNTER_TYPE}|##{serialized_tags})
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
- DEFAULT_HOST = '127.0.0.1'
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. Defaults to 8125.
11
+ # StatsD port.
15
12
  attr_reader :port
16
13
 
17
- def initialize(host, port, logger, telemetry)
18
- super(telemetry)
19
- @host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
20
- @port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT).to_i
21
- @logger = logger
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
- UDPSocket.new.tap do |socket|
28
- socket.connect(host, port)
29
- end
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.send(message, 0)
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, logger, telemetry)
14
- super(telemetry)
13
+ def initialize(socket_path, **kwargs)
14
+ super(**kwargs)
15
+
15
16
  @socket_path = socket_path
16
- @logger = logger
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
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
23
- socket.connect(Socket.pack_sockaddr_un(@socket_path))
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.sendmsg_nonblock(message)
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.
@@ -4,6 +4,6 @@ require_relative 'connection'
4
4
 
5
5
  module Datadog
6
6
  class Statsd
7
- VERSION = '4.8.2'
7
+ VERSION = '5.4.0'
8
8
  end
9
9
  end