dogstatsd-ruby 4.0.0 → 5.5.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.
@@ -1,6 +1,18 @@
1
1
  # frozen_string_literal: true
2
2
  require 'socket'
3
3
 
4
+ require_relative 'statsd/version'
5
+ require_relative 'statsd/telemetry'
6
+ require_relative 'statsd/udp_connection'
7
+ require_relative 'statsd/uds_connection'
8
+ require_relative 'statsd/connection_cfg'
9
+ require_relative 'statsd/message_buffer'
10
+ require_relative 'statsd/serialization'
11
+ require_relative 'statsd/sender'
12
+ require_relative 'statsd/single_thread_sender'
13
+ require_relative 'statsd/forwarder'
14
+ require_relative 'statsd/timer'
15
+
4
16
  # = Datadog::Statsd: A DogStatsd client (https://www.datadoghq.com)
5
17
  #
6
18
  # @example Set up a global Statsd client for a server on localhost:8125
@@ -19,219 +31,120 @@ require 'socket'
19
31
  # statsd = Datadog::Statsd.new 'localhost', 8125, tags: 'tag1:true'
20
32
  module Datadog
21
33
  class Statsd
22
-
23
- class Connection
24
- DEFAULT_HOST = '127.0.0.1'
25
- DEFAULT_PORT = 8125
26
-
27
- # StatsD host. Defaults to 127.0.0.1.
28
- attr_reader :host
29
-
30
- # StatsD port. Defaults to 8125.
31
- attr_reader :port
32
-
33
- # DogStatsd unix socket path. Not used by default.
34
- attr_reader :socket_path
35
-
36
- def initialize(host, port, socket_path, logger)
37
- @host = host || DEFAULT_HOST
38
- @port = port || DEFAULT_PORT
39
- @socket_path = socket_path
40
- @logger = logger
41
- end
42
-
43
- def write(message)
44
- @logger.debug { "Statsd: #{message}" } if @logger
45
- if @socket_path.nil?
46
- socket.send(message, 0)
47
- else
48
- socket.sendmsg_nonblock(message)
49
- end
50
- rescue StandardError => boom
51
- # Give up on this socket if it looks like it is bad
52
- bad_socket = !@socket_path.nil? && (
53
- boom.is_a?(Errno::ECONNREFUSED) ||
54
- boom.is_a?(Errno::ECONNRESET) ||
55
- boom.is_a?(Errno::ENOENT)
56
- )
57
- if bad_socket
58
- @socket = nil
59
- return
60
- end
61
-
62
- # Try once to reconnect if the socket has been closed
63
- retries ||= 1
64
- if retries <= 1 && boom.is_a?(IOError) && boom.message =~ /closed stream/i
65
- retries += 1
66
- begin
67
- @socket = connect
68
- retry
69
- rescue StandardError => e
70
- boom = e
71
- end
72
- end
73
-
74
- @logger.error { "Statsd: #{boom.class} #{boom}" } if @logger
75
- nil
76
- end
77
-
78
- # Close the underlying socket
79
- def close
80
- @socket && @socket.close
81
- end
82
-
83
- private
84
-
85
- def socket
86
- @socket ||= connect
87
- end
88
-
89
- def connect
90
- if @socket_path.nil?
91
- socket = UDPSocket.new
92
- socket.connect(@host, @port)
93
- else
94
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
95
- socket.connect(Socket.pack_sockaddr_un(@socket_path))
96
- end
97
- socket
98
- end
34
+ class Error < StandardError
99
35
  end
100
36
 
101
- class Batch
102
- def initialize(connection, max_buffer_bytes)
103
- @connection = connection
104
- @max_buffer_bytes = max_buffer_bytes
105
- @depth = 0
106
- reset
107
- end
108
-
109
- def open
110
- @depth += 1
111
- yield
112
- ensure
113
- @depth -= 1
114
- flush if !open?
115
- end
116
-
117
- def open?
118
- @depth > 0
119
- end
120
-
121
- def add(message)
122
- message_bytes = message.bytesize
37
+ OK = 0
38
+ WARNING = 1
39
+ CRITICAL = 2
40
+ UNKNOWN = 3
123
41
 
124
- unless @buffer_bytes == 0
125
- if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
126
- flush
127
- else
128
- @buffer << NEW_LINE
129
- @buffer_bytes += 1
130
- end
131
- end
42
+ UDP_DEFAULT_BUFFER_SIZE = 1_432
43
+ UDS_DEFAULT_BUFFER_SIZE = 8_192
44
+ DEFAULT_BUFFER_POOL_SIZE = Float::INFINITY
132
45
 
133
- @buffer << message
134
- @buffer_bytes += message_bytes
135
- end
136
-
137
- def flush
138
- return if @buffer_bytes == 0
139
- @connection.write @buffer
140
- reset
141
- end
46
+ UDP_DEFAULT_SENDER_QUEUE_SIZE = 2048
47
+ UDS_DEFAULT_SENDER_QUEUE_SIZE = 512
142
48
 
143
- private
49
+ MAX_EVENT_SIZE = 8 * 1_024
144
50
 
145
- def reset
146
- @buffer = String.new
147
- @buffer_bytes = 0
148
- end
149
- end
51
+ # minimum flush interval for the telemetry in seconds
52
+ DEFAULT_TELEMETRY_FLUSH_INTERVAL = 10
150
53
 
151
- # Create a dictionary to assign a key to every parameter's name, except for tags (treated differently)
152
- # Goal: Simple and fast to add some other parameters
153
- OPTS_KEYS = {
154
- :date_happened => :d,
155
- :hostname => :h,
156
- :aggregation_key => :k,
157
- :priority => :p,
158
- :source_type_name => :s,
159
- :alert_type => :t,
160
- }
161
-
162
- # Service check options
163
- SC_OPT_KEYS = {
164
- :timestamp => 'd:'.freeze,
165
- :hostname => 'h:'.freeze,
166
- :tags => '#'.freeze,
167
- :message => 'm:'.freeze,
168
- }
169
-
170
- OK = 0
171
- WARNING = 1
172
- CRITICAL = 2
173
- UNKNOWN = 3
174
-
175
- MAX_EVENT_SIZE = 8 * 1024
176
-
177
- COUNTER_TYPE = 'c'.freeze
178
- GAUGE_TYPE = 'g'.freeze
179
- HISTOGRAM_TYPE = 'h'.freeze
180
- DISTRIBUTION_TYPE = 'd'.freeze
181
- TIMING_TYPE = 'ms'.freeze
182
- SET_TYPE = 's'.freeze
183
- VERSION = "4.0.0".freeze
54
+ COUNTER_TYPE = 'c'
55
+ GAUGE_TYPE = 'g'
56
+ HISTOGRAM_TYPE = 'h'
57
+ DISTRIBUTION_TYPE = 'd'
58
+ TIMING_TYPE = 'ms'
59
+ SET_TYPE = 's'
184
60
 
185
61
  # A namespace to prepend to all statsd calls. Defaults to no namespace.
186
62
  attr_reader :namespace
187
63
 
188
64
  # Global tags to be added to every statsd call. Defaults to no tags.
189
- attr_reader :tags
190
-
191
- # Buffer containing the statsd message before they are sent in batch
192
- attr_reader :buffer
193
-
194
- # Maximum buffer size in bytes before it is flushed
195
- attr_reader :max_buffer_bytes
65
+ def tags
66
+ serializer.global_tags
67
+ end
196
68
 
197
- # Connection
198
- attr_reader :connection
69
+ # Default sample rate
70
+ attr_reader :sample_rate
199
71
 
200
72
  # @param [String] host your statsd host
201
73
  # @param [Integer] port your statsd port
202
74
  # @option [String] namespace set a namespace to be prepended to every metric name
203
- # @option [Array<String>] tags tags to be added to every metric
204
- # @option [Loger] logger for debugging
205
- # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
75
+ # @option [Array<String>|Hash] tags tags to be added to every metric
76
+ # @option [Logger] logger for debugging
77
+ # @option [Integer] buffer_max_payload_size max bytes to buffer
78
+ # @option [Integer] buffer_max_pool_size max messages to buffer
79
+ # @option [Integer] sender_queue_size size of the sender queue in number of buffers (multi-thread only)
80
+ # @option [Numeric] buffer_flush_interval interval in second to flush buffer
206
81
  # @option [String] socket_path unix socket path
82
+ # @option [Float] default sample rate if not overridden
83
+ # @option [Boolean] single_thread flushes the metrics on the main thread instead of in a companion thread
207
84
  def initialize(
208
85
  host = nil,
209
86
  port = nil,
87
+ socket_path: nil,
88
+
210
89
  namespace: nil,
211
90
  tags: nil,
212
- max_buffer_bytes: 8192,
213
- socket_path: nil,
214
- logger: nil
91
+ sample_rate: nil,
92
+
93
+ buffer_max_payload_size: nil,
94
+ buffer_max_pool_size: nil,
95
+ buffer_overflowing_stategy: :drop,
96
+ buffer_flush_interval: nil,
97
+
98
+ sender_queue_size: nil,
99
+
100
+ logger: nil,
101
+
102
+ single_thread: false,
103
+
104
+ telemetry_enable: true,
105
+ telemetry_flush_interval: DEFAULT_TELEMETRY_FLUSH_INTERVAL
215
106
  )
216
- @connection = Connection.new(host, port, socket_path, logger)
217
- @logger = logger
107
+ unless tags.nil? || tags.is_a?(Array) || tags.is_a?(Hash)
108
+ raise ArgumentError, 'tags must be an array of string tags or a Hash'
109
+ end
218
110
 
219
111
  @namespace = namespace
220
112
  @prefix = @namespace ? "#{@namespace}.".freeze : nil
113
+ @serializer = Serialization::Serializer.new(prefix: @prefix, global_tags: tags)
114
+ @sample_rate = sample_rate
115
+
116
+ @forwarder = Forwarder.new(
117
+ connection_cfg: ConnectionCfg.new(
118
+ host: host,
119
+ port: port,
120
+ socket_path: socket_path,
121
+ ),
221
122
 
222
- raise ArgumentError, 'tags must be a Array<String>' unless tags.nil? or tags.is_a? Array
223
- @tags = (tags || []).compact.map! {|tag| escape_tag_content(tag)}
123
+ global_tags: tags,
124
+ logger: logger,
224
125
 
225
- @batch = Batch.new @connection, max_buffer_bytes
126
+ single_thread: single_thread,
127
+
128
+ buffer_max_payload_size: buffer_max_payload_size,
129
+ buffer_max_pool_size: buffer_max_pool_size,
130
+ buffer_overflowing_stategy: buffer_overflowing_stategy,
131
+ buffer_flush_interval: buffer_flush_interval,
132
+
133
+ sender_queue_size: sender_queue_size,
134
+
135
+ telemetry_flush_interval: telemetry_enable ? telemetry_flush_interval : nil,
136
+ )
226
137
  end
227
138
 
228
139
  # yield a new instance to a block and close it when done
229
140
  # for short-term use-cases that don't want to close the socket manually
230
- def self.open(*args)
231
- instance = new(*args)
141
+ # TODO: replace with ... once we are on ruby 2.7
142
+ def self.open(*args, **kwargs)
143
+ instance = new(*args, **kwargs)
144
+
232
145
  yield instance
233
146
  ensure
234
- instance.close
147
+ instance.close if instance
235
148
  end
236
149
 
237
150
  # Sends an increment (count = 1) for the given stat to the statsd server.
@@ -239,13 +152,14 @@ module Datadog
239
152
  # @param [String] stat stat name
240
153
  # @param [Hash] opts the options to create the metric with
241
154
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
155
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
242
156
  # @option opts [Array<String>] :tags An array of tags
243
157
  # @option opts [Numeric] :by increment value, default 1
244
158
  # @see #count
245
- def increment(stat, opts=EMPTY_OPTIONS)
246
- opts = {:sample_rate => opts} if opts.is_a? Numeric
159
+ def increment(stat, opts = EMPTY_OPTIONS)
160
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
247
161
  incr_value = opts.fetch(:by, 1)
248
- count stat, incr_value, opts
162
+ count(stat, incr_value, opts)
249
163
  end
250
164
 
251
165
  # Sends a decrement (count = -1) for the given stat to the statsd server.
@@ -253,13 +167,14 @@ module Datadog
253
167
  # @param [String] stat stat name
254
168
  # @param [Hash] opts the options to create the metric with
255
169
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
170
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
256
171
  # @option opts [Array<String>] :tags An array of tags
257
172
  # @option opts [Numeric] :by decrement value, default 1
258
173
  # @see #count
259
- def decrement(stat, opts=EMPTY_OPTIONS)
260
- opts = {:sample_rate => opts} if opts.is_a? Numeric
174
+ def decrement(stat, opts = EMPTY_OPTIONS)
175
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
261
176
  decr_value = - opts.fetch(:by, 1)
262
- count stat, decr_value, opts
177
+ count(stat, decr_value, opts)
263
178
  end
264
179
 
265
180
  # Sends an arbitrary count for the given stat to the statsd server.
@@ -268,13 +183,14 @@ module Datadog
268
183
  # @param [Integer] count count
269
184
  # @param [Hash] opts the options to create the metric with
270
185
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
186
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
271
187
  # @option opts [Array<String>] :tags An array of tags
272
- def count(stat, count, opts=EMPTY_OPTIONS)
273
- opts = {:sample_rate => opts} if opts.is_a? Numeric
274
- send_stats stat, count, COUNTER_TYPE, opts
188
+ def count(stat, count, opts = EMPTY_OPTIONS)
189
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
190
+ send_stats(stat, count, COUNTER_TYPE, opts)
275
191
  end
276
192
 
277
- # Sends an arbitary gauge value for the given stat to the statsd server.
193
+ # Sends an arbitrary gauge value for the given stat to the statsd server.
278
194
  #
279
195
  # This is useful for recording things like available disk space,
280
196
  # memory usage, and the like, which have different semantics than
@@ -284,12 +200,13 @@ module Datadog
284
200
  # @param [Numeric] value gauge value.
285
201
  # @param [Hash] opts the options to create the metric with
286
202
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
203
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
287
204
  # @option opts [Array<String>] :tags An array of tags
288
205
  # @example Report the current user count:
289
206
  # $statsd.gauge('user.count', User.count)
290
- def gauge(stat, value, opts=EMPTY_OPTIONS)
291
- opts = {:sample_rate => opts} if opts.is_a? Numeric
292
- send_stats stat, value, GAUGE_TYPE, opts
207
+ def gauge(stat, value, opts = EMPTY_OPTIONS)
208
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
209
+ send_stats(stat, value, GAUGE_TYPE, opts)
293
210
  end
294
211
 
295
212
  # Sends a value to be tracked as a histogram to the statsd server.
@@ -298,27 +215,46 @@ module Datadog
298
215
  # @param [Numeric] value histogram value.
299
216
  # @param [Hash] opts the options to create the metric with
300
217
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
218
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
301
219
  # @option opts [Array<String>] :tags An array of tags
302
220
  # @example Report the current user count:
303
221
  # $statsd.histogram('user.count', User.count)
304
- def histogram(stat, value, opts=EMPTY_OPTIONS)
305
- send_stats stat, value, HISTOGRAM_TYPE, opts
222
+ def histogram(stat, value, opts = EMPTY_OPTIONS)
223
+ send_stats(stat, value, HISTOGRAM_TYPE, opts)
306
224
  end
307
225
 
308
226
  # Sends a value to be tracked as a distribution to the statsd server.
309
- # Note: Distributions are a beta feature of Datadog and not generally
310
- # available. Distributions must be specifically enabled for your
311
- # organization.
312
227
  #
313
228
  # @param [String] stat stat name.
314
229
  # @param [Numeric] value distribution value.
315
230
  # @param [Hash] opts the options to create the metric with
316
231
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
232
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
317
233
  # @option opts [Array<String>] :tags An array of tags
318
234
  # @example Report the current user count:
319
235
  # $statsd.distribution('user.count', User.count)
320
- def distribution(stat, value, opts=EMPTY_OPTIONS)
321
- send_stats stat, value, DISTRIBUTION_TYPE, opts
236
+ def distribution(stat, value, opts = EMPTY_OPTIONS)
237
+ send_stats(stat, value, DISTRIBUTION_TYPE, opts)
238
+ end
239
+
240
+ # Reports execution time of the provided block as a distribution.
241
+ #
242
+ # If the block fails, the stat is still reported, then the error
243
+ # is reraised
244
+ #
245
+ # @param [String] stat stat name.
246
+ # @param [Numeric] value distribution value.
247
+ # @param [Hash] opts the options to create the metric with
248
+ # @option opts [Numeric] :sample_rate sample rate, 1 for always
249
+ # @option opts [Array<String>] :tags An array of tags
250
+ # @example Report the time (in ms) taken to activate an account
251
+ # $statsd.distribution_time('account.activate') { @account.activate! }
252
+ def distribution_time(stat, opts = EMPTY_OPTIONS)
253
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
254
+ start = now
255
+ yield
256
+ ensure
257
+ distribution(stat, ((now - start) * 1000).round, opts)
322
258
  end
323
259
 
324
260
  # Sends a timing (in ms) for the given stat to the statsd server. The
@@ -330,10 +266,11 @@ module Datadog
330
266
  # @param [Integer] ms timing in milliseconds
331
267
  # @param [Hash] opts the options to create the metric with
332
268
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
269
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
333
270
  # @option opts [Array<String>] :tags An array of tags
334
- def timing(stat, ms, opts=EMPTY_OPTIONS)
335
- opts = {:sample_rate => opts} if opts.is_a? Numeric
336
- send_stats stat, ms, TIMING_TYPE, opts
271
+ def timing(stat, ms, opts = EMPTY_OPTIONS)
272
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
273
+ send_stats(stat, ms, TIMING_TYPE, opts)
337
274
  end
338
275
 
339
276
  # Reports execution time of the provided block using {#timing}.
@@ -344,18 +281,18 @@ module Datadog
344
281
  # @param [String] stat stat name
345
282
  # @param [Hash] opts the options to create the metric with
346
283
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
284
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
347
285
  # @option opts [Array<String>] :tags An array of tags
348
286
  # @yield The operation to be timed
349
287
  # @see #timing
350
288
  # @example Report the time (in ms) taken to activate an account
351
289
  # $statsd.time('account.activate') { @account.activate! }
352
- def time(stat, opts=EMPTY_OPTIONS)
353
- opts = {:sample_rate => opts} if opts.is_a? Numeric
354
- start = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
355
- return yield
290
+ def time(stat, opts = EMPTY_OPTIONS)
291
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
292
+ start = now
293
+ yield
356
294
  ensure
357
- finished = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
358
- timing(stat, ((finished - start) * 1000).round, opts)
295
+ timing(stat, ((now - start) * 1000).round, opts)
359
296
  end
360
297
 
361
298
  # Sends a value to be tracked as a set to the statsd server.
@@ -364,12 +301,13 @@ module Datadog
364
301
  # @param [Numeric] value set value.
365
302
  # @param [Hash] opts the options to create the metric with
366
303
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
304
+ # @option opts [Boolean] :pre_sampled If true, the client assumes the caller has already sampled metrics at :sample_rate, and doesn't perform sampling.
367
305
  # @option opts [Array<String>] :tags An array of tags
368
306
  # @example Record a unique visitory by id:
369
307
  # $statsd.set('visitors.uniques', User.id)
370
- def set(stat, value, opts=EMPTY_OPTIONS)
371
- opts = {:sample_rate => opts} if opts.is_a? Numeric
372
- send_stats stat, value, SET_TYPE, opts
308
+ def set(stat, value, opts = EMPTY_OPTIONS)
309
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
310
+ send_stats(stat, value, SET_TYPE, opts)
373
311
  end
374
312
 
375
313
  # This method allows you to send custom service check statuses.
@@ -377,14 +315,16 @@ module Datadog
377
315
  # @param [String] name Service check name
378
316
  # @param [String] status Service check status.
379
317
  # @param [Hash] opts the additional data about the service check
380
- # @option opts [Integer, nil] :timestamp (nil) Assign a timestamp to the event. Default is now when none
381
- # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
318
+ # @option opts [Integer, String, nil] :timestamp (nil) Assign a timestamp to the service check. Default is now when none
319
+ # @option opts [String, nil] :hostname (nil) Assign a hostname to the service check.
382
320
  # @option opts [Array<String>, nil] :tags (nil) An array of tags
383
321
  # @option opts [String, nil] :message (nil) A message to associate with this service check status
384
322
  # @example Report a critical service check status
385
323
  # $statsd.service_check('my.service.check', Statsd::CRITICAL, :tags=>['urgent'])
386
- def service_check(name, status, opts=EMPTY_OPTIONS)
387
- send_stat format_service_check(name, status, opts)
324
+ def service_check(name, status, opts = EMPTY_OPTIONS)
325
+ telemetry.sent(service_checks: 1) if telemetry
326
+
327
+ forwarder.send_message(serializer.to_service_check(name, status, opts))
388
328
  end
389
329
 
390
330
  # This end point allows you to post events to the stream. You can tag them, set priority and even aggregate them with other events.
@@ -394,23 +334,33 @@ module Datadog
394
334
  # it will be grouped with other events that don't have an event type.
395
335
  #
396
336
  # @param [String] title Event title
397
- # @param [String] text Event text. Supports \n
337
+ # @param [String] text Event text. Supports newlines (+\n+)
398
338
  # @param [Hash] opts the additional data about the event
399
- # @option opts [Integer, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
339
+ # @option opts [Integer, String, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
400
340
  # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
401
341
  # @option opts [String, nil] :aggregation_key (nil) Assign an aggregation key to the event, to group it with some others
402
342
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
403
343
  # @option opts [String, nil] :source_type_name (nil) Assign a source type to the event
404
344
  # @option opts [String, nil] :alert_type ('info') Can be "error", "warning", "info" or "success".
345
+ # @option opts [Boolean, false] :truncate_if_too_long (false) Truncate the event if it is too long
405
346
  # @option opts [Array<String>] :tags tags to be added to every metric
406
347
  # @example Report an awful event:
407
348
  # $statsd.event('Something terrible happened', 'The end is near if we do nothing', :alert_type=>'warning', :tags=>['end_of_times','urgent'])
408
- def event(title, text, opts=EMPTY_OPTIONS)
409
- send_stat format_event(title, text, opts)
349
+ def event(title, text, opts = EMPTY_OPTIONS)
350
+ telemetry.sent(events: 1) if telemetry
351
+
352
+ forwarder.send_message(serializer.to_event(title, text, opts))
410
353
  end
411
354
 
412
- # Send several metrics in the same UDP Packet
413
- # They will be buffered and flushed when the block finishes
355
+ # Send several metrics in the same packet.
356
+ # They will be buffered and flushed when the block finishes.
357
+ #
358
+ # This method exists for compatibility with v4.x versions, it is not needed
359
+ # anymore since the batching is now automatically done internally.
360
+ # It also means that an automatic flush could occur if the buffer is filled
361
+ # during the execution of the batch block.
362
+ #
363
+ # This method is DEPRECATED and will be removed in future v6.x API.
414
364
  #
415
365
  # @example Send several metrics in one packet:
416
366
  # $statsd.batch do |s|
@@ -418,140 +368,66 @@ module Datadog
418
368
  # s.increment('page.views')
419
369
  # end
420
370
  def batch
421
- @batch.open { yield self }
371
+ yield self
372
+ flush(sync: true)
422
373
  end
423
374
 
424
375
  # Close the underlying socket
425
- def close
426
- @connection.close
376
+ #
377
+ # @param [Boolean, true] flush Should we flush the metrics before closing
378
+ def close(flush: true)
379
+ flush(sync: true) if flush
380
+ forwarder.close
427
381
  end
428
382
 
429
- private
430
-
431
- NEW_LINE = "\n".freeze
432
- ESC_NEW_LINE = "\\n".freeze
433
- COMMA = ",".freeze
434
- PIPE = "|".freeze
435
- DOT = ".".freeze
436
- DOUBLE_COLON = "::".freeze
437
- UNDERSCORE = "_".freeze
438
- PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= "2.1.0")
439
- EMPTY_OPTIONS = {}.freeze
440
-
441
- private_constant :NEW_LINE, :ESC_NEW_LINE, :COMMA, :PIPE, :DOT,
442
- :DOUBLE_COLON, :UNDERSCORE, :EMPTY_OPTIONS
443
-
444
- def format_service_check(name, status, opts=EMPTY_OPTIONS)
445
- sc_string = "_sc|#{name}|#{status}".dup
446
-
447
- SC_OPT_KEYS.each do |key, shorthand_key|
448
- next unless opts[key]
449
-
450
- if key == :tags
451
- if tags_string = tags_as_string(opts)
452
- sc_string << "|##{tags_string}"
453
- end
454
- elsif key == :message
455
- message = remove_pipes(opts[:message])
456
- escaped_message = escape_service_check_message(message)
457
- sc_string << "|m:#{escaped_message}"
458
- else
459
- value = remove_pipes(opts[key])
460
- sc_string << "|#{shorthand_key}#{value}"
461
- end
462
- end
463
- sc_string
383
+ def sync_with_outbound_io
384
+ forwarder.sync_with_outbound_io
464
385
  end
465
386
 
466
- def format_event(title, text, opts=EMPTY_OPTIONS)
467
- escaped_title = escape_event_content(title)
468
- escaped_text = escape_event_content(text)
469
- event_string_data = "_e{#{escaped_title.length},#{escaped_text.length}}:#{escaped_title}|#{escaped_text}".dup
470
-
471
- # We construct the string to be sent by adding '|key:value' parts to it when needed
472
- # All pipes ('|') in the metadata are removed. Title and Text can keep theirs
473
- OPTS_KEYS.each do |key, shorthand_key|
474
- if key != :tags && opts[key]
475
- value = remove_pipes(opts[key])
476
- event_string_data << "|#{shorthand_key}:#{value}"
477
- end
478
- end
479
-
480
- # Tags are joined and added as last part to the string to be sent
481
- if tags_string = tags_as_string(opts)
482
- event_string_data << "|##{tags_string}"
483
- end
484
-
485
- raise "Event #{title} payload is too big (more that 8KB), event discarded" if event_string_data.length > MAX_EVENT_SIZE
486
- event_string_data
387
+ # Flush the buffer into the connection
388
+ def flush(flush_telemetry: false, sync: false)
389
+ forwarder.flush(flush_telemetry: flush_telemetry, sync: sync)
487
390
  end
488
391
 
489
- def tags_as_string(opts)
490
- if tag_arr = opts[:tags]
491
- tag_arr = tag_arr.map { |tag| escape_tag_content(tag) }
492
- tag_arr = tags + tag_arr # @tags are normalized when set, so not need to normalize them again
493
- else
494
- tag_arr = tags
495
- end
496
- tag_arr.join(COMMA) unless tag_arr.empty?
392
+ def telemetry
393
+ forwarder.telemetry
497
394
  end
498
395
 
499
- def escape_event_content(msg)
500
- msg.gsub NEW_LINE, ESC_NEW_LINE
396
+ def host
397
+ forwarder.host
501
398
  end
502
399
 
503
- def escape_tag_content(tag)
504
- tag = remove_pipes(tag.to_s)
505
- tag.delete! COMMA
506
- tag
400
+ def port
401
+ forwarder.port
507
402
  end
508
403
 
509
- def remove_pipes(msg)
510
- msg.delete PIPE
404
+ def socket_path
405
+ forwarder.socket_path
511
406
  end
512
407
 
513
- def escape_service_check_message(msg)
514
- escape_event_content(msg).gsub('m:'.freeze, 'm\:'.freeze)
408
+ def transport_type
409
+ forwarder.transport_type
515
410
  end
516
411
 
517
- def send_stats(stat, delta, type, opts=EMPTY_OPTIONS)
518
- sample_rate = opts[:sample_rate] || 1
519
- if sample_rate == 1 or rand < sample_rate
520
- full_stat = ''.dup
521
- full_stat << @prefix if @prefix
522
-
523
- stat = stat.is_a?(String) ? stat.dup : stat.to_s
524
- # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
525
- stat.gsub!(DOUBLE_COLON, DOT)
526
- stat.tr!(':|@'.freeze, UNDERSCORE)
527
- full_stat << stat
528
-
529
- full_stat << ':'.freeze
530
- full_stat << delta.to_s
531
- full_stat << PIPE
532
- full_stat << type
533
-
534
- unless sample_rate == 1
535
- full_stat << PIPE
536
- full_stat << '@'.freeze
537
- full_stat << sample_rate.to_s
538
- end
539
-
540
- if tags_string = tags_as_string(opts)
541
- full_stat << PIPE
542
- full_stat << '#'.freeze
543
- full_stat << tags_string
544
- end
545
-
546
- send_stat(full_stat)
547
- end
412
+ private
413
+ attr_reader :serializer
414
+ attr_reader :forwarder
415
+
416
+ EMPTY_OPTIONS = {}.freeze
417
+
418
+ def now
419
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
548
420
  end
549
421
 
550
- def send_stat(message)
551
- if @batch.open?
552
- @batch.add message
553
- else
554
- @connection.write(message)
422
+ def send_stats(stat, delta, type, opts = EMPTY_OPTIONS)
423
+ telemetry.sent(metrics: 1) if telemetry
424
+
425
+ sample_rate = opts[:sample_rate] || @sample_rate || 1
426
+
427
+ if sample_rate == 1 || opts[:pre_sampled] || rand <= sample_rate
428
+ full_stat = serializer.to_stat(stat, delta, type, tags: opts[:tags], sample_rate: sample_rate)
429
+
430
+ forwarder.send_message(full_stat)
555
431
  end
556
432
  end
557
433
  end