dogstatsd-ruby 4.2.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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