dogstatsd-ruby 4.8.2 → 5.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dca98cecf21fceb3e5a4c4e76ceb8be8ffa2032ad317d63d1b3df7817dd60ef7
4
- data.tar.gz: 0ef534d77693c3f1efbd8e8c8e1399f54ee6664e1044a16c913e4f9fa302e3d7
3
+ metadata.gz: 9abfaabd9be58a320dd8cc8d1d4998ac287daa08e68ab894054cf2910f5f3133
4
+ data.tar.gz: 88ebf4ef472f7663897c06fa3e5753a17c12722bc1a58f8aa4e9fb0a3ace07c6
5
5
  SHA512:
6
- metadata.gz: d95681cd4c29de465efb5f6429227716d5cf4c6d1c57d1d5f6ba06cae2297b6ac06a961763a800b7997e6bf0011fa6f922826fdea7842fde536943c151c8e050
7
- data.tar.gz: 0b560868db6815d18283ae938536c234d169e5aba4e1d602ee97759b36ad19fab626297b56cecf07b426c14de211e4bab43a010474e51a347387f82cd0c9cabe
6
+ metadata.gz: 199527bd5e9d39d94be3cabf28dc5f4d8913c4d3f746a6018a22c9ed13440c34d77fc11b363f47f9e2fd325974005fdc3567a223de4669b388ced99c3cd0750f
7
+ data.tar.gz: 0da25bafef540e4d36be8cae9c1fbb1f11c6aea94415828941aa9c34877db75166574f41b5e1a0829c71285f958162133d0f592a34619ce066e14c97ea86fd4d
data/README.md CHANGED
@@ -24,15 +24,88 @@ require 'datadog/statsd'
24
24
 
25
25
  # Create a DogStatsD client instance.
26
26
  statsd = Datadog::Statsd.new('localhost', 8125)
27
+ ...
28
+ # release resources used by the client instance
29
+ statsd.close()
27
30
  ```
28
31
  Or if you want to connect over Unix Domain Socket:
29
32
  ```ruby
30
33
  # Connection over Unix Domain Socket
31
34
  statsd = Datadog::Statsd.new(socket_path: '/path/to/socket/file')
35
+ ...
36
+ # release resources used by the client instance
37
+ statsd.close()
32
38
  ```
33
39
 
34
40
  Find a list of all the available options for your DogStatsD Client in the [DogStatsD-ruby rubydoc](https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/Datadog/Statsd) or in the [Datadog public DogStatsD documentation](https://docs.datadoghq.com/developers/dogstatsd/?tab=ruby#client-instantiation-parameters).
35
41
 
42
+ ### Migrating from v4.x to v5.x
43
+
44
+ If you are already using DogStatsD-ruby v4.x and you want to migrate to a version v5.x, the major
45
+ change concerning you is the new threading model (please see section Threading model):
46
+
47
+ In practice, it means two things:
48
+
49
+ 1. Now that the client is buffering metrics before sending them, you have to manually
50
+ call the method `Datadog::Statsd#flush` if you want to force the sending of metrics. Note that the companion thread will automatically flush the buffered metrics if the buffer gets full or when you are closing the instance.
51
+
52
+ 2. You have to make sure you are either:
53
+
54
+ * using singletons instances of the DogStatsD client and not allocating one each time you need one, letting the buffering mechanism flush metrics, it's still a bad solution if the process later forks (see related section below). Or,
55
+ * properly closing your DogStatsD client instance when it is not needed anymore using the method `Datadog::Statsd#close` to release the resources used by the instance and to close the socket
56
+
57
+ If you have issues with the companion thread or the buffering mode, you can instantiate a client that behaves exactly as in v4.x (i.e. no companion thread and flush on every metric submission):
58
+
59
+ ```ruby
60
+ # Import the library
61
+ require 'datadog/statsd'
62
+
63
+ # Create a DogStatsD client instance using UDP
64
+ statsd = Datadog::Statsd.new('localhost', 8125, single_thread: true, buffer_max_payload_size: 1)
65
+ ...
66
+ # to close the instance is not necessary in this case since metrics are flushed on submission
67
+ # but it is still a good practice and it explicitely closes the socket
68
+ statsd.close()
69
+ ```
70
+
71
+ or
72
+
73
+ ```ruby
74
+ # Import the library
75
+ require 'datadog/statsd'
76
+
77
+ # Create a DogStatsD client instance using UDS
78
+ statsd = Datadog::Statsd.new(socket_path: '/path/to/socket/file', single_thread: true, buffer_max_payload_size: 1)
79
+ ...
80
+ # to close the instance is not necessary in this case since metrics are flushed on submission
81
+ # but it is still a good practice and it explicitely closes the socket
82
+ statsd.close()
83
+ ```
84
+
85
+ ### v5.x Common Pitfalls
86
+
87
+ Version v5.x of `dogstatsd-ruby` is using a companion thread for preemptive flushing, it brings better performances for application having a high-throughput of statsd metrics, but it comes with new pitfalls:
88
+
89
+ * Applications forking after having created the dogstatsd instance: forking a process can't duplicate the existing threads, meaning that one of the processes won't have a companion thread to flush the metrics and will lead to missing metrics.
90
+ * Applications creating a lot of different instances of the client without closing them: it is important to close the instance to free the thread and the socket it is using or it will lead to thread leaks.
91
+
92
+ If you are using [Sidekiq](https://github.com/mperham/sidekiq), please make sure to close the client instances that are instantiated. [See this example on using DogStatsD-ruby v5.x with Sidekiq](https://github.com/DataDog/dogstatsd-ruby/blob/master/examples/sidekiq_example.rb).
93
+
94
+ If you are using [Puma](https://github.com/puma/puma) or [Unicorn](https://yhbt.net/unicorn.git), please make sure to create the instance of DogStatsD in the workers, not in the main process before it forks to create its workers. See [this comment for more details](https://github.com/DataDog/dogstatsd-ruby/issues/179#issuecomment-845570345).
95
+
96
+ Applications that are in these situations but can't apply these recommendations should enable the `single_thread` mode which does not use a companion thread. Here is how to instantiate a client in this mode:
97
+
98
+ ```ruby
99
+ # Import the library
100
+ require 'datadog/statsd'
101
+
102
+ # Create a DogStatsD client instance.
103
+ statsd = Datadog::Statsd.new('localhost', 8125, single_thread: true)
104
+ ...
105
+ # release resources used by the client instance and flush last metrics
106
+ statsd.close()
107
+ ```
108
+
36
109
  ### Origin detection over UDP
37
110
 
38
111
  Origin detection is a method to detect which pod DogStatsD packets are coming from in order to add the pod's tags to the tag list.
@@ -84,9 +157,61 @@ size should be used, you can set it with the parameter `buffer_max_payload_size`
84
157
  statsd = Datadog::Statsd.new('localhost', 8125, buffer_max_payload_size: 4096)
85
158
  ```
86
159
 
160
+ ## Threading model
161
+
162
+ On versions greater than 5.0, we changed the threading model of the library so that one instance of `Datadog::Statsd` could be shared between threads and so that the writes in the socket are non blocking.
163
+
164
+ When you instantiate a `Datadog::Statsd`, a companion thread is spawned. This thread will be called the Sender thread, as it is modeled by the [Sender](../lib/datadog/statsd/sender.rb) class. Please use `single_thread: true` while creating an instance if you don't want to or can't use a companion thread.
165
+
166
+ This thread is stopped when you close the statsd client (`Datadog::Statsd#close`). It also means that allocating a lot of statsd clients without closing them properly when not used anymore
167
+ could lead to a thread leak (even though they will be sleeping, blocked on IO).
168
+ The communication between the current thread is managed through a standard Ruby Queue.
169
+
170
+ The sender thread has the following logic (Code present in the method `Datadog::Statsd::Sender#send_loop`):
171
+
172
+ ```
173
+ while the sender message queue is not closed do
174
+ read message from sender message queue
175
+
176
+ if message is a Control message to flush
177
+ flush buffer in connection
178
+ else if message is a Control message to synchronize
179
+ synchronize with calling thread
180
+ else
181
+ add message to the buffer
182
+ end
183
+ end while
184
+ ```
185
+
186
+ Most of the time, the sender thread is blocked and sleeping when doing a blocking read from the sender message queue.
187
+
188
+ We can see that there is 3 different kind of messages:
189
+
190
+ * a control message to flush the buffer in the connection
191
+ * a control message to synchronize any thread with the sender thread
192
+ * a message to append to the buffer
193
+
194
+ There is also an implicit message which is closing the queue as it will stop blocking read from the message queue (if happening) and thus, stop the sender thread.
195
+
196
+ ### Usual workflow
197
+
198
+ You push metrics to the statsd client which writes them quickly to the sender message queue. The sender thread receives those message, buffers them and flushes them to the connection when the buffer limit is reached.
199
+
200
+ ### Flushing
201
+
202
+ When calling a flush, a specific control message (the `:flush` symbol) is sent to the sender thread. When finding it, it flushes its internal buffer into the connection.
203
+
204
+ ### Rendez-vous
205
+
206
+ It is possible to ensure a message has been consumed by the sender thread and written to the buffer by simply calling a rendez-vous right after. This is done when you are doing a synchronized flush (calling `Datadog::Statsd#flush` with the `sync: true` option).
207
+
208
+ This means the current thread is going to sleep and wait for a Queue which is given to the sender thread. When the sender thread reads this queue from its own message queue, it puts a placeholder message in it so that it wakes up the calling thread.
209
+
210
+ This is useful when closing the application or when checking unit tests.
211
+
87
212
  ## Credits
88
213
 
89
- dogstatsd-ruby is forked from Rien Henrichs [original Statsd
214
+ dogstatsd-ruby is forked from Rein Henrichs [original Statsd
90
215
  client](https://github.com/reinh/statsd).
91
216
 
92
217
  Copyright (c) 2011 Rein Henrichs. See LICENSE.txt for
@@ -5,8 +5,11 @@ require_relative 'statsd/version'
5
5
  require_relative 'statsd/telemetry'
6
6
  require_relative 'statsd/udp_connection'
7
7
  require_relative 'statsd/uds_connection'
8
- require_relative 'statsd/batch'
8
+ require_relative 'statsd/message_buffer'
9
9
  require_relative 'statsd/serialization'
10
+ require_relative 'statsd/sender'
11
+ require_relative 'statsd/single_thread_sender'
12
+ require_relative 'statsd/forwarder'
10
13
 
11
14
  # = Datadog::Statsd: A DogStatsd client (https://www.datadoghq.com)
12
15
  #
@@ -26,12 +29,17 @@ require_relative 'statsd/serialization'
26
29
  # statsd = Datadog::Statsd.new 'localhost', 8125, tags: 'tag1:true'
27
30
  module Datadog
28
31
  class Statsd
32
+ class Error < StandardError
33
+ end
34
+
29
35
  OK = 0
30
36
  WARNING = 1
31
37
  CRITICAL = 2
32
38
  UNKNOWN = 3
33
39
 
34
- DEFAULT_BUFFER_SIZE = 8 * 1_024
40
+ UDP_DEFAULT_BUFFER_SIZE = 1_432
41
+ UDS_DEFAULT_BUFFER_SIZE = 8_192
42
+ DEFAULT_BUFFER_POOL_SIZE = Float::INFINITY
35
43
  MAX_EVENT_SIZE = 8 * 1_024
36
44
  # minimum flush interval for the telemetry in seconds
37
45
  DEFAULT_TELEMETRY_FLUSH_INTERVAL = 10
@@ -51,67 +59,64 @@ module Datadog
51
59
  serializer.global_tags
52
60
  end
53
61
 
54
- # Buffer containing the statsd message before they are sent in batch
55
- attr_reader :buffer
56
-
57
- # Maximum buffer size in bytes before it is flushed
58
- attr_reader :max_buffer_bytes
59
-
60
62
  # Default sample rate
61
63
  attr_reader :sample_rate
62
64
 
63
- # Connection
64
- attr_reader :connection
65
-
66
65
  # @param [String] host your statsd host
67
66
  # @param [Integer] port your statsd port
68
67
  # @option [String] namespace set a namespace to be prepended to every metric name
69
68
  # @option [Array<String>|Hash] tags tags to be added to every metric
70
69
  # @option [Logger] logger for debugging
71
- # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
70
+ # @option [Integer] buffer_max_payload_size max bytes to buffer
71
+ # @option [Integer] buffer_max_pool_size max messages to buffer
72
72
  # @option [String] socket_path unix socket path
73
73
  # @option [Float] default sample rate if not overridden
74
+ # @option [Boolean] single_thread flushes the metrics on the main thread instead of in a companion thread
74
75
  def initialize(
75
76
  host = nil,
76
77
  port = nil,
78
+ socket_path: nil,
79
+
77
80
  namespace: nil,
78
81
  tags: nil,
79
- max_buffer_bytes: DEFAULT_BUFFER_SIZE,
80
- socket_path: nil,
81
- logger: nil,
82
82
  sample_rate: nil,
83
- disable_telemetry: false,
83
+
84
+ buffer_max_payload_size: nil,
85
+ buffer_max_pool_size: nil,
86
+ buffer_overflowing_stategy: :drop,
87
+
88
+ logger: nil,
89
+
90
+ single_thread: false,
91
+
92
+ telemetry_enable: true,
84
93
  telemetry_flush_interval: DEFAULT_TELEMETRY_FLUSH_INTERVAL
85
94
  )
86
95
  unless tags.nil? || tags.is_a?(Array) || tags.is_a?(Hash)
87
- raise ArgumentError, 'tags must be a Array<String> or a Hash'
96
+ raise ArgumentError, 'tags must be an array of string tags or a Hash'
88
97
  end
89
98
 
90
99
  @namespace = namespace
91
100
  @prefix = @namespace ? "#{@namespace}.".freeze : nil
92
-
93
101
  @serializer = Serialization::Serializer.new(prefix: @prefix, global_tags: tags)
102
+ @sample_rate = sample_rate
94
103
 
95
- transport_type = socket_path.nil? ? :udp : :uds
104
+ @forwarder = Forwarder.new(
105
+ host: host,
106
+ port: port,
107
+ socket_path: socket_path,
96
108
 
97
- @telemetry = Telemetry.new(disable_telemetry, telemetry_flush_interval,
98
109
  global_tags: tags,
99
- transport_type: transport_type
100
- )
101
-
102
- @connection = case transport_type
103
- when :udp
104
- UDPConnection.new(host, port, logger, telemetry)
105
- when :uds
106
- UDSConnection.new(socket_path, logger, telemetry)
107
- end
110
+ logger: logger,
108
111
 
109
- @logger = logger
112
+ single_thread: single_thread,
110
113
 
111
- @sample_rate = sample_rate
114
+ buffer_max_payload_size: buffer_max_payload_size,
115
+ buffer_max_pool_size: buffer_max_pool_size,
116
+ buffer_overflowing_stategy: buffer_overflowing_stategy,
112
117
 
113
- # we reduce max_buffer_bytes by a the rough estimate of the telemetry payload
114
- @batch = Batch.new(connection, (max_buffer_bytes - telemetry.estimate_max_size))
118
+ telemetry_flush_interval: telemetry_enable ? telemetry_flush_interval : nil,
119
+ )
115
120
  end
116
121
 
117
122
  # yield a new instance to a block and close it when done
@@ -270,9 +275,9 @@ module Datadog
270
275
  # @example Report a critical service check status
271
276
  # $statsd.service_check('my.service.check', Statsd::CRITICAL, :tags=>['urgent'])
272
277
  def service_check(name, status, opts = EMPTY_OPTIONS)
273
- telemetry.sent(service_checks: 1)
278
+ telemetry.sent(service_checks: 1) if telemetry
274
279
 
275
- send_stat(serializer.to_service_check(name, status, opts))
280
+ forwarder.send_message(serializer.to_service_check(name, status, opts))
276
281
  end
277
282
 
278
283
  # This end point allows you to post events to the stream. You can tag them, set priority and even aggregate them with other events.
@@ -290,17 +295,25 @@ module Datadog
290
295
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
291
296
  # @option opts [String, nil] :source_type_name (nil) Assign a source type to the event
292
297
  # @option opts [String, nil] :alert_type ('info') Can be "error", "warning", "info" or "success".
298
+ # @option opts [Boolean, false] :truncate_if_too_long (false) Truncate the event if it is too long
293
299
  # @option opts [Array<String>] :tags tags to be added to every metric
294
300
  # @example Report an awful event:
295
301
  # $statsd.event('Something terrible happened', 'The end is near if we do nothing', :alert_type=>'warning', :tags=>['end_of_times','urgent'])
296
302
  def event(title, text, opts = EMPTY_OPTIONS)
297
- telemetry.sent(events: 1)
303
+ telemetry.sent(events: 1) if telemetry
298
304
 
299
- send_stat(serializer.to_event(title, text, opts))
305
+ forwarder.send_message(serializer.to_event(title, text, opts))
300
306
  end
301
307
 
302
- # Send several metrics in the same UDP Packet
303
- # They will be buffered and flushed when the block finishes
308
+ # Send several metrics in the same packet.
309
+ # They will be buffered and flushed when the block finishes.
310
+ #
311
+ # This method exists for compatibility with v4.x versions, it is not needed
312
+ # anymore since the batching is now automatically done internally.
313
+ # It also means that an automatic flush could occur if the buffer is filled
314
+ # during the execution of the batch block.
315
+ #
316
+ # This method is DEPRECATED and will be removed in future v6.x API.
304
317
  #
305
318
  # @example Send several metrics in one packet:
306
319
  # $statsd.batch do |s|
@@ -308,19 +321,50 @@ module Datadog
308
321
  # s.increment('page.views')
309
322
  # end
310
323
  def batch
311
- @batch.open do
312
- yield self
313
- end
324
+ yield self
325
+ flush(sync: true)
314
326
  end
315
327
 
316
328
  # Close the underlying socket
317
- def close
318
- connection.close
329
+ #
330
+ # @param [Boolean, true] flush Should we flush the metrics before closing
331
+ def close(flush: true)
332
+ flush(sync: true) if flush
333
+ forwarder.close
334
+ end
335
+
336
+ def sync_with_outbound_io
337
+ forwarder.sync_with_outbound_io
338
+ end
339
+
340
+ # Flush the buffer into the connection
341
+ def flush(flush_telemetry: false, sync: false)
342
+ forwarder.flush(flush_telemetry: flush_telemetry, sync: sync)
343
+ end
344
+
345
+ def telemetry
346
+ forwarder.telemetry
347
+ end
348
+
349
+ def host
350
+ forwarder.host
351
+ end
352
+
353
+ def port
354
+ forwarder.port
355
+ end
356
+
357
+ def socket_path
358
+ forwarder.socket_path
359
+ end
360
+
361
+ def transport_type
362
+ forwarder.transport_type
319
363
  end
320
364
 
321
365
  private
322
366
  attr_reader :serializer
323
- attr_reader :telemetry
367
+ attr_reader :forwarder
324
368
 
325
369
  PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= '2.1.0')
326
370
  EMPTY_OPTIONS = {}.freeze
@@ -336,22 +380,14 @@ module Datadog
336
380
  end
337
381
 
338
382
  def send_stats(stat, delta, type, opts = EMPTY_OPTIONS)
339
- telemetry.sent(metrics: 1)
383
+ telemetry.sent(metrics: 1) if telemetry
340
384
 
341
385
  sample_rate = opts[:sample_rate] || @sample_rate || 1
342
386
 
343
387
  if sample_rate == 1 || rand <= sample_rate
344
388
  full_stat = serializer.to_stat(stat, delta, type, tags: opts[:tags], sample_rate: sample_rate)
345
389
 
346
- send_stat(full_stat)
347
- end
348
- end
349
-
350
- def send_stat(message)
351
- if @batch.open?
352
- @batch.add(message)
353
- else
354
- @connection.write(message)
390
+ forwarder.send_message(full_stat)
355
391
  end
356
392
  end
357
393
  end
@@ -3,8 +3,9 @@
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
@@ -20,15 +21,11 @@ module Datadog
20
21
  def write(payload)
21
22
  logger.debug { "Statsd: #{payload}" } if logger
22
23
 
23
- flush_telemetry = telemetry.flush?
24
-
25
- payload += telemetry.flush if flush_telemetry
26
-
27
24
  send_message(payload)
28
25
 
29
- telemetry.reset if flush_telemetry
26
+ telemetry.sent(packets: 1, bytes: payload.length) if telemetry
30
27
 
31
- telemetry.sent(packets: 1, bytes: payload.length)
28
+ true
32
29
  rescue StandardError => boom
33
30
  # Try once to reconnect if the socket has been closed
34
31
  retries ||= 1
@@ -45,7 +42,7 @@ module Datadog
45
42
  end
46
43
  end
47
44
 
48
- telemetry.dropped(packets: 1, bytes: payload.length)
45
+ telemetry.dropped(packets: 1, bytes: payload.length) if telemetry
49
46
  logger.error { "Statsd: #{boom.class} #{boom}" } if logger
50
47
  nil
51
48
  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
@@ -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,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ class SingleThreadSender
6
+ def initialize(message_buffer)
7
+ @message_buffer = message_buffer
8
+ end
9
+
10
+ def add(message)
11
+ @message_buffer.add(message)
12
+ end
13
+
14
+ def flush(*)
15
+ @message_buffer.flush()
16
+ end
17
+
18
+ # Compatibility with `Sender`
19
+ def start()
20
+ end
21
+
22
+ # Compatibility with `Sender`
23
+ def stop()
24
+ end
25
+
26
+ # Compatibility with `Sender`
27
+ def rendez_vous()
28
+ end
29
+ end
30
+ end
31
+ end
@@ -11,10 +11,11 @@ module Datadog
11
11
  attr_reader :bytes_dropped
12
12
  attr_reader :packets_sent
13
13
  attr_reader :packets_dropped
14
- attr_reader :estimate_max_size
15
14
 
16
- def initialize(disabled, flush_interval, global_tags: [], transport_type: :udp)
17
- @disabled = disabled
15
+ # Rough estimation of maximum telemetry message size without tags
16
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS = 50 # bytes
17
+
18
+ def initialize(flush_interval, global_tags: [], transport_type: :udp)
18
19
  @flush_interval = flush_interval
19
20
  @global_tags = global_tags
20
21
  @transport_type = transport_type
@@ -27,15 +28,10 @@ module Datadog
27
28
  client_version: VERSION,
28
29
  client_transport: transport_type,
29
30
  ).format(global_tags)
31
+ end
30
32
 
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
33
+ def would_fit_in?(max_buffer_payload_size)
34
+ MAX_TELEMETRY_MESSAGE_SIZE_WT_TAGS + serialized_tags.size < max_buffer_payload_size
39
35
  end
40
36
 
41
37
  def reset
@@ -63,27 +59,29 @@ module Datadog
63
59
  @packets_dropped += packets
64
60
  end
65
61
 
66
- def flush?
62
+ def should_flush?
67
63
  @next_flush_time < now_in_s
68
64
  end
69
65
 
70
66
  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})
67
+ [
68
+ sprintf(pattern, 'metrics', @metrics),
69
+ sprintf(pattern, 'events', @events),
70
+ sprintf(pattern, 'service_checks', @service_checks),
71
+ sprintf(pattern, 'bytes_sent', @bytes_sent),
72
+ sprintf(pattern, 'bytes_dropped', @bytes_dropped),
73
+ sprintf(pattern, 'packets_sent', @packets_sent),
74
+ sprintf(pattern, 'packets_dropped', @packets_dropped),
75
+ ]
82
76
  end
83
77
 
84
78
  private
85
79
  attr_reader :serialized_tags
86
80
 
81
+ def pattern
82
+ @pattern ||= "datadog.dogstatsd.client.%s:%d|#{COUNTER_TYPE}|##{serialized_tags}"
83
+ end
84
+
87
85
  if Kernel.const_defined?('Process') && Process.respond_to?(:clock_gettime)
88
86
  def now_in_s
89
87
  Process.clock_gettime(Process::CLOCK_MONOTONIC, :second)
@@ -14,11 +14,11 @@ module Datadog
14
14
  # StatsD port. Defaults to 8125.
15
15
  attr_reader :port
16
16
 
17
- def initialize(host, port, logger, telemetry)
18
- super(telemetry)
17
+ def initialize(host, port, **kwargs)
18
+ super(**kwargs)
19
+
19
20
  @host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
20
21
  @port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT).to_i
21
- @logger = logger
22
22
  end
23
23
 
24
24
  private
@@ -10,10 +10,10 @@ 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
17
  end
18
18
 
19
19
  private
@@ -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.2.0'
8
8
  end
9
9
  end
metadata CHANGED
@@ -1,16 +1,17 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dogstatsd-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.8.2
4
+ version: 5.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rein Henrichs
8
+ - Karim Bogtob
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2020-11-16 00:00:00.000000000 Z
12
+ date: 2021-07-01 00:00:00.000000000 Z
12
13
  dependencies: []
13
- description: A Ruby DogStastd client
14
+ description: A Ruby DogStatsd client
14
15
  email: code@datadoghq.com
15
16
  executables: []
16
17
  extensions: []
@@ -21,14 +22,17 @@ files:
21
22
  - LICENSE.txt
22
23
  - README.md
23
24
  - lib/datadog/statsd.rb
24
- - lib/datadog/statsd/batch.rb
25
25
  - lib/datadog/statsd/connection.rb
26
+ - lib/datadog/statsd/forwarder.rb
27
+ - lib/datadog/statsd/message_buffer.rb
28
+ - lib/datadog/statsd/sender.rb
26
29
  - lib/datadog/statsd/serialization.rb
27
30
  - lib/datadog/statsd/serialization/event_serializer.rb
28
31
  - lib/datadog/statsd/serialization/serializer.rb
29
32
  - lib/datadog/statsd/serialization/service_check_serializer.rb
30
33
  - lib/datadog/statsd/serialization/stat_serializer.rb
31
34
  - lib/datadog/statsd/serialization/tag_serializer.rb
35
+ - lib/datadog/statsd/single_thread_sender.rb
32
36
  - lib/datadog/statsd/telemetry.rb
33
37
  - lib/datadog/statsd/udp_connection.rb
34
38
  - lib/datadog/statsd/uds_connection.rb
@@ -38,9 +42,9 @@ licenses:
38
42
  - MIT
39
43
  metadata:
40
44
  bug_tracker_uri: https://github.com/DataDog/dogstatsd-ruby/issues
41
- changelog_uri: https://github.com/DataDog/dogstatsd-ruby/blob/v4.8.2/CHANGELOG.md
42
- documentation_uri: https://www.rubydoc.info/gems/dogstatsd-ruby/4.8.2
43
- source_code_uri: https://github.com/DataDog/dogstatsd-ruby/tree/v4.8.2
45
+ changelog_uri: https://github.com/DataDog/dogstatsd-ruby/blob/v5.2.0/CHANGELOG.md
46
+ documentation_uri: https://www.rubydoc.info/gems/dogstatsd-ruby/5.2.0
47
+ source_code_uri: https://github.com/DataDog/dogstatsd-ruby/tree/v5.2.0
44
48
  post_install_message:
45
49
  rdoc_options: []
46
50
  require_paths:
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Datadog
4
- class Statsd
5
- class Batch
6
- def initialize(connection, max_buffer_bytes)
7
- @connection = connection
8
- @max_buffer_bytes = max_buffer_bytes
9
- @depth = 0
10
- reset
11
- end
12
-
13
- def open
14
- @depth += 1
15
-
16
- yield
17
- ensure
18
- @depth -= 1
19
- flush if !open?
20
- end
21
-
22
- def open?
23
- @depth > 0
24
- end
25
-
26
- def add(message)
27
- message_bytes = message.bytesize
28
-
29
- unless @buffer_bytes == 0
30
- if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
31
- flush
32
- else
33
- @buffer << "\n"
34
- @buffer_bytes += 1
35
- end
36
- end
37
-
38
- @buffer << message
39
- @buffer_bytes += message_bytes
40
- end
41
-
42
- def flush
43
- return if @buffer_bytes == 0
44
- @connection.write(@buffer)
45
- reset
46
- end
47
-
48
- private
49
-
50
- def reset
51
- @buffer = String.new
52
- @buffer_bytes = 0
53
- end
54
- end
55
- end
56
- end