dogstatsd-ruby 4.8.2 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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