dogstatsd-ruby 3.3.0 → 5.6.1

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