dogstatsd-ruby 4.2.0 → 4.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c8e8d0982550cf2f3e3dddee6eee8dcf88534ccf4e26ef47731c2866b85e81f2
4
- data.tar.gz: 8f9d33a02a6b86e343144c98511e243a65bdaeb2f881ae00cd7842528158f3f6
3
+ metadata.gz: 57d7660dad73ad8c0c5f37f6d6130474d255beaa467aca03f532e552d0b608e0
4
+ data.tar.gz: 30c8686d3045e4cfb2ca18c57390f78dbcccba1ff941deb170d89497953d5e84
5
5
  SHA512:
6
- metadata.gz: 68e6cc9c4bbbbffa546eaefd643785ca945ae00dfd03d84d5984262d0061f25b059c325063220fd15bdd354d6ec721ba7d50ad7b7c2310d2b64c24b49314ba36
7
- data.tar.gz: c070c0fd60a513b3c89a8eff5c4c755e38b5d646529840adca886a52651831f7bef1bdf437723329237f1faf4066960bee32ae5e5b6b9f4ddeb1eb69bfb87afe
6
+ metadata.gz: 99214bb827186d3b01568cc59064f7d2a8d043a4a739c7f90a2b013feeeead11ed363937c37d2c29846d101ed886c318a6369c2d5a57292b0c058db81c708813
7
+ data.tar.gz: c82e6c5e9668804688fd308e091398e05ada88dac8ea78a678ee2da671eaf297ffd95997efaf376506632f65e88b4096a6a9b2590667f772f19c6b020e238827
data/README.md CHANGED
@@ -1,95 +1,77 @@
1
+ # dogstatsd-ruby
1
2
 
2
- dogstatsd-ruby
3
- ==============
3
+ A client for DogStatsD, an extension of the StatsD metric server for Datadog. Full API documentation is available in [DogStatsD-ruby rubydoc](https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/Datadog/Statsd).
4
4
 
5
- A client for DogStatsD, an extension of the StatsD metric server for Datadog.
5
+ [![Build Status](https://secure.travis-ci.org/DataDog/dogstatsd-ruby.svg)](http://travis-ci.org/DataDog/dogstatsd-ruby)
6
6
 
7
- [![Build Status](https://secure.travis-ci.org/DataDog/dogstatsd-ruby.png)](http://travis-ci.org/DataDog/dogstatsd-ruby)
7
+ See [CHANGELOG.md](CHANGELOG.md) for changes. To suggest a feature, report a bug, or general discussion, [open an issue](http://github.com/DataDog/dogstatsd-ruby/issues/).
8
8
 
9
- Quick Start Guide
10
- -----------------
9
+ ## Installation
11
10
 
12
11
  First install the library:
13
12
 
14
- gem install dogstatsd-ruby
13
+ ```
14
+ gem install dogstatsd-ruby
15
+ ```
16
+
17
+ ## Configuration
15
18
 
16
- Then start instrumenting your code:
19
+ To instantiate a DogStatsd client:
17
20
 
18
- ``` ruby
19
- # Load the dogstats module.
21
+ ```ruby
22
+ # Import the library
20
23
  require 'datadog/statsd'
21
24
 
22
- # Create a stats instance.
25
+ # Create a DogStatsD client instance.
23
26
  statsd = Datadog::Statsd.new('localhost', 8125)
24
-
25
- # you could also create a statsd class if you need a drop in replacement
26
- # class Statsd < Datadog::Statsd
27
- # end
28
-
29
- # Increment a counter.
30
- statsd.increment('page.views')
31
-
32
- # Record a gauge 50% of the time.
33
- statsd.gauge('users.online', 123, :sample_rate=>0.5)
34
-
35
- # Sample a histogram
36
- statsd.histogram('file.upload.size', 1234)
37
-
38
- # Time a block of code
39
- statsd.time('page.render') do
40
- render_page('home.html')
41
- end
42
-
43
- # Send several metrics at the same time
44
- # All metrics will be buffered and sent in one packet when the block completes
45
- statsd.batch do |s|
46
- s.increment('page.views')
47
- s.gauge('users.online', 123)
48
- end
49
-
50
- # Tag a metric.
51
- statsd.histogram('query.time', 10, :tags => ["version:1"])
52
-
53
- # Auto-close socket after end of block
54
- Datadog::Statsd.open('localhost', 8125) do |s|
55
- s.increment('page.views')
56
- end
27
+ ```
28
+ Or if you want to connect over Unix Domain Socket:
29
+ ```ruby
30
+ # Connection over Unix Domain Socket
31
+ statsd = Datadog::Statsd.new(socket_path: '/path/to/socket/file')
57
32
  ```
58
33
 
59
- You can also post events to your stream. You can tag them, set priority and even aggregate them with other events.
34
+ Find a list of all the available options for your DogStatsD Client in the [DogStatsD-ruby rubydoc](https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/Datadog/Statsd) or in the [Datadog public DogStatsD documentation](https://docs.datadoghq.com/developers/dogstatsd/?tab=ruby#client-instantiation-parameters).
60
35
 
61
- Aggregation in the stream is made on hostname/event_type/source_type/aggregation_key.
36
+ ### Origin detection over UDP
62
37
 
63
- ``` ruby
64
- # Post a simple message
65
- statsd.event("There might be a storm tomorrow", "A friend warned me earlier.")
38
+ 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.
66
39
 
67
- # Cry for help
68
- statsd.event("SO MUCH SNOW", "Started yesterday and it won't stop !!", :alert_type => "error", :tags => ["urgent", "endoftheworld"])
40
+ To enable origin detection over UDP, add the following lines to your application manifest
41
+ ```yaml
42
+ env:
43
+ - name: DD_ENTITY_ID
44
+ valueFrom:
45
+ fieldRef:
46
+ fieldPath: metadata.uid
69
47
  ```
48
+ 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.
70
49
 
50
+ ## Usage
71
51
 
52
+ In order to use DogStatsD metrics, events, and Service Checks the Agent must be [running and available](https://docs.datadoghq.com/developers/dogstatsd/?tab=ruby).
72
53
 
73
- Documentation
74
- -------------
54
+ ### Metrics
75
55
 
76
- Full API documentation is available
77
- [here](http://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/frames).
56
+ After the client is created, you can start sending custom metrics to Datadog. See the dedicated [Metric Submission: DogStatsD documentation](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby) to see how to submit all supported metric types to Datadog with working code examples:
78
57
 
58
+ * [Submit a COUNT metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#count).
59
+ * [Submit a GAUGE metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#gauge).
60
+ * [Submit a SET metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#set)
61
+ * [Submit a HISTOGRAM metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#histogram)
62
+ * [Submit a DISTRIBUTION metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#distribution)
79
63
 
80
- Feedback
81
- --------
64
+ Some options are suppported when submitting metrics, like [applying a Sample Rate to your metrics](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#metric-submission-options) or [tagging your metrics with your custom tags](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#metric-tagging). Find all the available functions to report metrics in the [DogStatsD-ruby rubydoc](https://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/Datadog/Statsd).
82
65
 
83
- To suggest a feature, report a bug, or general discussion, head over
84
- [here](http://github.com/DataDog/dogstatsd-ruby/issues/).
66
+ ### Events
85
67
 
68
+ After the client is created, you can start sending events to your Datadog Event Stream. See the dedicated [Event Submission: DogStatsD documentation](https://docs.datadoghq.com/developers/events/dogstatsd/?tab=ruby) to see how to submit an event to Datadog your Event Stream.
86
69
 
87
- [Change Log](CHANGELOG.md)
88
- ----------------------------
70
+ ### Service Checks
89
71
 
72
+ After the client is created, you can start sending Service Checks to Datadog. See the dedicated [Service Check Submission: DogStatsD documentation](https://docs.datadoghq.com/developers/service_checks/dogstatsd_service_checks_submission/?tab=ruby) to see how to submit a Service Check to Datadog.
90
73
 
91
- Credits
92
- -------
74
+ ## Credits
93
75
 
94
76
  dogstatsd-ruby is forked from Rien Henrichs [original Statsd
95
77
  client](https://github.com/reinh/statsd).
@@ -1,6 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
  require 'socket'
3
3
 
4
+ require_relative 'statsd/telemetry'
5
+ require_relative 'statsd/udp_connection'
6
+ require_relative 'statsd/uds_connection'
7
+ require_relative 'statsd/batch'
8
+
4
9
  # = Datadog::Statsd: A DogStatsd client (https://www.datadoghq.com)
5
10
  #
6
11
  # @example Set up a global Statsd client for a server on localhost:8125
@@ -19,169 +24,42 @@ require 'socket'
19
24
  # statsd = Datadog::Statsd.new 'localhost', 8125, tags: 'tag1:true'
20
25
  module Datadog
21
26
  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 || ENV.fetch('DD_AGENT_HOST', nil) || DEFAULT_HOST
38
- @port = port || ENV.fetch('DD_DOGSTATSD_PORT', nil) || 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?(Errno::ENOTCONN) or
65
- retries <= 1 && boom.is_a?(IOError) && boom.message =~ /closed stream/i
66
- retries += 1
67
- begin
68
- @socket = connect
69
- retry
70
- rescue StandardError => e
71
- boom = e
72
- end
73
- end
74
-
75
- @logger.error { "Statsd: #{boom.class} #{boom}" } if @logger
76
- nil
77
- end
78
-
79
- # Close the underlying socket
80
- def close
81
- @socket && @socket.close
82
- end
83
-
84
- private
85
-
86
- def socket
87
- @socket ||= connect
88
- end
89
-
90
- def connect
91
- if @socket_path.nil?
92
- socket = UDPSocket.new
93
- socket.connect(@host, @port)
94
- else
95
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
96
- socket.connect(Socket.pack_sockaddr_un(@socket_path))
97
- end
98
- socket
99
- end
100
- end
101
-
102
- class Batch
103
- def initialize(connection, max_buffer_bytes)
104
- @connection = connection
105
- @max_buffer_bytes = max_buffer_bytes
106
- @depth = 0
107
- reset
108
- end
109
-
110
- def open
111
- @depth += 1
112
- yield
113
- ensure
114
- @depth -= 1
115
- flush if !open?
116
- end
117
-
118
- def open?
119
- @depth > 0
120
- end
121
-
122
- def add(message)
123
- message_bytes = message.bytesize
124
-
125
- unless @buffer_bytes == 0
126
- if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
127
- flush
128
- else
129
- @buffer << NEW_LINE
130
- @buffer_bytes += 1
131
- end
132
- end
133
-
134
- @buffer << message
135
- @buffer_bytes += message_bytes
136
- end
137
-
138
- def flush
139
- return if @buffer_bytes == 0
140
- @connection.write @buffer
141
- reset
142
- end
143
-
144
- private
145
-
146
- def reset
147
- @buffer = String.new
148
- @buffer_bytes = 0
149
- end
150
- end
151
-
152
27
  # Create a dictionary to assign a key to every parameter's name, except for tags (treated differently)
153
28
  # Goal: Simple and fast to add some other parameters
154
29
  OPTS_KEYS = {
155
- :date_happened => :d,
156
- :hostname => :h,
157
- :aggregation_key => :k,
158
- :priority => :p,
159
- :source_type_name => :s,
160
- :alert_type => :t,
161
- }
30
+ date_happened: :d,
31
+ hostname: :h,
32
+ aggregation_key: :k,
33
+ priority: :p,
34
+ source_type_name: :s,
35
+ alert_type: :t,
36
+ }.freeze
162
37
 
163
38
  # Service check options
164
39
  SC_OPT_KEYS = {
165
- :timestamp => 'd:'.freeze,
166
- :hostname => 'h:'.freeze,
167
- :tags => '#'.freeze,
168
- :message => 'm:'.freeze,
169
- }
170
-
171
- OK = 0
172
- WARNING = 1
173
- CRITICAL = 2
174
- UNKNOWN = 3
175
-
176
- MAX_EVENT_SIZE = 8 * 1024
177
-
178
- COUNTER_TYPE = 'c'.freeze
179
- GAUGE_TYPE = 'g'.freeze
180
- HISTOGRAM_TYPE = 'h'.freeze
181
- DISTRIBUTION_TYPE = 'd'.freeze
182
- TIMING_TYPE = 'ms'.freeze
183
- SET_TYPE = 's'.freeze
184
- VERSION = "4.2.0".freeze
40
+ timestamp: 'd:',
41
+ hostname: 'h:',
42
+ tags: '#',
43
+ message: 'm:',
44
+ }.freeze
45
+
46
+ OK = 0
47
+ WARNING = 1
48
+ CRITICAL = 2
49
+ UNKNOWN = 3
50
+
51
+ DEFAULT_BUFFER_SIZE = 8 * 1_024
52
+ MAX_EVENT_SIZE = 8 * 1_024
53
+ # minimum flush interval for the telemetry in seconds
54
+ DEFAULT_TELEMETRY_FLUSH_INTERVAL = 10
55
+
56
+ COUNTER_TYPE = 'c'
57
+ GAUGE_TYPE = 'g'
58
+ HISTOGRAM_TYPE = 'h'
59
+ DISTRIBUTION_TYPE = 'd'
60
+ TIMING_TYPE = 'ms'
61
+ SET_TYPE = 's'
62
+ VERSION = '4.7.0'
185
63
 
186
64
  # A namespace to prepend to all statsd calls. Defaults to no namespace.
187
65
  attr_reader :namespace
@@ -195,44 +73,73 @@ module Datadog
195
73
  # Maximum buffer size in bytes before it is flushed
196
74
  attr_reader :max_buffer_bytes
197
75
 
76
+ # Default sample rate
77
+ attr_reader :sample_rate
78
+
198
79
  # Connection
199
80
  attr_reader :connection
200
81
 
201
82
  # @param [String] host your statsd host
202
83
  # @param [Integer] port your statsd port
203
84
  # @option [String] namespace set a namespace to be prepended to every metric name
204
- # @option [Array<String>] tags tags to be added to every metric
205
- # @option [Loger] logger for debugging
85
+ # @option [Array<String>|Hash] tags tags to be added to every metric
86
+ # @option [Logger] logger for debugging
206
87
  # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
207
88
  # @option [String] socket_path unix socket path
89
+ # @option [Float] default sample rate if not overridden
208
90
  def initialize(
209
91
  host = nil,
210
92
  port = nil,
211
93
  namespace: nil,
212
94
  tags: nil,
213
- max_buffer_bytes: 8192,
95
+ max_buffer_bytes: DEFAULT_BUFFER_SIZE,
214
96
  socket_path: nil,
215
- logger: nil
97
+ logger: nil,
98
+ sample_rate: nil,
99
+ disable_telemetry: false,
100
+ telemetry_flush_interval: DEFAULT_TELEMETRY_FLUSH_INTERVAL
216
101
  )
217
- @connection = Connection.new(host, port, socket_path, logger)
102
+ unless tags.nil? || tags.is_a?(Array) || tags.is_a?(Hash)
103
+ raise ArgumentError, 'tags must be a Array<String> or a Hash'
104
+ end
105
+
106
+ tags = tag_hash_to_array(tags) if tags.is_a?(Hash)
107
+ @tags = (tags || []).compact.map! do |tag|
108
+ escape_tag_content(tag)
109
+ end
110
+
111
+ # append the entity id to tags if DD_ENTITY_ID env var is not nil
112
+ unless ENV.fetch('DD_ENTITY_ID', nil).nil?
113
+ dd_entity = escape_tag_content(ENV.fetch('DD_ENTITY_ID', nil))
114
+ @tags << 'dd.internal.entity_id:' + dd_entity
115
+ end
116
+
117
+ # init telemetry
118
+ transport_type = socket_path.nil? ? 'udp': 'uds'
119
+ telemetry_tags = (["client:ruby", "client_version:#{VERSION}", "client_transport:#{transport_type}"] + @tags).join(COMMA).freeze
120
+ @telemetry = Telemetry.new(disable_telemetry, telemetry_tags, telemetry_flush_interval)
121
+
122
+ if socket_path.nil?
123
+ @connection = UDPConnection.new(host, port, logger, @telemetry)
124
+ else
125
+ @connection = UDSConnection.new(socket_path, logger, @telemetry)
126
+ end
218
127
  @logger = logger
219
128
 
220
129
  @namespace = namespace
221
130
  @prefix = @namespace ? "#{@namespace}.".freeze : nil
222
131
 
223
- raise ArgumentError, 'tags must be a Array<String>' unless tags.nil? or tags.is_a? Array
224
- @tags = (tags || []).compact.map! {|tag| escape_tag_content(tag)}
225
-
226
- # append the entity id to tags if DD_ENTITY_ID env var is not nil
227
- @tags << 'dd.internal.entity_id:' + escape_tag_content(ENV.fetch('DD_ENTITY_ID', nil)) unless ENV.fetch('DD_ENTITY_ID', nil).nil?
132
+ @sample_rate = sample_rate
228
133
 
229
- @batch = Batch.new @connection, max_buffer_bytes
134
+ # we reduce max_buffer_bytes by a the rough estimate of the telemetry payload
135
+ @batch = Batch.new(@connection, (max_buffer_bytes - @telemetry.estimate_max_size))
230
136
  end
231
137
 
232
138
  # yield a new instance to a block and close it when done
233
139
  # for short-term use-cases that don't want to close the socket manually
234
140
  def self.open(*args)
235
141
  instance = new(*args)
142
+
236
143
  yield instance
237
144
  ensure
238
145
  instance.close
@@ -246,10 +153,10 @@ module Datadog
246
153
  # @option opts [Array<String>] :tags An array of tags
247
154
  # @option opts [Numeric] :by increment value, default 1
248
155
  # @see #count
249
- def increment(stat, opts=EMPTY_OPTIONS)
250
- opts = {:sample_rate => opts} if opts.is_a? Numeric
156
+ def increment(stat, opts = EMPTY_OPTIONS)
157
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
251
158
  incr_value = opts.fetch(:by, 1)
252
- count stat, incr_value, opts
159
+ count(stat, incr_value, opts)
253
160
  end
254
161
 
255
162
  # Sends a decrement (count = -1) for the given stat to the statsd server.
@@ -260,10 +167,10 @@ module Datadog
260
167
  # @option opts [Array<String>] :tags An array of tags
261
168
  # @option opts [Numeric] :by decrement value, default 1
262
169
  # @see #count
263
- def decrement(stat, opts=EMPTY_OPTIONS)
264
- opts = {:sample_rate => opts} if opts.is_a? Numeric
170
+ def decrement(stat, opts = EMPTY_OPTIONS)
171
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
265
172
  decr_value = - opts.fetch(:by, 1)
266
- count stat, decr_value, opts
173
+ count(stat, decr_value, opts)
267
174
  end
268
175
 
269
176
  # Sends an arbitrary count for the given stat to the statsd server.
@@ -273,9 +180,9 @@ module Datadog
273
180
  # @param [Hash] opts the options to create the metric with
274
181
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
275
182
  # @option opts [Array<String>] :tags An array of tags
276
- def count(stat, count, opts=EMPTY_OPTIONS)
277
- opts = {:sample_rate => opts} if opts.is_a? Numeric
278
- send_stats stat, count, COUNTER_TYPE, opts
183
+ def count(stat, count, opts = EMPTY_OPTIONS)
184
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
185
+ send_stats(stat, count, COUNTER_TYPE, opts)
279
186
  end
280
187
 
281
188
  # Sends an arbitary gauge value for the given stat to the statsd server.
@@ -291,9 +198,9 @@ module Datadog
291
198
  # @option opts [Array<String>] :tags An array of tags
292
199
  # @example Report the current user count:
293
200
  # $statsd.gauge('user.count', User.count)
294
- def gauge(stat, value, opts=EMPTY_OPTIONS)
295
- opts = {:sample_rate => opts} if opts.is_a? Numeric
296
- send_stats stat, value, GAUGE_TYPE, opts
201
+ def gauge(stat, value, opts = EMPTY_OPTIONS)
202
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
203
+ send_stats(stat, value, GAUGE_TYPE, opts)
297
204
  end
298
205
 
299
206
  # Sends a value to be tracked as a histogram to the statsd server.
@@ -305,14 +212,11 @@ module Datadog
305
212
  # @option opts [Array<String>] :tags An array of tags
306
213
  # @example Report the current user count:
307
214
  # $statsd.histogram('user.count', User.count)
308
- def histogram(stat, value, opts=EMPTY_OPTIONS)
309
- send_stats stat, value, HISTOGRAM_TYPE, opts
215
+ def histogram(stat, value, opts = EMPTY_OPTIONS)
216
+ send_stats(stat, value, HISTOGRAM_TYPE, opts)
310
217
  end
311
218
 
312
219
  # Sends a value to be tracked as a distribution to the statsd server.
313
- # Note: Distributions are a beta feature of Datadog and not generally
314
- # available. Distributions must be specifically enabled for your
315
- # organization.
316
220
  #
317
221
  # @param [String] stat stat name.
318
222
  # @param [Numeric] value distribution value.
@@ -321,8 +225,8 @@ module Datadog
321
225
  # @option opts [Array<String>] :tags An array of tags
322
226
  # @example Report the current user count:
323
227
  # $statsd.distribution('user.count', User.count)
324
- def distribution(stat, value, opts=EMPTY_OPTIONS)
325
- send_stats stat, value, DISTRIBUTION_TYPE, opts
228
+ def distribution(stat, value, opts = EMPTY_OPTIONS)
229
+ send_stats(stat, value, DISTRIBUTION_TYPE, opts)
326
230
  end
327
231
 
328
232
  # Sends a timing (in ms) for the given stat to the statsd server. The
@@ -335,9 +239,9 @@ module Datadog
335
239
  # @param [Hash] opts the options to create the metric with
336
240
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
337
241
  # @option opts [Array<String>] :tags An array of tags
338
- def timing(stat, ms, opts=EMPTY_OPTIONS)
339
- opts = {:sample_rate => opts} if opts.is_a? Numeric
340
- send_stats stat, ms, TIMING_TYPE, opts
242
+ def timing(stat, ms, opts = EMPTY_OPTIONS)
243
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
244
+ send_stats(stat, ms, TIMING_TYPE, opts)
341
245
  end
342
246
 
343
247
  # Reports execution time of the provided block using {#timing}.
@@ -353,12 +257,21 @@ module Datadog
353
257
  # @see #timing
354
258
  # @example Report the time (in ms) taken to activate an account
355
259
  # $statsd.time('account.activate') { @account.activate! }
356
- def time(stat, opts=EMPTY_OPTIONS)
357
- opts = {:sample_rate => opts} if opts.is_a? Numeric
358
- start = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
359
- return yield
260
+ def time(stat, opts = EMPTY_OPTIONS)
261
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
262
+ start = if PROCESS_TIME_SUPPORTED
263
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) # uncovered
264
+ else
265
+ Time.now.to_f # uncovered
266
+ end
267
+ yield
360
268
  ensure
361
- finished = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
269
+ finished = if PROCESS_TIME_SUPPORTED
270
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) # uncovered
271
+ else
272
+ Time.now.to_f # uncovered
273
+ end
274
+
362
275
  timing(stat, ((finished - start) * 1000).round, opts)
363
276
  end
364
277
 
@@ -371,9 +284,9 @@ module Datadog
371
284
  # @option opts [Array<String>] :tags An array of tags
372
285
  # @example Record a unique visitory by id:
373
286
  # $statsd.set('visitors.uniques', User.id)
374
- def set(stat, value, opts=EMPTY_OPTIONS)
375
- opts = {:sample_rate => opts} if opts.is_a? Numeric
376
- send_stats stat, value, SET_TYPE, opts
287
+ def set(stat, value, opts = EMPTY_OPTIONS)
288
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
289
+ send_stats(stat, value, SET_TYPE, opts)
377
290
  end
378
291
 
379
292
  # This method allows you to send custom service check statuses.
@@ -381,14 +294,15 @@ module Datadog
381
294
  # @param [String] name Service check name
382
295
  # @param [String] status Service check status.
383
296
  # @param [Hash] opts the additional data about the service check
384
- # @option opts [Integer, nil] :timestamp (nil) Assign a timestamp to the event. Default is now when none
385
- # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
297
+ # @option opts [Integer, String, nil] :timestamp (nil) Assign a timestamp to the service check. Default is now when none
298
+ # @option opts [String, nil] :hostname (nil) Assign a hostname to the service check.
386
299
  # @option opts [Array<String>, nil] :tags (nil) An array of tags
387
300
  # @option opts [String, nil] :message (nil) A message to associate with this service check status
388
301
  # @example Report a critical service check status
389
302
  # $statsd.service_check('my.service.check', Statsd::CRITICAL, :tags=>['urgent'])
390
- def service_check(name, status, opts=EMPTY_OPTIONS)
391
- send_stat format_service_check(name, status, opts)
303
+ def service_check(name, status, opts = EMPTY_OPTIONS)
304
+ @telemetry.service_checks += 1
305
+ send_stat(format_service_check(name, status, opts))
392
306
  end
393
307
 
394
308
  # This end point allows you to post events to the stream. You can tag them, set priority and even aggregate them with other events.
@@ -398,9 +312,9 @@ module Datadog
398
312
  # it will be grouped with other events that don't have an event type.
399
313
  #
400
314
  # @param [String] title Event title
401
- # @param [String] text Event text. Supports \n
315
+ # @param [String] text Event text. Supports newlines (+\n+)
402
316
  # @param [Hash] opts the additional data about the event
403
- # @option opts [Integer, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
317
+ # @option opts [Integer, String, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
404
318
  # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
405
319
  # @option opts [String, nil] :aggregation_key (nil) Assign an aggregation key to the event, to group it with some others
406
320
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
@@ -409,8 +323,9 @@ module Datadog
409
323
  # @option opts [Array<String>] :tags tags to be added to every metric
410
324
  # @example Report an awful event:
411
325
  # $statsd.event('Something terrible happened', 'The end is near if we do nothing', :alert_type=>'warning', :tags=>['end_of_times','urgent'])
412
- def event(title, text, opts=EMPTY_OPTIONS)
413
- send_stat format_event(title, text, opts)
326
+ def event(title, text, opts = EMPTY_OPTIONS)
327
+ @telemetry.events += 1
328
+ send_stat(format_event(title, text, opts))
414
329
  end
415
330
 
416
331
  # Send several metrics in the same UDP Packet
@@ -422,7 +337,9 @@ module Datadog
422
337
  # s.increment('page.views')
423
338
  # end
424
339
  def batch
425
- @batch.open { yield self }
340
+ @batch.open do
341
+ yield self
342
+ end
426
343
  end
427
344
 
428
345
  # Close the underlying socket
@@ -432,20 +349,20 @@ module Datadog
432
349
 
433
350
  private
434
351
 
435
- NEW_LINE = "\n".freeze
436
- ESC_NEW_LINE = "\\n".freeze
437
- COMMA = ",".freeze
438
- PIPE = "|".freeze
439
- DOT = ".".freeze
440
- DOUBLE_COLON = "::".freeze
441
- UNDERSCORE = "_".freeze
442
- PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= "2.1.0")
352
+ NEW_LINE = "\n"
353
+ ESC_NEW_LINE = '\n'
354
+ COMMA = ','
355
+ PIPE = '|'
356
+ DOT = '.'
357
+ DOUBLE_COLON = '::'
358
+ UNDERSCORE = '_'
359
+ PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= '2.1.0')
443
360
  EMPTY_OPTIONS = {}.freeze
444
361
 
445
362
  private_constant :NEW_LINE, :ESC_NEW_LINE, :COMMA, :PIPE, :DOT,
446
363
  :DOUBLE_COLON, :UNDERSCORE, :EMPTY_OPTIONS
447
364
 
448
- def format_service_check(name, status, opts=EMPTY_OPTIONS)
365
+ def format_service_check(name, status, opts = EMPTY_OPTIONS)
449
366
  sc_string = "_sc|#{name}|#{status}".dup
450
367
 
451
368
  SC_OPT_KEYS.each do |key, shorthand_key|
@@ -460,23 +377,34 @@ module Datadog
460
377
  escaped_message = escape_service_check_message(message)
461
378
  sc_string << "|m:#{escaped_message}"
462
379
  else
463
- value = remove_pipes(opts[key])
380
+ if key == :timestamp && opts[key].is_a?(Integer)
381
+ value = opts[key]
382
+ else
383
+ value = remove_pipes(opts[key])
384
+ end
464
385
  sc_string << "|#{shorthand_key}#{value}"
465
386
  end
466
387
  end
467
388
  sc_string
468
389
  end
469
390
 
470
- def format_event(title, text, opts=EMPTY_OPTIONS)
391
+ def format_event(title, text, opts = EMPTY_OPTIONS)
471
392
  escaped_title = escape_event_content(title)
472
393
  escaped_text = escape_event_content(text)
473
- event_string_data = "_e{#{escaped_title.length},#{escaped_text.length}}:#{escaped_title}|#{escaped_text}".dup
394
+ event_string_data = "_e{#{escaped_title.bytesize},#{escaped_text.bytesize}}:#{escaped_title}|#{escaped_text}".dup
474
395
 
475
396
  # We construct the string to be sent by adding '|key:value' parts to it when needed
476
397
  # All pipes ('|') in the metadata are removed. Title and Text can keep theirs
477
398
  OPTS_KEYS.each do |key, shorthand_key|
478
399
  if key != :tags && opts[key]
479
- value = remove_pipes(opts[key])
400
+ # :date_happened is the only key where the value is an Integer
401
+ # To not break backwards compatibility, we still accept a String
402
+ if key == :date_happened && opts[key].is_a?(Integer)
403
+ value = opts[key]
404
+ # All other keys only have String values
405
+ else
406
+ value = remove_pipes(opts[key])
407
+ end
480
408
  event_string_data << "|#{shorthand_key}:#{value}"
481
409
  end
482
410
  end
@@ -486,13 +414,18 @@ module Datadog
486
414
  event_string_data << "|##{tags_string}"
487
415
  end
488
416
 
489
- raise "Event #{title} payload is too big (more that 8KB), event discarded" if event_string_data.length > MAX_EVENT_SIZE
417
+ if event_string_data.bytesize > MAX_EVENT_SIZE
418
+ raise "Event #{title} payload is too big (more that 8KB), event discarded"
419
+ end
490
420
  event_string_data
491
421
  end
492
422
 
493
423
  def tags_as_string(opts)
494
424
  if tag_arr = opts[:tags]
495
- tag_arr = tag_arr.map { |tag| escape_tag_content(tag) }
425
+ tag_arr = tag_hash_to_array(tag_arr) if tag_arr.is_a?(Hash)
426
+ tag_arr = tag_arr.map do |tag|
427
+ escape_tag_content(tag)
428
+ end
496
429
  tag_arr = tags + tag_arr # @tags are normalized when set, so not need to normalize them again
497
430
  else
498
431
  tag_arr = tags
@@ -500,63 +433,65 @@ module Datadog
500
433
  tag_arr.join(COMMA) unless tag_arr.empty?
501
434
  end
502
435
 
503
- def escape_event_content(msg)
504
- msg.gsub NEW_LINE, ESC_NEW_LINE
436
+ def tag_hash_to_array(tag_hash)
437
+ tag_hash.to_a.map do |pair|
438
+ pair.compact.join(':')
439
+ end
440
+ end
441
+
442
+ def escape_event_content(message)
443
+ message.gsub(NEW_LINE, ESC_NEW_LINE)
505
444
  end
506
445
 
507
446
  def escape_tag_content(tag)
508
447
  tag = remove_pipes(tag.to_s)
509
- tag.delete! COMMA
448
+ tag.delete!(COMMA)
510
449
  tag
511
450
  end
512
451
 
513
- def remove_pipes(msg)
514
- msg.delete PIPE
452
+ def remove_pipes(message)
453
+ message.delete(PIPE)
515
454
  end
516
455
 
517
- def escape_service_check_message(msg)
518
- escape_event_content(msg).gsub('m:'.freeze, 'm\:'.freeze)
456
+ def escape_service_check_message(message)
457
+ escape_event_content(message).gsub('m:', 'm\:')
519
458
  end
520
459
 
521
- def send_stats(stat, delta, type, opts=EMPTY_OPTIONS)
522
- sample_rate = opts[:sample_rate] || 1
523
- if sample_rate == 1 or rand < sample_rate
460
+ def send_stats(stat, delta, type, opts = EMPTY_OPTIONS)
461
+ @telemetry.metrics += 1
462
+ sample_rate = opts[:sample_rate] || @sample_rate || 1
463
+ if sample_rate == 1 || rand <= sample_rate
524
464
  full_stat = ''.dup
525
465
  full_stat << @prefix if @prefix
526
466
 
527
467
  stat = stat.is_a?(String) ? stat.dup : stat.to_s
528
468
  # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
529
469
  stat.gsub!(DOUBLE_COLON, DOT)
530
- stat.tr!(':|@'.freeze, UNDERSCORE)
470
+ stat.tr!(':|@', UNDERSCORE)
531
471
  full_stat << stat
532
472
 
533
- full_stat << ':'.freeze
473
+ full_stat << ':'
534
474
  full_stat << delta.to_s
535
475
  full_stat << PIPE
536
476
  full_stat << type
537
477
 
538
478
  unless sample_rate == 1
539
479
  full_stat << PIPE
540
- full_stat << '@'.freeze
480
+ full_stat << '@'
541
481
  full_stat << sample_rate.to_s
542
482
  end
543
483
 
544
484
  if tags_string = tags_as_string(opts)
545
485
  full_stat << PIPE
546
- full_stat << '#'.freeze
486
+ full_stat << '#'
547
487
  full_stat << tags_string
548
488
  end
549
-
550
489
  send_stat(full_stat)
551
490
  end
552
491
  end
553
492
 
554
493
  def send_stat(message)
555
- if @batch.open?
556
- @batch.add message
557
- else
558
- @connection.write(message)
559
- end
494
+ @batch.open? ? @batch.add(message) : @connection.write(message)
560
495
  end
561
496
  end
562
497
  end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ class Batch
6
+ def initialize(connection, max_buffer_bytes)
7
+ @connection = connection
8
+ @max_buffer_bytes = max_buffer_bytes
9
+ @depth = 0
10
+ reset
11
+ end
12
+
13
+ def open
14
+ @depth += 1
15
+
16
+ yield
17
+ ensure
18
+ @depth -= 1
19
+ flush if !open?
20
+ end
21
+
22
+ def open?
23
+ @depth > 0
24
+ end
25
+
26
+ def add(message)
27
+ message_bytes = message.bytesize
28
+
29
+ unless @buffer_bytes == 0
30
+ if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
31
+ flush
32
+ else
33
+ @buffer << NEW_LINE
34
+ @buffer_bytes += 1
35
+ end
36
+ end
37
+
38
+ @buffer << message
39
+ @buffer_bytes += message_bytes
40
+ end
41
+
42
+ def flush
43
+ return if @buffer_bytes == 0
44
+ @connection.write(@buffer)
45
+ reset
46
+ end
47
+
48
+ private
49
+
50
+ def reset
51
+ @buffer = String.new
52
+ @buffer_bytes = 0
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ class Connection
6
+ def initialize(telemetry)
7
+ @telemetry = telemetry
8
+ end
9
+
10
+ # Close the underlying socket
11
+ def close
12
+ @socket && @socket.close
13
+ end
14
+
15
+ def write(payload)
16
+ logger.debug { "Statsd: #{payload}" } if logger
17
+ flush_telemetry = @telemetry.flush?
18
+ if flush_telemetry
19
+ payload += @telemetry.flush()
20
+ end
21
+
22
+ send_message(payload)
23
+
24
+ if flush_telemetry
25
+ @telemetry.reset
26
+ end
27
+
28
+ telemetry.bytes_sent += payload.length
29
+ telemetry.packets_sent += 1
30
+ rescue StandardError => boom
31
+ # Try once to reconnect if the socket has been closed
32
+ retries ||= 1
33
+ if retries <= 1 &&
34
+ (boom.is_a?(Errno::ENOTCONN) or
35
+ boom.is_a?(Errno::ECONNREFUSED) or
36
+ boom.is_a?(IOError) && boom.message =~ /closed stream/i)
37
+ retries += 1
38
+ begin
39
+ @socket = connect
40
+ retry
41
+ rescue StandardError => e
42
+ boom = e
43
+ end
44
+ end
45
+
46
+ telemetry.bytes_dropped += payload.length
47
+ telemetry.packets_dropped += 1
48
+ logger.error { "Statsd: #{boom.class} #{boom}" } if logger
49
+ nil
50
+ end
51
+
52
+ private
53
+
54
+ attr_reader :telemetry
55
+ attr_reader :logger
56
+
57
+ def socket
58
+ @socket ||= connect
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+ require 'time'
3
+
4
+ module Datadog
5
+ class Statsd
6
+ class Telemetry
7
+ attr_accessor :metrics
8
+ attr_accessor :events
9
+ attr_accessor :service_checks
10
+ attr_accessor :bytes_sent
11
+ attr_accessor :bytes_dropped
12
+ attr_accessor :packets_sent
13
+ attr_accessor :packets_dropped
14
+ attr_reader :estimate_max_size
15
+
16
+ def initialize(disabled, tags, flush_interval)
17
+ @disabled = disabled
18
+ @tags = tags
19
+ @flush_interval = flush_interval
20
+ reset
21
+
22
+ # estimate_max_size is an estimation or the maximum size of the
23
+ # telemetry payload. Since we don't want our packet to go over
24
+ # 'max_buffer_bytes', we have to adjust with the size of the telemetry
25
+ # (and any tags used). The telemetry payload size will change depending
26
+ # on the actual value of metrics: metrics received, packet dropped,
27
+ # etc. This is why we add a 63bytes margin: 9 bytes for each of the 7
28
+ # telemetry metrics.
29
+ @estimate_max_size = @disabled ? 0 : flush().length + 9 * 7
30
+ end
31
+
32
+ def reset
33
+ @metrics = 0
34
+ @events = 0
35
+ @service_checks = 0
36
+ @bytes_sent = 0
37
+ @bytes_dropped = 0
38
+ @packets_sent = 0
39
+ @packets_dropped = 0
40
+ @next_flush_time = Time.now.to_i + @flush_interval
41
+ end
42
+
43
+ def flush?
44
+ if @next_flush_time < Time.now.to_i
45
+ return true
46
+ end
47
+ return false
48
+ end
49
+
50
+ def flush
51
+ return '' if @disabled
52
+
53
+ # using shorthand syntax to reduce the garbage collection
54
+ return %Q(
55
+ datadog.dogstatsd.client.metrics:#{@metrics}|#{COUNTER_TYPE}|##{@tags}
56
+ datadog.dogstatsd.client.events:#{@events}|#{COUNTER_TYPE}|##{@tags}
57
+ datadog.dogstatsd.client.service_checks:#{@service_checks}|#{COUNTER_TYPE}|##{@tags}
58
+ datadog.dogstatsd.client.bytes_sent:#{@bytes_sent}|#{COUNTER_TYPE}|##{@tags}
59
+ datadog.dogstatsd.client.bytes_dropped:#{@bytes_dropped}|#{COUNTER_TYPE}|##{@tags}
60
+ datadog.dogstatsd.client.packets_sent:#{@packets_sent}|#{COUNTER_TYPE}|##{@tags}
61
+ datadog.dogstatsd.client.packets_dropped:#{@packets_dropped}|#{COUNTER_TYPE}|##{@tags})
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'connection'
4
+
5
+ module Datadog
6
+ class Statsd
7
+ class UDPConnection < Connection
8
+ DEFAULT_HOST = '127.0.0.1'
9
+ DEFAULT_PORT = 8125
10
+
11
+ # StatsD host. Defaults to 127.0.0.1.
12
+ attr_reader :host
13
+
14
+ # StatsD port. Defaults to 8125.
15
+ attr_reader :port
16
+
17
+ def initialize(host, port, logger, telemetry)
18
+ super(telemetry)
19
+ @host = host || ENV.fetch('DD_AGENT_HOST', DEFAULT_HOST)
20
+ @port = port || ENV.fetch('DD_DOGSTATSD_PORT', DEFAULT_PORT)
21
+ @logger = logger
22
+ end
23
+
24
+ private
25
+
26
+ def connect
27
+ UDPSocket.new.tap do |socket|
28
+ socket.connect(host, port)
29
+ end
30
+ end
31
+
32
+ def send_message(message)
33
+ socket.send(message, 0)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'connection'
4
+
5
+ module Datadog
6
+ class Statsd
7
+ class UDSConnection < Connection
8
+ class BadSocketError < StandardError; end
9
+
10
+ # DogStatsd unix socket path
11
+ attr_reader :socket_path
12
+
13
+ def initialize(socket_path, logger, telemetry)
14
+ super(telemetry)
15
+ @socket_path = socket_path
16
+ @logger = logger
17
+ end
18
+
19
+ private
20
+
21
+ def connect
22
+ socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
23
+ socket.connect(Socket.pack_sockaddr_un(@socket_path))
24
+ socket
25
+ end
26
+
27
+ def send_message(message)
28
+ socket.sendmsg_nonblock(message)
29
+ rescue Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::ENOENT => e
30
+ @socket = nil
31
+ raise BadSocketError, "#{e.class}: #{e}"
32
+ end
33
+ end
34
+ end
35
+ 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: 4.2.0
4
+ version: 4.7.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: 2019-04-04 00:00:00.000000000 Z
11
+ date: 2020-02-14 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A Ruby DogStastd client
14
14
  email: code@datadoghq.com
@@ -21,10 +21,19 @@ files:
21
21
  - LICENSE.txt
22
22
  - README.md
23
23
  - lib/datadog/statsd.rb
24
- homepage: http://github.com/datadog/dogstatsd-ruby
24
+ - lib/datadog/statsd/batch.rb
25
+ - lib/datadog/statsd/connection.rb
26
+ - lib/datadog/statsd/telemetry.rb
27
+ - lib/datadog/statsd/udp_connection.rb
28
+ - lib/datadog/statsd/uds_connection.rb
29
+ homepage: https://github.com/DataDog/dogstatsd-ruby
25
30
  licenses:
26
31
  - MIT
27
- metadata: {}
32
+ metadata:
33
+ bug_tracker_uri: https://github.com/DataDog/dogstatsd-ruby/issues
34
+ changelog_uri: https://github.com/DataDog/dogstatsd-ruby/blob/v4.7.0/CHANGELOG.md
35
+ documentation_uri: https://www.rubydoc.info/gems/dogstatsd-ruby/4.7.0
36
+ source_code_uri: https://github.com/DataDog/dogstatsd-ruby/tree/v4.7.0
28
37
  post_install_message:
29
38
  rdoc_options: []
30
39
  require_paths:
@@ -40,7 +49,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
40
49
  - !ruby/object:Gem::Version
41
50
  version: '0'
42
51
  requirements: []
43
- rubygems_version: 3.0.2
52
+ rubyforge_project:
53
+ rubygems_version: 2.7.10
44
54
  signing_key:
45
55
  specification_version: 4
46
56
  summary: A Ruby DogStatsd client