dogstatsd-ruby 3.3.0 → 5.6.1

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