dogstatsd-ruby 3.3.0 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +34 -5
  3. data/lib/datadog/statsd.rb +290 -182
  4. metadata +4 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 046a0af9f230451c91fdeb9ae128c550b1c20623fa017d326e63a2f053f7cc9a
4
- data.tar.gz: e2ba838497de521e89d386645a0228f819fdd03c28acc05a4789aa7f8d979de8
3
+ metadata.gz: fde2be6b614574c2555c8cbbd2ca16ef33bcd09837502f482f5f6baf582fd67b
4
+ data.tar.gz: cc1523c5aa78643d93fc891116c46bb5322dda297f1676cd73d201c9beee2a26
5
5
  SHA512:
6
- metadata.gz: f2048376477bd1af31a63b75d17638df999c9f6341f967b2c826177add2a13831a54a72729e2a5c62aa09fa4aca6dccbaba308c79d9adcd470808d3bb80cde59
7
- data.tar.gz: 2e0ac5fc90f8a2359f35a754034bb6474c1169ac8c8f3301257555553e1aca4f708cecd4d9245ff835cbe4396561a89d3bd3700ef6cea6c92c343a1e41ca2055
6
+ metadata.gz: 9c96177f1ea0ab2288712cf9829cb8408687e12ce0f22f06ddbb1433447a3a4eaf05fe13f0fda8746b9f0e8fad71ea3b0b50ba7d8a028e464db334349951c64b
7
+ data.tar.gz: 11e23d434a7e88925e049484dfbb252d5c78ce12a4a07fd3c09edca57835db30224cba202f86f57352357acdf2073b8c8e93f20bd403975242b201321bef8ecc
data/README.md CHANGED
@@ -4,7 +4,7 @@ dogstatsd-ruby
4
4
 
5
5
  A client for DogStatsD, an extension of the StatsD metric server for Datadog.
6
6
 
7
- [![Build Status](https://secure.travis-ci.org/DataDog/dogstatsd-ruby.png)](http://travis-ci.org/DataDog/dogstatsd-ruby)
7
+ [![Build Status](https://secure.travis-ci.org/DataDog/dogstatsd-ruby.svg)](http://travis-ci.org/DataDog/dogstatsd-ruby)
8
8
 
9
9
  Quick Start Guide
10
10
  -----------------
@@ -28,9 +28,10 @@ statsd = Datadog::Statsd.new('localhost', 8125)
28
28
 
29
29
  # Increment a counter.
30
30
  statsd.increment('page.views')
31
+ statsd.increment('messages.count', by: 2, tags: ['kind:incoming'])
31
32
 
32
33
  # Record a gauge 50% of the time.
33
- statsd.gauge('users.online', 123, :sample_rate=>0.5)
34
+ statsd.gauge('users.online', 123, sample_rate: 0.5)
34
35
 
35
36
  # Sample a histogram
36
37
  statsd.histogram('file.upload.size', 1234)
@@ -48,7 +49,15 @@ statsd.batch do |s|
48
49
  end
49
50
 
50
51
  # Tag a metric.
51
- statsd.histogram('query.time', 10, :tags => ["version:1"])
52
+ statsd.histogram('query.time', 10, tags: ['version:1'])
53
+
54
+ # Tag a metric by passing in a Hash
55
+ statsd.histogram('query.time', 10, :tags => {version: 1})
56
+
57
+ # Auto-close socket after end of block
58
+ Datadog::Statsd.open('localhost', 8125) do |s|
59
+ s.increment('page.views')
60
+ end
52
61
  ```
53
62
 
54
63
  You can also post events to your stream. You can tag them, set priority and even aggregate them with other events.
@@ -57,14 +66,34 @@ Aggregation in the stream is made on hostname/event_type/source_type/aggregation
57
66
 
58
67
  ``` ruby
59
68
  # Post a simple message
60
- statsd.event("There might be a storm tomorrow", "A friend warned me earlier.")
69
+ statsd.event('There might be a storm tomorrow', 'A friend warned me earlier.')
61
70
 
62
71
  # Cry for help
63
- statsd.event("SO MUCH SNOW", "Started yesterday and it won't stop !!", :alert_type => "error", :tags => ["urgent", "endoftheworld"])
72
+ statsd.event(
73
+ 'SO MUCH SNOW',
74
+ "Started yesterday and it won't stop !!",
75
+ alert_type: 'error',
76
+ tags: ['urgent', 'endoftheworld']
77
+ )
64
78
  ```
65
79
 
66
80
 
67
81
 
82
+ Origin detection over UDP
83
+ -------------
84
+ 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.
85
+
86
+ To enable origin detection over UDP, add the following lines to your application manifest
87
+ ```yaml
88
+ env:
89
+ - name: DD_ENTITY_ID
90
+ valueFrom:
91
+ fieldRef:
92
+ fieldPath: metadata.uid
93
+ ```
94
+ The DogStatsD client attaches an internal tag, `entity_id`. The value of this tag is the content of the `DD_ENTITY_ID` environment variable, which is the pod’s UID.
95
+
96
+
68
97
  Documentation
69
98
  -------------
70
99
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'socket'
2
3
 
3
4
  # = Datadog::Statsd: A DogStatsd client (https://www.datadoghq.com)
@@ -15,12 +16,151 @@ require 'socket'
15
16
  # statsd = Datadog::Statsd.new 'localhost', 8125, :namespace => 'account'
16
17
  # statsd.increment 'activate'
17
18
  # @example Create a statsd client with global tags
18
- # statsd = Datadog::Statsd.new 'localhost', 8125, :tags => 'tag1:true'
19
+ # statsd = Datadog::Statsd.new 'localhost', 8125, tags: 'tag1:true'
19
20
  module Datadog
20
21
  class Statsd
21
22
 
22
- DEFAULT_HOST = '127.0.0.1'
23
- DEFAULT_PORT = 8125
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
+ # Close the underlying socket
37
+ def close
38
+ @socket && @socket.close
39
+ end
40
+
41
+ def write(message)
42
+ @logger.debug { "Statsd: #{message}" } if @logger
43
+ send_message(message)
44
+ rescue StandardError => boom
45
+ # Try once to reconnect if the socket has been closed
46
+ retries ||= 1
47
+ if retries <= 1 &&
48
+ (boom.is_a?(Errno::ENOTCONN) or
49
+ boom.is_a?(Errno::ECONNREFUSED) or
50
+ boom.is_a?(IOError) && boom.message =~ /closed stream/i)
51
+ retries += 1
52
+ begin
53
+ @socket = connect
54
+ retry
55
+ rescue StandardError => e
56
+ boom = e
57
+ end
58
+ end
59
+
60
+ @logger.error { "Statsd: #{boom.class} #{boom}" } if @logger
61
+ nil
62
+ end
63
+
64
+ private
65
+
66
+ def socket
67
+ @socket ||= connect
68
+ end
69
+ end
70
+
71
+ class UDPConnection < Connection
72
+ def initialize(host, port, logger)
73
+ @host = host || ENV.fetch('DD_AGENT_HOST', nil) || DEFAULT_HOST
74
+ @port = port || ENV.fetch('DD_DOGSTATSD_PORT', nil) || DEFAULT_PORT
75
+ @logger = logger
76
+ end
77
+
78
+ private
79
+
80
+ def connect
81
+ socket = UDPSocket.new
82
+ socket.connect(@host, @port)
83
+ socket
84
+ end
85
+
86
+ def send_message(message)
87
+ socket.send(message, 0)
88
+ end
89
+ end
90
+
91
+ class UDSConnection < Connection
92
+ class BadSocketError < StandardError; end
93
+
94
+ def initialize(socket_path, logger)
95
+ @socket_path = socket_path
96
+ @logger = logger
97
+ end
98
+
99
+ private
100
+
101
+ def connect
102
+ socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
103
+ socket.connect(Socket.pack_sockaddr_un(@socket_path))
104
+ socket
105
+ end
106
+
107
+ def send_message(message)
108
+ socket.sendmsg_nonblock(message)
109
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
110
+ @socket = nil
111
+ raise BadSocketError, "#{e.class}: #{e}"
112
+ end
113
+ end
114
+
115
+ class Batch
116
+ def initialize(connection, max_buffer_bytes)
117
+ @connection = connection
118
+ @max_buffer_bytes = max_buffer_bytes
119
+ @depth = 0
120
+ reset
121
+ end
122
+
123
+ def open
124
+ @depth += 1
125
+ yield
126
+ ensure
127
+ @depth -= 1
128
+ flush if !open?
129
+ end
130
+
131
+ def open?
132
+ @depth > 0
133
+ end
134
+
135
+ def add(message)
136
+ message_bytes = message.bytesize
137
+
138
+ unless @buffer_bytes == 0
139
+ if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
140
+ flush
141
+ else
142
+ @buffer << NEW_LINE
143
+ @buffer_bytes += 1
144
+ end
145
+ end
146
+
147
+ @buffer << message
148
+ @buffer_bytes += message_bytes
149
+ end
150
+
151
+ def flush
152
+ return if @buffer_bytes == 0
153
+ @connection.write @buffer
154
+ reset
155
+ end
156
+
157
+ private
158
+
159
+ def reset
160
+ @buffer = String.new
161
+ @buffer_bytes = 0
162
+ end
163
+ end
24
164
 
25
165
  # Create a dictionary to assign a key to every parameter's name, except for tags (treated differently)
26
166
  # Goal: Simple and fast to add some other parameters
@@ -46,78 +186,84 @@ module Datadog
46
186
  CRITICAL = 2
47
187
  UNKNOWN = 3
48
188
 
189
+ MAX_EVENT_SIZE = 8 * 1024
190
+
49
191
  COUNTER_TYPE = 'c'.freeze
50
192
  GAUGE_TYPE = 'g'.freeze
51
193
  HISTOGRAM_TYPE = 'h'.freeze
52
194
  DISTRIBUTION_TYPE = 'd'.freeze
53
195
  TIMING_TYPE = 'ms'.freeze
54
196
  SET_TYPE = 's'.freeze
55
- VERSION = "3.3.0".freeze
197
+ VERSION = "4.5.0".freeze
56
198
 
57
199
  # A namespace to prepend to all statsd calls. Defaults to no namespace.
58
200
  attr_reader :namespace
59
201
 
60
- # StatsD host. Defaults to 127.0.0.1.
61
- attr_reader :host
62
-
63
- # StatsD port. Defaults to 8125.
64
- attr_reader :port
65
-
66
- # DogStatsd unix socket path. Not used by default.
67
- attr_reader :socket_path
68
-
69
202
  # Global tags to be added to every statsd call. Defaults to no tags.
70
203
  attr_reader :tags
71
204
 
72
205
  # Buffer containing the statsd message before they are sent in batch
73
206
  attr_reader :buffer
74
207
 
75
- # Maximum number of metrics in the buffer before it is flushed
76
- attr_accessor :max_buffer_size
208
+ # Maximum buffer size in bytes before it is flushed
209
+ attr_reader :max_buffer_bytes
77
210
 
78
- class << self
79
- # Set to a standard logger instance to enable debug logging.
80
- attr_accessor :logger
81
- end
211
+ # Default sample rate
212
+ attr_reader :sample_rate
82
213
 
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
214
+ # Connection
215
+ attr_reader :connection
88
216
 
89
217
  # @param [String] host your statsd host
90
218
  # @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
219
+ # @option [String] namespace set a namespace to be prepended to every metric name
220
+ # @option [Array<String>|Hash] tags tags to be added to every metric
221
+ # @option [Logger] logger for debugging
222
+ # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
223
+ # @option [String] socket_path unix socket path
224
+ # @option [Float] default sample rate if not overridden
225
+ def initialize(
226
+ host = nil,
227
+ port = nil,
228
+ namespace: nil,
229
+ tags: nil,
230
+ max_buffer_bytes: 8192,
231
+ socket_path: nil,
232
+ logger: nil,
233
+ sample_rate: nil
234
+ )
235
+ if socket_path.nil?
236
+ @connection = UDPConnection.new(host, port, logger)
237
+ else
238
+ @connection = UDSConnection.new(socket_path, logger)
239
+ end
240
+ @logger = logger
104
241
 
105
- def namespace=(namespace) #:nodoc:
106
242
  @namespace = namespace
107
- @prefix = namespace.nil? ? nil : "#{namespace}.".freeze
108
- end
243
+ @prefix = @namespace ? "#{@namespace}.".freeze : nil
109
244
 
110
- def host=(host) #:nodoc:
111
- @host = host || DEFAULT_HOST
112
- end
245
+ @sample_rate = sample_rate
113
246
 
114
- def port=(port) #:nodoc:
115
- @port = port || DEFAULT_PORT
116
- end
247
+ unless tags.nil? or tags.is_a? Array or tags.is_a? Hash
248
+ raise ArgumentError, 'tags must be a Array<String> or a Hash'
249
+ end
117
250
 
118
- def tags=(tags) #:nodoc:
119
- raise ArgumentError, 'tags must be a Array<String>' unless tags.nil? or tags.is_a? Array
251
+ tags = tag_hash_to_array(tags) if tags.is_a? Hash
120
252
  @tags = (tags || []).compact.map! {|tag| escape_tag_content(tag)}
253
+
254
+ # append the entity id to tags if DD_ENTITY_ID env var is not nil
255
+ @tags << 'dd.internal.entity_id:' + escape_tag_content(ENV.fetch('DD_ENTITY_ID', nil)) unless ENV.fetch('DD_ENTITY_ID', nil).nil?
256
+
257
+ @batch = Batch.new @connection, max_buffer_bytes
258
+ end
259
+
260
+ # yield a new instance to a block and close it when done
261
+ # for short-term use-cases that don't want to close the socket manually
262
+ def self.open(*args)
263
+ instance = new(*args)
264
+ yield instance
265
+ ensure
266
+ instance.close
121
267
  end
122
268
 
123
269
  # Sends an increment (count = 1) for the given stat to the statsd server.
@@ -128,7 +274,7 @@ module Datadog
128
274
  # @option opts [Array<String>] :tags An array of tags
129
275
  # @option opts [Numeric] :by increment value, default 1
130
276
  # @see #count
131
- def increment(stat, opts={})
277
+ def increment(stat, opts=EMPTY_OPTIONS)
132
278
  opts = {:sample_rate => opts} if opts.is_a? Numeric
133
279
  incr_value = opts.fetch(:by, 1)
134
280
  count stat, incr_value, opts
@@ -142,7 +288,7 @@ module Datadog
142
288
  # @option opts [Array<String>] :tags An array of tags
143
289
  # @option opts [Numeric] :by decrement value, default 1
144
290
  # @see #count
145
- def decrement(stat, opts={})
291
+ def decrement(stat, opts=EMPTY_OPTIONS)
146
292
  opts = {:sample_rate => opts} if opts.is_a? Numeric
147
293
  decr_value = - opts.fetch(:by, 1)
148
294
  count stat, decr_value, opts
@@ -155,7 +301,7 @@ module Datadog
155
301
  # @param [Hash] opts the options to create the metric with
156
302
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
157
303
  # @option opts [Array<String>] :tags An array of tags
158
- def count(stat, count, opts={})
304
+ def count(stat, count, opts=EMPTY_OPTIONS)
159
305
  opts = {:sample_rate => opts} if opts.is_a? Numeric
160
306
  send_stats stat, count, COUNTER_TYPE, opts
161
307
  end
@@ -173,7 +319,7 @@ module Datadog
173
319
  # @option opts [Array<String>] :tags An array of tags
174
320
  # @example Report the current user count:
175
321
  # $statsd.gauge('user.count', User.count)
176
- def gauge(stat, value, opts={})
322
+ def gauge(stat, value, opts=EMPTY_OPTIONS)
177
323
  opts = {:sample_rate => opts} if opts.is_a? Numeric
178
324
  send_stats stat, value, GAUGE_TYPE, opts
179
325
  end
@@ -187,7 +333,7 @@ module Datadog
187
333
  # @option opts [Array<String>] :tags An array of tags
188
334
  # @example Report the current user count:
189
335
  # $statsd.histogram('user.count', User.count)
190
- def histogram(stat, value, opts={})
336
+ def histogram(stat, value, opts=EMPTY_OPTIONS)
191
337
  send_stats stat, value, HISTOGRAM_TYPE, opts
192
338
  end
193
339
 
@@ -203,7 +349,7 @@ module Datadog
203
349
  # @option opts [Array<String>] :tags An array of tags
204
350
  # @example Report the current user count:
205
351
  # $statsd.distribution('user.count', User.count)
206
- def distribution(stat, value, opts={})
352
+ def distribution(stat, value, opts=EMPTY_OPTIONS)
207
353
  send_stats stat, value, DISTRIBUTION_TYPE, opts
208
354
  end
209
355
 
@@ -217,7 +363,7 @@ module Datadog
217
363
  # @param [Hash] opts the options to create the metric with
218
364
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
219
365
  # @option opts [Array<String>] :tags An array of tags
220
- def timing(stat, ms, opts={})
366
+ def timing(stat, ms, opts=EMPTY_OPTIONS)
221
367
  opts = {:sample_rate => opts} if opts.is_a? Numeric
222
368
  send_stats stat, ms, TIMING_TYPE, opts
223
369
  end
@@ -235,7 +381,7 @@ module Datadog
235
381
  # @see #timing
236
382
  # @example Report the time (in ms) taken to activate an account
237
383
  # $statsd.time('account.activate') { @account.activate! }
238
- def time(stat, opts={})
384
+ def time(stat, opts=EMPTY_OPTIONS)
239
385
  opts = {:sample_rate => opts} if opts.is_a? Numeric
240
386
  start = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
241
387
  return yield
@@ -243,6 +389,7 @@ module Datadog
243
389
  finished = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
244
390
  timing(stat, ((finished - start) * 1000).round, opts)
245
391
  end
392
+
246
393
  # Sends a value to be tracked as a set to the statsd server.
247
394
  #
248
395
  # @param [String] stat stat name.
@@ -252,7 +399,7 @@ module Datadog
252
399
  # @option opts [Array<String>] :tags An array of tags
253
400
  # @example Record a unique visitory by id:
254
401
  # $statsd.set('visitors.uniques', User.id)
255
- def set(stat, value, opts={})
402
+ def set(stat, value, opts=EMPTY_OPTIONS)
256
403
  opts = {:sample_rate => opts} if opts.is_a? Numeric
257
404
  send_stats stat, value, SET_TYPE, opts
258
405
  end
@@ -262,37 +409,14 @@ module Datadog
262
409
  # @param [String] name Service check name
263
410
  # @param [String] status Service check status.
264
411
  # @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.
412
+ # @option opts [Integer, String, nil] :timestamp (nil) Assign a timestamp to the service check. Default is now when none
413
+ # @option opts [String, nil] :hostname (nil) Assign a hostname to the service check.
267
414
  # @option opts [Array<String>, nil] :tags (nil) An array of tags
268
415
  # @option opts [String, nil] :message (nil) A message to associate with this service check status
269
416
  # @example Report a critical service check status
270
417
  # $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}"
278
-
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
418
+ def service_check(name, status, opts=EMPTY_OPTIONS)
419
+ send_stat format_service_check(name, status, opts)
296
420
  end
297
421
 
298
422
  # 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,9 +426,9 @@ module Datadog
302
426
  # it will be grouped with other events that don't have an event type.
303
427
  #
304
428
  # @param [String] title Event title
305
- # @param [String] text Event text. Supports \n
429
+ # @param [String] text Event text. Supports newlines (+\n+)
306
430
  # @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
431
+ # @option opts [Integer, String, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
308
432
  # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
309
433
  # @option opts [String, nil] :aggregation_key (nil) Assign an aggregation key to the event, to group it with some others
310
434
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
@@ -313,11 +437,8 @@ module Datadog
313
437
  # @option opts [Array<String>] :tags tags to be added to every metric
314
438
  # @example Report an awful event:
315
439
  # $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
319
-
320
- send_to_socket event_string
440
+ def event(title, text, opts=EMPTY_OPTIONS)
441
+ send_stat format_event(title, text, opts)
321
442
  end
322
443
 
323
444
  # Send several metrics in the same UDP Packet
@@ -328,24 +449,73 @@ module Datadog
328
449
  # s.gauge('users.online',156)
329
450
  # s.increment('page.views')
330
451
  # end
331
- def batch()
332
- @batch_nesting_depth += 1
333
- yield self
334
- ensure
335
- @batch_nesting_depth -= 1
336
- flush_buffer if @batch_nesting_depth == 0
452
+ def batch
453
+ @batch.open { yield self }
454
+ end
455
+
456
+ # Close the underlying socket
457
+ def close
458
+ @connection.close
459
+ end
460
+
461
+ private
462
+
463
+ NEW_LINE = "\n".freeze
464
+ ESC_NEW_LINE = "\\n".freeze
465
+ COMMA = ",".freeze
466
+ PIPE = "|".freeze
467
+ DOT = ".".freeze
468
+ DOUBLE_COLON = "::".freeze
469
+ UNDERSCORE = "_".freeze
470
+ PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= "2.1.0")
471
+ EMPTY_OPTIONS = {}.freeze
472
+
473
+ private_constant :NEW_LINE, :ESC_NEW_LINE, :COMMA, :PIPE, :DOT,
474
+ :DOUBLE_COLON, :UNDERSCORE, :EMPTY_OPTIONS
475
+
476
+ def format_service_check(name, status, opts=EMPTY_OPTIONS)
477
+ sc_string = "_sc|#{name}|#{status}".dup
478
+
479
+ SC_OPT_KEYS.each do |key, shorthand_key|
480
+ next unless opts[key]
481
+
482
+ if key == :tags
483
+ if tags_string = tags_as_string(opts)
484
+ sc_string << "|##{tags_string}"
485
+ end
486
+ elsif key == :message
487
+ message = remove_pipes(opts[:message])
488
+ escaped_message = escape_service_check_message(message)
489
+ sc_string << "|m:#{escaped_message}"
490
+ else
491
+ if key == :timestamp && opts[key].is_a?(Integer)
492
+ value = opts[key]
493
+ else
494
+ value = remove_pipes(opts[key])
495
+ end
496
+ sc_string << "|#{shorthand_key}#{value}"
497
+ end
498
+ end
499
+ sc_string
337
500
  end
338
501
 
339
- def format_event(title, text, opts={})
502
+ def format_event(title, text, opts=EMPTY_OPTIONS)
340
503
  escaped_title = escape_event_content(title)
341
504
  escaped_text = escape_event_content(text)
342
- event_string_data = "_e{#{escaped_title.length},#{escaped_text.length}}:#{escaped_title}|#{escaped_text}"
505
+ event_string_data = "_e{#{escaped_title.bytesize},#{escaped_text.bytesize}}:#{escaped_title}|#{escaped_text}".dup
343
506
 
344
507
  # We construct the string to be sent by adding '|key:value' parts to it when needed
345
508
  # All pipes ('|') in the metadata are removed. Title and Text can keep theirs
346
509
  OPTS_KEYS.each do |key, shorthand_key|
347
510
  if key != :tags && opts[key]
348
- value = remove_pipes(opts[key])
511
+ # :date_happened is the only key where the value is an Integer
512
+ # To not break backwards compatibility, we still accept a String
513
+ if key == :date_happened && opts[key].is_a?(Integer)
514
+ value = opts[key]
515
+ # All other keys only have String values
516
+ else
517
+ value = remove_pipes(opts[key])
518
+ end
349
519
  event_string_data << "|#{shorthand_key}:#{value}"
350
520
  end
351
521
  end
@@ -355,36 +525,25 @@ module Datadog
355
525
  event_string_data << "|##{tags_string}"
356
526
  end
357
527
 
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
360
- end
361
-
362
- # Close the underlying socket
363
- def close()
364
- @socket.close
528
+ raise "Event #{title} payload is too big (more that 8KB), event discarded" if event_string_data.bytesize > MAX_EVENT_SIZE
529
+ event_string_data
365
530
  end
366
531
 
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
532
  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
533
+ if tag_arr = opts[:tags]
534
+ tag_arr = tag_hash_to_array(tag_arr) if tag_arr.is_a? Hash
535
+ tag_arr = tag_arr.map { |tag| escape_tag_content(tag) }
536
+ tag_arr = tags + tag_arr # @tags are normalized when set, so not need to normalize them again
537
+ else
538
+ tag_arr = tags
539
+ end
385
540
  tag_arr.join(COMMA) unless tag_arr.empty?
386
541
  end
387
542
 
543
+ def tag_hash_to_array(tag_hash)
544
+ tag_hash.to_a.map {|pair| pair.compact.join(":")}
545
+ end
546
+
388
547
  def escape_event_content(msg)
389
548
  msg.gsub NEW_LINE, ESC_NEW_LINE
390
549
  end
@@ -403,10 +562,10 @@ module Datadog
403
562
  escape_event_content(msg).gsub('m:'.freeze, 'm\:'.freeze)
404
563
  end
405
564
 
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 = ''
565
+ def send_stats(stat, delta, type, opts=EMPTY_OPTIONS)
566
+ sample_rate = opts[:sample_rate] || @sample_rate || 1
567
+ if sample_rate == 1 or rand <= sample_rate
568
+ full_stat = ''.dup
410
569
  full_stat << @prefix if @prefix
411
570
 
412
571
  stat = stat.is_a?(String) ? stat.dup : stat.to_s
@@ -437,62 +596,11 @@ module Datadog
437
596
  end
438
597
 
439
598
  def send_stat(message)
440
- if @batch_nesting_depth > 0
441
- @buffer << message
442
- flush_buffer if @buffer.length >= @max_buffer_size
599
+ if @batch.open?
600
+ @batch.add message
443
601
  else
444
- send_to_socket(message)
445
- end
446
- end
447
-
448
- def flush_buffer
449
- return @buffer if @buffer.empty?
450
- send_to_socket(@buffer.join(NEW_LINE))
451
- @buffer = Array.new
452
- end
453
-
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)
602
+ @connection.write(message)
461
603
  end
462
- socket
463
- end
464
-
465
- def sock
466
- @socket ||= connect_to_socket
467
- end
468
-
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
493
-
494
- self.class.logger.error { "Statsd: #{boom.class} #{boom}" } if self.class.logger
495
- nil
496
604
  end
497
605
  end
498
606
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dogstatsd-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.3.0
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rein Henrichs
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-04 00:00:00.000000000 Z
11
+ date: 2019-08-22 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby DogStastd client
14
14
  email: code@datadoghq.com
@@ -33,7 +33,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
33
33
  requirements:
34
34
  - - ">="
35
35
  - !ruby/object:Gem::Version
36
- version: '0'
36
+ version: 2.0.0
37
37
  required_rubygems_version: !ruby/object:Gem::Requirement
38
38
  requirements:
39
39
  - - ">="
@@ -41,7 +41,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
41
41
  version: '0'
42
42
  requirements: []
43
43
  rubyforge_project:
44
- rubygems_version: 2.7.1
44
+ rubygems_version: 2.7.6
45
45
  signing_key:
46
46
  specification_version: 4
47
47
  summary: A Ruby DogStatsd client