dogstatsd-ruby 4.0.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: 6d2b1b9e7ec2a48c5305f6acda103d0b7a09787801dd899a38fb665f5389e4f3
4
- data.tar.gz: 3d974fffb4ace3bc248e69dcdaabb4c556e6783a25d65af98cb65bdccafc52ee
3
+ metadata.gz: 57d7660dad73ad8c0c5f37f6d6130474d255beaa467aca03f532e552d0b608e0
4
+ data.tar.gz: 30c8686d3045e4cfb2ca18c57390f78dbcccba1ff941deb170d89497953d5e84
5
5
  SHA512:
6
- metadata.gz: 16c82cb62bfd324d5e1dc1c6293fa121b92bd0c88d00474210c5da67f2cb07ae350cc0b717b15739c7a6f09e9021323ba348cec70ec2d0ac66a362f15f81007b
7
- data.tar.gz: 6e82cef56d746ecf7b10890d397e313c143611b18b4ae65db67eb2f252cb27acacb45de03f6447b7b4ebb1666209806d6b048fb2857c5c8790f1f6729f4ae806
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,168 +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 || DEFAULT_HOST
38
- @port = port || 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?(IOError) && boom.message =~ /closed stream/i
65
- retries += 1
66
- begin
67
- @socket = connect
68
- retry
69
- rescue StandardError => e
70
- boom = e
71
- end
72
- end
73
-
74
- @logger.error { "Statsd: #{boom.class} #{boom}" } if @logger
75
- nil
76
- end
77
-
78
- # Close the underlying socket
79
- def close
80
- @socket && @socket.close
81
- end
82
-
83
- private
84
-
85
- def socket
86
- @socket ||= connect
87
- end
88
-
89
- def connect
90
- if @socket_path.nil?
91
- socket = UDPSocket.new
92
- socket.connect(@host, @port)
93
- else
94
- socket = Socket.new(Socket::AF_UNIX, Socket::SOCK_DGRAM)
95
- socket.connect(Socket.pack_sockaddr_un(@socket_path))
96
- end
97
- socket
98
- end
99
- end
100
-
101
- class Batch
102
- def initialize(connection, max_buffer_bytes)
103
- @connection = connection
104
- @max_buffer_bytes = max_buffer_bytes
105
- @depth = 0
106
- reset
107
- end
108
-
109
- def open
110
- @depth += 1
111
- yield
112
- ensure
113
- @depth -= 1
114
- flush if !open?
115
- end
116
-
117
- def open?
118
- @depth > 0
119
- end
120
-
121
- def add(message)
122
- message_bytes = message.bytesize
123
-
124
- unless @buffer_bytes == 0
125
- if @buffer_bytes + 1 + message_bytes >= @max_buffer_bytes
126
- flush
127
- else
128
- @buffer << NEW_LINE
129
- @buffer_bytes += 1
130
- end
131
- end
132
-
133
- @buffer << message
134
- @buffer_bytes += message_bytes
135
- end
136
-
137
- def flush
138
- return if @buffer_bytes == 0
139
- @connection.write @buffer
140
- reset
141
- end
142
-
143
- private
144
-
145
- def reset
146
- @buffer = String.new
147
- @buffer_bytes = 0
148
- end
149
- end
150
-
151
27
  # Create a dictionary to assign a key to every parameter's name, except for tags (treated differently)
152
28
  # Goal: Simple and fast to add some other parameters
153
29
  OPTS_KEYS = {
154
- :date_happened => :d,
155
- :hostname => :h,
156
- :aggregation_key => :k,
157
- :priority => :p,
158
- :source_type_name => :s,
159
- :alert_type => :t,
160
- }
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
161
37
 
162
38
  # Service check options
163
39
  SC_OPT_KEYS = {
164
- :timestamp => 'd:'.freeze,
165
- :hostname => 'h:'.freeze,
166
- :tags => '#'.freeze,
167
- :message => 'm:'.freeze,
168
- }
169
-
170
- OK = 0
171
- WARNING = 1
172
- CRITICAL = 2
173
- UNKNOWN = 3
174
-
175
- MAX_EVENT_SIZE = 8 * 1024
176
-
177
- COUNTER_TYPE = 'c'.freeze
178
- GAUGE_TYPE = 'g'.freeze
179
- HISTOGRAM_TYPE = 'h'.freeze
180
- DISTRIBUTION_TYPE = 'd'.freeze
181
- TIMING_TYPE = 'ms'.freeze
182
- SET_TYPE = 's'.freeze
183
- VERSION = "4.0.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'
184
63
 
185
64
  # A namespace to prepend to all statsd calls. Defaults to no namespace.
186
65
  attr_reader :namespace
@@ -194,41 +73,73 @@ module Datadog
194
73
  # Maximum buffer size in bytes before it is flushed
195
74
  attr_reader :max_buffer_bytes
196
75
 
76
+ # Default sample rate
77
+ attr_reader :sample_rate
78
+
197
79
  # Connection
198
80
  attr_reader :connection
199
81
 
200
82
  # @param [String] host your statsd host
201
83
  # @param [Integer] port your statsd port
202
84
  # @option [String] namespace set a namespace to be prepended to every metric name
203
- # @option [Array<String>] tags tags to be added to every metric
204
- # @option [Loger] logger for debugging
85
+ # @option [Array<String>|Hash] tags tags to be added to every metric
86
+ # @option [Logger] logger for debugging
205
87
  # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
206
88
  # @option [String] socket_path unix socket path
89
+ # @option [Float] default sample rate if not overridden
207
90
  def initialize(
208
91
  host = nil,
209
92
  port = nil,
210
93
  namespace: nil,
211
94
  tags: nil,
212
- max_buffer_bytes: 8192,
95
+ max_buffer_bytes: DEFAULT_BUFFER_SIZE,
213
96
  socket_path: nil,
214
- logger: nil
97
+ logger: nil,
98
+ sample_rate: nil,
99
+ disable_telemetry: false,
100
+ telemetry_flush_interval: DEFAULT_TELEMETRY_FLUSH_INTERVAL
215
101
  )
216
- @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
217
127
  @logger = logger
218
128
 
219
129
  @namespace = namespace
220
130
  @prefix = @namespace ? "#{@namespace}.".freeze : nil
221
131
 
222
- raise ArgumentError, 'tags must be a Array<String>' unless tags.nil? or tags.is_a? Array
223
- @tags = (tags || []).compact.map! {|tag| escape_tag_content(tag)}
132
+ @sample_rate = sample_rate
224
133
 
225
- @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))
226
136
  end
227
137
 
228
138
  # yield a new instance to a block and close it when done
229
139
  # for short-term use-cases that don't want to close the socket manually
230
140
  def self.open(*args)
231
141
  instance = new(*args)
142
+
232
143
  yield instance
233
144
  ensure
234
145
  instance.close
@@ -242,10 +153,10 @@ module Datadog
242
153
  # @option opts [Array<String>] :tags An array of tags
243
154
  # @option opts [Numeric] :by increment value, default 1
244
155
  # @see #count
245
- def increment(stat, opts=EMPTY_OPTIONS)
246
- 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)
247
158
  incr_value = opts.fetch(:by, 1)
248
- count stat, incr_value, opts
159
+ count(stat, incr_value, opts)
249
160
  end
250
161
 
251
162
  # Sends a decrement (count = -1) for the given stat to the statsd server.
@@ -256,10 +167,10 @@ module Datadog
256
167
  # @option opts [Array<String>] :tags An array of tags
257
168
  # @option opts [Numeric] :by decrement value, default 1
258
169
  # @see #count
259
- def decrement(stat, opts=EMPTY_OPTIONS)
260
- 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)
261
172
  decr_value = - opts.fetch(:by, 1)
262
- count stat, decr_value, opts
173
+ count(stat, decr_value, opts)
263
174
  end
264
175
 
265
176
  # Sends an arbitrary count for the given stat to the statsd server.
@@ -269,9 +180,9 @@ module Datadog
269
180
  # @param [Hash] opts the options to create the metric with
270
181
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
271
182
  # @option opts [Array<String>] :tags An array of tags
272
- def count(stat, count, opts=EMPTY_OPTIONS)
273
- opts = {:sample_rate => opts} if opts.is_a? Numeric
274
- 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)
275
186
  end
276
187
 
277
188
  # Sends an arbitary gauge value for the given stat to the statsd server.
@@ -287,9 +198,9 @@ module Datadog
287
198
  # @option opts [Array<String>] :tags An array of tags
288
199
  # @example Report the current user count:
289
200
  # $statsd.gauge('user.count', User.count)
290
- def gauge(stat, value, opts=EMPTY_OPTIONS)
291
- opts = {:sample_rate => opts} if opts.is_a? Numeric
292
- 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)
293
204
  end
294
205
 
295
206
  # Sends a value to be tracked as a histogram to the statsd server.
@@ -301,14 +212,11 @@ module Datadog
301
212
  # @option opts [Array<String>] :tags An array of tags
302
213
  # @example Report the current user count:
303
214
  # $statsd.histogram('user.count', User.count)
304
- def histogram(stat, value, opts=EMPTY_OPTIONS)
305
- send_stats stat, value, HISTOGRAM_TYPE, opts
215
+ def histogram(stat, value, opts = EMPTY_OPTIONS)
216
+ send_stats(stat, value, HISTOGRAM_TYPE, opts)
306
217
  end
307
218
 
308
219
  # Sends a value to be tracked as a distribution to the statsd server.
309
- # Note: Distributions are a beta feature of Datadog and not generally
310
- # available. Distributions must be specifically enabled for your
311
- # organization.
312
220
  #
313
221
  # @param [String] stat stat name.
314
222
  # @param [Numeric] value distribution value.
@@ -317,8 +225,8 @@ module Datadog
317
225
  # @option opts [Array<String>] :tags An array of tags
318
226
  # @example Report the current user count:
319
227
  # $statsd.distribution('user.count', User.count)
320
- def distribution(stat, value, opts=EMPTY_OPTIONS)
321
- send_stats stat, value, DISTRIBUTION_TYPE, opts
228
+ def distribution(stat, value, opts = EMPTY_OPTIONS)
229
+ send_stats(stat, value, DISTRIBUTION_TYPE, opts)
322
230
  end
323
231
 
324
232
  # Sends a timing (in ms) for the given stat to the statsd server. The
@@ -331,9 +239,9 @@ module Datadog
331
239
  # @param [Hash] opts the options to create the metric with
332
240
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
333
241
  # @option opts [Array<String>] :tags An array of tags
334
- def timing(stat, ms, opts=EMPTY_OPTIONS)
335
- opts = {:sample_rate => opts} if opts.is_a? Numeric
336
- 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)
337
245
  end
338
246
 
339
247
  # Reports execution time of the provided block using {#timing}.
@@ -349,12 +257,21 @@ module Datadog
349
257
  # @see #timing
350
258
  # @example Report the time (in ms) taken to activate an account
351
259
  # $statsd.time('account.activate') { @account.activate! }
352
- def time(stat, opts=EMPTY_OPTIONS)
353
- opts = {:sample_rate => opts} if opts.is_a? Numeric
354
- start = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
355
- 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
356
268
  ensure
357
- 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
+
358
275
  timing(stat, ((finished - start) * 1000).round, opts)
359
276
  end
360
277
 
@@ -367,9 +284,9 @@ module Datadog
367
284
  # @option opts [Array<String>] :tags An array of tags
368
285
  # @example Record a unique visitory by id:
369
286
  # $statsd.set('visitors.uniques', User.id)
370
- def set(stat, value, opts=EMPTY_OPTIONS)
371
- opts = {:sample_rate => opts} if opts.is_a? Numeric
372
- 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)
373
290
  end
374
291
 
375
292
  # This method allows you to send custom service check statuses.
@@ -377,14 +294,15 @@ module Datadog
377
294
  # @param [String] name Service check name
378
295
  # @param [String] status Service check status.
379
296
  # @param [Hash] opts the additional data about the service check
380
- # @option opts [Integer, nil] :timestamp (nil) Assign a timestamp to the event. Default is now when none
381
- # @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.
382
299
  # @option opts [Array<String>, nil] :tags (nil) An array of tags
383
300
  # @option opts [String, nil] :message (nil) A message to associate with this service check status
384
301
  # @example Report a critical service check status
385
302
  # $statsd.service_check('my.service.check', Statsd::CRITICAL, :tags=>['urgent'])
386
- def service_check(name, status, opts=EMPTY_OPTIONS)
387
- 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))
388
306
  end
389
307
 
390
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.
@@ -394,9 +312,9 @@ module Datadog
394
312
  # it will be grouped with other events that don't have an event type.
395
313
  #
396
314
  # @param [String] title Event title
397
- # @param [String] text Event text. Supports \n
315
+ # @param [String] text Event text. Supports newlines (+\n+)
398
316
  # @param [Hash] opts the additional data about the event
399
- # @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
400
318
  # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
401
319
  # @option opts [String, nil] :aggregation_key (nil) Assign an aggregation key to the event, to group it with some others
402
320
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
@@ -405,8 +323,9 @@ module Datadog
405
323
  # @option opts [Array<String>] :tags tags to be added to every metric
406
324
  # @example Report an awful event:
407
325
  # $statsd.event('Something terrible happened', 'The end is near if we do nothing', :alert_type=>'warning', :tags=>['end_of_times','urgent'])
408
- def event(title, text, opts=EMPTY_OPTIONS)
409
- 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))
410
329
  end
411
330
 
412
331
  # Send several metrics in the same UDP Packet
@@ -418,7 +337,9 @@ module Datadog
418
337
  # s.increment('page.views')
419
338
  # end
420
339
  def batch
421
- @batch.open { yield self }
340
+ @batch.open do
341
+ yield self
342
+ end
422
343
  end
423
344
 
424
345
  # Close the underlying socket
@@ -428,20 +349,20 @@ module Datadog
428
349
 
429
350
  private
430
351
 
431
- NEW_LINE = "\n".freeze
432
- ESC_NEW_LINE = "\\n".freeze
433
- COMMA = ",".freeze
434
- PIPE = "|".freeze
435
- DOT = ".".freeze
436
- DOUBLE_COLON = "::".freeze
437
- UNDERSCORE = "_".freeze
438
- 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')
439
360
  EMPTY_OPTIONS = {}.freeze
440
361
 
441
362
  private_constant :NEW_LINE, :ESC_NEW_LINE, :COMMA, :PIPE, :DOT,
442
363
  :DOUBLE_COLON, :UNDERSCORE, :EMPTY_OPTIONS
443
364
 
444
- def format_service_check(name, status, opts=EMPTY_OPTIONS)
365
+ def format_service_check(name, status, opts = EMPTY_OPTIONS)
445
366
  sc_string = "_sc|#{name}|#{status}".dup
446
367
 
447
368
  SC_OPT_KEYS.each do |key, shorthand_key|
@@ -456,23 +377,34 @@ module Datadog
456
377
  escaped_message = escape_service_check_message(message)
457
378
  sc_string << "|m:#{escaped_message}"
458
379
  else
459
- 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
460
385
  sc_string << "|#{shorthand_key}#{value}"
461
386
  end
462
387
  end
463
388
  sc_string
464
389
  end
465
390
 
466
- def format_event(title, text, opts=EMPTY_OPTIONS)
391
+ def format_event(title, text, opts = EMPTY_OPTIONS)
467
392
  escaped_title = escape_event_content(title)
468
393
  escaped_text = escape_event_content(text)
469
- 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
470
395
 
471
396
  # We construct the string to be sent by adding '|key:value' parts to it when needed
472
397
  # All pipes ('|') in the metadata are removed. Title and Text can keep theirs
473
398
  OPTS_KEYS.each do |key, shorthand_key|
474
399
  if key != :tags && opts[key]
475
- 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
476
408
  event_string_data << "|#{shorthand_key}:#{value}"
477
409
  end
478
410
  end
@@ -482,13 +414,18 @@ module Datadog
482
414
  event_string_data << "|##{tags_string}"
483
415
  end
484
416
 
485
- 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
486
420
  event_string_data
487
421
  end
488
422
 
489
423
  def tags_as_string(opts)
490
424
  if tag_arr = opts[:tags]
491
- 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
492
429
  tag_arr = tags + tag_arr # @tags are normalized when set, so not need to normalize them again
493
430
  else
494
431
  tag_arr = tags
@@ -496,63 +433,65 @@ module Datadog
496
433
  tag_arr.join(COMMA) unless tag_arr.empty?
497
434
  end
498
435
 
499
- def escape_event_content(msg)
500
- 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)
501
444
  end
502
445
 
503
446
  def escape_tag_content(tag)
504
447
  tag = remove_pipes(tag.to_s)
505
- tag.delete! COMMA
448
+ tag.delete!(COMMA)
506
449
  tag
507
450
  end
508
451
 
509
- def remove_pipes(msg)
510
- msg.delete PIPE
452
+ def remove_pipes(message)
453
+ message.delete(PIPE)
511
454
  end
512
455
 
513
- def escape_service_check_message(msg)
514
- escape_event_content(msg).gsub('m:'.freeze, 'm\:'.freeze)
456
+ def escape_service_check_message(message)
457
+ escape_event_content(message).gsub('m:', 'm\:')
515
458
  end
516
459
 
517
- def send_stats(stat, delta, type, opts=EMPTY_OPTIONS)
518
- sample_rate = opts[:sample_rate] || 1
519
- 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
520
464
  full_stat = ''.dup
521
465
  full_stat << @prefix if @prefix
522
466
 
523
467
  stat = stat.is_a?(String) ? stat.dup : stat.to_s
524
468
  # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
525
469
  stat.gsub!(DOUBLE_COLON, DOT)
526
- stat.tr!(':|@'.freeze, UNDERSCORE)
470
+ stat.tr!(':|@', UNDERSCORE)
527
471
  full_stat << stat
528
472
 
529
- full_stat << ':'.freeze
473
+ full_stat << ':'
530
474
  full_stat << delta.to_s
531
475
  full_stat << PIPE
532
476
  full_stat << type
533
477
 
534
478
  unless sample_rate == 1
535
479
  full_stat << PIPE
536
- full_stat << '@'.freeze
480
+ full_stat << '@'
537
481
  full_stat << sample_rate.to_s
538
482
  end
539
483
 
540
484
  if tags_string = tags_as_string(opts)
541
485
  full_stat << PIPE
542
- full_stat << '#'.freeze
486
+ full_stat << '#'
543
487
  full_stat << tags_string
544
488
  end
545
-
546
489
  send_stat(full_stat)
547
490
  end
548
491
  end
549
492
 
550
493
  def send_stat(message)
551
- if @batch.open?
552
- @batch.add message
553
- else
554
- @connection.write(message)
555
- end
494
+ @batch.open? ? @batch.add(message) : @connection.write(message)
556
495
  end
557
496
  end
558
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.0.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: 2018-08-20 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:
@@ -41,7 +50,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
41
50
  version: '0'
42
51
  requirements: []
43
52
  rubyforge_project:
44
- rubygems_version: 2.7.7
53
+ rubygems_version: 2.7.10
45
54
  signing_key:
46
55
  specification_version: 4
47
56
  summary: A Ruby DogStatsd client