dogstatsd-ruby 4.0.0 → 5.2.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: 9abfaabd9be58a320dd8cc8d1d4998ac287daa08e68ab894054cf2910f5f3133
4
+ data.tar.gz: 88ebf4ef472f7663897c06fa3e5753a17c12722bc1a58f8aa4e9fb0a3ace07c6
5
5
  SHA512:
6
- metadata.gz: 16c82cb62bfd324d5e1dc1c6293fa121b92bd0c88d00474210c5da67f2cb07ae350cc0b717b15739c7a6f09e9021323ba348cec70ec2d0ac66a362f15f81007b
7
- data.tar.gz: 6e82cef56d746ecf7b10890d397e313c143611b18b4ae65db67eb2f252cb27acacb45de03f6447b7b4ebb1666209806d6b048fb2857c5c8790f1f6729f4ae806
6
+ metadata.gz: 199527bd5e9d39d94be3cabf28dc5f4d8913c4d3f746a6018a22c9ed13440c34d77fc11b363f47f9e2fd325974005fdc3567a223de4669b388ced99c3cd0750f
7
+ data.tar.gz: 0da25bafef540e4d36be8cae9c1fbb1f11c6aea94415828941aa9c34877db75166574f41b5e1a0829c71285f958162133d0f592a34619ce066e14c97ea86fd4d
data/README.md CHANGED
@@ -1,97 +1,217 @@
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)
27
+ ...
28
+ # release resources used by the client instance
29
+ statsd.close()
30
+ ```
31
+ Or if you want to connect over Unix Domain Socket:
32
+ ```ruby
33
+ # Connection over Unix Domain Socket
34
+ statsd = Datadog::Statsd.new(socket_path: '/path/to/socket/file')
35
+ ...
36
+ # release resources used by the client instance
37
+ statsd.close()
38
+ ```
39
+
40
+ 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).
41
+
42
+ ### Migrating from v4.x to v5.x
43
+
44
+ If you are already using DogStatsD-ruby v4.x and you want to migrate to a version v5.x, the major
45
+ change concerning you is the new threading model (please see section Threading model):
46
+
47
+ In practice, it means two things:
48
+
49
+ 1. Now that the client is buffering metrics before sending them, you have to manually
50
+ call the method `Datadog::Statsd#flush` if you want to force the sending of metrics. Note that the companion thread will automatically flush the buffered metrics if the buffer gets full or when you are closing the instance.
51
+
52
+ 2. You have to make sure you are either:
53
+
54
+ * using singletons instances of the DogStatsD client and not allocating one each time you need one, letting the buffering mechanism flush metrics, it's still a bad solution if the process later forks (see related section below). Or,
55
+ * properly closing your DogStatsD client instance when it is not needed anymore using the method `Datadog::Statsd#close` to release the resources used by the instance and to close the socket
56
+
57
+ If you have issues with the companion thread or the buffering mode, you can instantiate a client that behaves exactly as in v4.x (i.e. no companion thread and flush on every metric submission):
58
+
59
+ ```ruby
60
+ # Import the library
61
+ require 'datadog/statsd'
62
+
63
+ # Create a DogStatsD client instance using UDP
64
+ statsd = Datadog::Statsd.new('localhost', 8125, single_thread: true, buffer_max_payload_size: 1)
65
+ ...
66
+ # to close the instance is not necessary in this case since metrics are flushed on submission
67
+ # but it is still a good practice and it explicitely closes the socket
68
+ statsd.close()
69
+ ```
70
+
71
+ or
72
+
73
+ ```ruby
74
+ # Import the library
75
+ require 'datadog/statsd'
76
+
77
+ # Create a DogStatsD client instance using UDS
78
+ statsd = Datadog::Statsd.new(socket_path: '/path/to/socket/file', single_thread: true, buffer_max_payload_size: 1)
79
+ ...
80
+ # to close the instance is not necessary in this case since metrics are flushed on submission
81
+ # but it is still a good practice and it explicitely closes the socket
82
+ statsd.close()
83
+ ```
84
+
85
+ ### v5.x Common Pitfalls
24
86
 
25
- # you could also create a statsd class if you need a drop in replacement
26
- # class Statsd < Datadog::Statsd
27
- # end
87
+ Version v5.x of `dogstatsd-ruby` is using a companion thread for preemptive flushing, it brings better performances for application having a high-throughput of statsd metrics, but it comes with new pitfalls:
28
88
 
29
- # Increment a counter.
30
- statsd.increment('page.views')
89
+ * Applications forking after having created the dogstatsd instance: forking a process can't duplicate the existing threads, meaning that one of the processes won't have a companion thread to flush the metrics and will lead to missing metrics.
90
+ * Applications creating a lot of different instances of the client without closing them: it is important to close the instance to free the thread and the socket it is using or it will lead to thread leaks.
31
91
 
32
- # Record a gauge 50% of the time.
33
- statsd.gauge('users.online', 123, :sample_rate=>0.5)
92
+ If you are using [Sidekiq](https://github.com/mperham/sidekiq), please make sure to close the client instances that are instantiated. [See this example on using DogStatsD-ruby v5.x with Sidekiq](https://github.com/DataDog/dogstatsd-ruby/blob/master/examples/sidekiq_example.rb).
34
93
 
35
- # Sample a histogram
36
- statsd.histogram('file.upload.size', 1234)
94
+ If you are using [Puma](https://github.com/puma/puma) or [Unicorn](https://yhbt.net/unicorn.git), please make sure to create the instance of DogStatsD in the workers, not in the main process before it forks to create its workers. See [this comment for more details](https://github.com/DataDog/dogstatsd-ruby/issues/179#issuecomment-845570345).
37
95
 
38
- # Time a block of code
39
- statsd.time('page.render') do
40
- render_page('home.html')
41
- end
96
+ Applications that are in these situations but can't apply these recommendations should enable the `single_thread` mode which does not use a companion thread. Here is how to instantiate a client in this mode:
42
97
 
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
98
+ ```ruby
99
+ # Import the library
100
+ require 'datadog/statsd'
101
+
102
+ # Create a DogStatsD client instance.
103
+ statsd = Datadog::Statsd.new('localhost', 8125, single_thread: true)
104
+ ...
105
+ # release resources used by the client instance and flush last metrics
106
+ statsd.close()
107
+ ```
49
108
 
50
- # Tag a metric.
51
- statsd.histogram('query.time', 10, :tags => ["version:1"])
109
+ ### Origin detection over UDP
52
110
 
53
- # Auto-close socket after end of block
54
- Datadog::Statsd.open('localhost', 8125) do |s|
55
- s.increment('page.views')
56
- end
111
+ 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.
112
+
113
+ To enable origin detection over UDP, add the following lines to your application manifest
114
+ ```yaml
115
+ env:
116
+ - name: DD_ENTITY_ID
117
+ valueFrom:
118
+ fieldRef:
119
+ fieldPath: metadata.uid
57
120
  ```
121
+ 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.
122
+
123
+ ## Usage
124
+
125
+ 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).
126
+
127
+ ### Metrics
128
+
129
+ 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:
130
+
131
+ * [Submit a COUNT metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#count).
132
+ * [Submit a GAUGE metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#gauge).
133
+ * [Submit a SET metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#set)
134
+ * [Submit a HISTOGRAM metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#histogram)
135
+ * [Submit a DISTRIBUTION metric](https://docs.datadoghq.com/developers/metrics/dogstatsd_metrics_submission/?tab=ruby#distribution)
136
+
137
+ 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).
138
+
139
+ ### Events
140
+
141
+ 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.
58
142
 
59
- You can also post events to your stream. You can tag them, set priority and even aggregate them with other events.
143
+ ### Service Checks
60
144
 
61
- Aggregation in the stream is made on hostname/event_type/source_type/aggregation_key.
145
+ 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.
62
146
 
63
- ``` ruby
64
- # Post a simple message
65
- statsd.event("There might be a storm tomorrow", "A friend warned me earlier.")
147
+ ### Maximum packets size in high-throughput scenarios
66
148
 
67
- # Cry for help
68
- statsd.event("SO MUCH SNOW", "Started yesterday and it won't stop !!", :alert_type => "error", :tags => ["urgent", "endoftheworld"])
149
+ In order to have the most efficient use of this library in high-throughput scenarios,
150
+ default values for the maximum packets size have already been set for both UDS (8192 bytes)
151
+ and UDP (1432 bytes) in order to have the best usage of the underlying network.
152
+ However, if you perfectly know your network and you know that a different value for the maximum packets
153
+ size should be used, you can set it with the parameter `buffer_max_payload_size`. Example:
154
+
155
+ ```ruby
156
+ # Create a DogStatsD client instance.
157
+ statsd = Datadog::Statsd.new('localhost', 8125, buffer_max_payload_size: 4096)
69
158
  ```
70
159
 
160
+ ## Threading model
161
+
162
+ On versions greater than 5.0, we changed the threading model of the library so that one instance of `Datadog::Statsd` could be shared between threads and so that the writes in the socket are non blocking.
163
+
164
+ When you instantiate a `Datadog::Statsd`, a companion thread is spawned. This thread will be called the Sender thread, as it is modeled by the [Sender](../lib/datadog/statsd/sender.rb) class. Please use `single_thread: true` while creating an instance if you don't want to or can't use a companion thread.
165
+
166
+ This thread is stopped when you close the statsd client (`Datadog::Statsd#close`). It also means that allocating a lot of statsd clients without closing them properly when not used anymore
167
+ could lead to a thread leak (even though they will be sleeping, blocked on IO).
168
+ The communication between the current thread is managed through a standard Ruby Queue.
169
+
170
+ The sender thread has the following logic (Code present in the method `Datadog::Statsd::Sender#send_loop`):
171
+
172
+ ```
173
+ while the sender message queue is not closed do
174
+ read message from sender message queue
175
+
176
+ if message is a Control message to flush
177
+ flush buffer in connection
178
+ else if message is a Control message to synchronize
179
+ synchronize with calling thread
180
+ else
181
+ add message to the buffer
182
+ end
183
+ end while
184
+ ```
185
+
186
+ Most of the time, the sender thread is blocked and sleeping when doing a blocking read from the sender message queue.
187
+
188
+ We can see that there is 3 different kind of messages:
189
+
190
+ * a control message to flush the buffer in the connection
191
+ * a control message to synchronize any thread with the sender thread
192
+ * a message to append to the buffer
71
193
 
194
+ There is also an implicit message which is closing the queue as it will stop blocking read from the message queue (if happening) and thus, stop the sender thread.
72
195
 
73
- Documentation
74
- -------------
196
+ ### Usual workflow
75
197
 
76
- Full API documentation is available
77
- [here](http://www.rubydoc.info/github/DataDog/dogstatsd-ruby/master/frames).
198
+ You push metrics to the statsd client which writes them quickly to the sender message queue. The sender thread receives those message, buffers them and flushes them to the connection when the buffer limit is reached.
78
199
 
200
+ ### Flushing
79
201
 
80
- Feedback
81
- --------
202
+ When calling a flush, a specific control message (the `:flush` symbol) is sent to the sender thread. When finding it, it flushes its internal buffer into the connection.
82
203
 
83
- To suggest a feature, report a bug, or general discussion, head over
84
- [here](http://github.com/DataDog/dogstatsd-ruby/issues/).
204
+ ### Rendez-vous
85
205
 
206
+ It is possible to ensure a message has been consumed by the sender thread and written to the buffer by simply calling a rendez-vous right after. This is done when you are doing a synchronized flush (calling `Datadog::Statsd#flush` with the `sync: true` option).
86
207
 
87
- [Change Log](CHANGELOG.md)
88
- ----------------------------
208
+ This means the current thread is going to sleep and wait for a Queue which is given to the sender thread. When the sender thread reads this queue from its own message queue, it puts a placeholder message in it so that it wakes up the calling thread.
89
209
 
210
+ This is useful when closing the application or when checking unit tests.
90
211
 
91
- Credits
92
- -------
212
+ ## Credits
93
213
 
94
- dogstatsd-ruby is forked from Rien Henrichs [original Statsd
214
+ dogstatsd-ruby is forked from Rein Henrichs [original Statsd
95
215
  client](https://github.com/reinh/statsd).
96
216
 
97
217
  Copyright (c) 2011 Rein Henrichs. See LICENSE.txt for
@@ -1,6 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
  require 'socket'
3
3
 
4
+ require_relative 'statsd/version'
5
+ require_relative 'statsd/telemetry'
6
+ require_relative 'statsd/udp_connection'
7
+ require_relative 'statsd/uds_connection'
8
+ require_relative 'statsd/message_buffer'
9
+ require_relative 'statsd/serialization'
10
+ require_relative 'statsd/sender'
11
+ require_relative 'statsd/single_thread_sender'
12
+ require_relative 'statsd/forwarder'
13
+
4
14
  # = Datadog::Statsd: A DogStatsd client (https://www.datadoghq.com)
5
15
  #
6
16
  # @example Set up a global Statsd client for a server on localhost:8125
@@ -19,216 +29,101 @@ require 'socket'
19
29
  # statsd = Datadog::Statsd.new 'localhost', 8125, tags: 'tag1:true'
20
30
  module Datadog
21
31
  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
32
+ class Error < StandardError
99
33
  end
100
34
 
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
- # Create a dictionary to assign a key to every parameter's name, except for tags (treated differently)
152
- # Goal: Simple and fast to add some other parameters
153
- 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
- }
161
-
162
- # Service check options
163
- 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
35
+ OK = 0
36
+ WARNING = 1
37
+ CRITICAL = 2
38
+ UNKNOWN = 3
39
+
40
+ UDP_DEFAULT_BUFFER_SIZE = 1_432
41
+ UDS_DEFAULT_BUFFER_SIZE = 8_192
42
+ DEFAULT_BUFFER_POOL_SIZE = Float::INFINITY
43
+ MAX_EVENT_SIZE = 8 * 1_024
44
+ # minimum flush interval for the telemetry in seconds
45
+ DEFAULT_TELEMETRY_FLUSH_INTERVAL = 10
46
+
47
+ COUNTER_TYPE = 'c'
48
+ GAUGE_TYPE = 'g'
49
+ HISTOGRAM_TYPE = 'h'
50
+ DISTRIBUTION_TYPE = 'd'
51
+ TIMING_TYPE = 'ms'
52
+ SET_TYPE = 's'
184
53
 
185
54
  # A namespace to prepend to all statsd calls. Defaults to no namespace.
186
55
  attr_reader :namespace
187
56
 
188
57
  # Global tags to be added to every statsd call. Defaults to no tags.
189
- attr_reader :tags
190
-
191
- # Buffer containing the statsd message before they are sent in batch
192
- attr_reader :buffer
193
-
194
- # Maximum buffer size in bytes before it is flushed
195
- attr_reader :max_buffer_bytes
58
+ def tags
59
+ serializer.global_tags
60
+ end
196
61
 
197
- # Connection
198
- attr_reader :connection
62
+ # Default sample rate
63
+ attr_reader :sample_rate
199
64
 
200
65
  # @param [String] host your statsd host
201
66
  # @param [Integer] port your statsd port
202
67
  # @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
205
- # @option [Integer] max_buffer_bytes max bytes to buffer when using #batch
68
+ # @option [Array<String>|Hash] tags tags to be added to every metric
69
+ # @option [Logger] logger for debugging
70
+ # @option [Integer] buffer_max_payload_size max bytes to buffer
71
+ # @option [Integer] buffer_max_pool_size max messages to buffer
206
72
  # @option [String] socket_path unix socket path
73
+ # @option [Float] default sample rate if not overridden
74
+ # @option [Boolean] single_thread flushes the metrics on the main thread instead of in a companion thread
207
75
  def initialize(
208
76
  host = nil,
209
77
  port = nil,
78
+ socket_path: nil,
79
+
210
80
  namespace: nil,
211
81
  tags: nil,
212
- max_buffer_bytes: 8192,
213
- socket_path: nil,
214
- logger: nil
82
+ sample_rate: nil,
83
+
84
+ buffer_max_payload_size: nil,
85
+ buffer_max_pool_size: nil,
86
+ buffer_overflowing_stategy: :drop,
87
+
88
+ logger: nil,
89
+
90
+ single_thread: false,
91
+
92
+ telemetry_enable: true,
93
+ telemetry_flush_interval: DEFAULT_TELEMETRY_FLUSH_INTERVAL
215
94
  )
216
- @connection = Connection.new(host, port, socket_path, logger)
217
- @logger = logger
95
+ unless tags.nil? || tags.is_a?(Array) || tags.is_a?(Hash)
96
+ raise ArgumentError, 'tags must be an array of string tags or a Hash'
97
+ end
218
98
 
219
99
  @namespace = namespace
220
100
  @prefix = @namespace ? "#{@namespace}.".freeze : nil
101
+ @serializer = Serialization::Serializer.new(prefix: @prefix, global_tags: tags)
102
+ @sample_rate = sample_rate
103
+
104
+ @forwarder = Forwarder.new(
105
+ host: host,
106
+ port: port,
107
+ socket_path: socket_path,
221
108
 
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)}
109
+ global_tags: tags,
110
+ logger: logger,
224
111
 
225
- @batch = Batch.new @connection, max_buffer_bytes
112
+ single_thread: single_thread,
113
+
114
+ buffer_max_payload_size: buffer_max_payload_size,
115
+ buffer_max_pool_size: buffer_max_pool_size,
116
+ buffer_overflowing_stategy: buffer_overflowing_stategy,
117
+
118
+ telemetry_flush_interval: telemetry_enable ? telemetry_flush_interval : nil,
119
+ )
226
120
  end
227
121
 
228
122
  # yield a new instance to a block and close it when done
229
123
  # for short-term use-cases that don't want to close the socket manually
230
124
  def self.open(*args)
231
125
  instance = new(*args)
126
+
232
127
  yield instance
233
128
  ensure
234
129
  instance.close
@@ -242,10 +137,10 @@ module Datadog
242
137
  # @option opts [Array<String>] :tags An array of tags
243
138
  # @option opts [Numeric] :by increment value, default 1
244
139
  # @see #count
245
- def increment(stat, opts=EMPTY_OPTIONS)
246
- opts = {:sample_rate => opts} if opts.is_a? Numeric
140
+ def increment(stat, opts = EMPTY_OPTIONS)
141
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
247
142
  incr_value = opts.fetch(:by, 1)
248
- count stat, incr_value, opts
143
+ count(stat, incr_value, opts)
249
144
  end
250
145
 
251
146
  # Sends a decrement (count = -1) for the given stat to the statsd server.
@@ -256,10 +151,10 @@ module Datadog
256
151
  # @option opts [Array<String>] :tags An array of tags
257
152
  # @option opts [Numeric] :by decrement value, default 1
258
153
  # @see #count
259
- def decrement(stat, opts=EMPTY_OPTIONS)
260
- opts = {:sample_rate => opts} if opts.is_a? Numeric
154
+ def decrement(stat, opts = EMPTY_OPTIONS)
155
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
261
156
  decr_value = - opts.fetch(:by, 1)
262
- count stat, decr_value, opts
157
+ count(stat, decr_value, opts)
263
158
  end
264
159
 
265
160
  # Sends an arbitrary count for the given stat to the statsd server.
@@ -269,9 +164,9 @@ module Datadog
269
164
  # @param [Hash] opts the options to create the metric with
270
165
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
271
166
  # @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
167
+ def count(stat, count, opts = EMPTY_OPTIONS)
168
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
169
+ send_stats(stat, count, COUNTER_TYPE, opts)
275
170
  end
276
171
 
277
172
  # Sends an arbitary gauge value for the given stat to the statsd server.
@@ -287,9 +182,9 @@ module Datadog
287
182
  # @option opts [Array<String>] :tags An array of tags
288
183
  # @example Report the current user count:
289
184
  # $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
185
+ def gauge(stat, value, opts = EMPTY_OPTIONS)
186
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
187
+ send_stats(stat, value, GAUGE_TYPE, opts)
293
188
  end
294
189
 
295
190
  # Sends a value to be tracked as a histogram to the statsd server.
@@ -301,14 +196,11 @@ module Datadog
301
196
  # @option opts [Array<String>] :tags An array of tags
302
197
  # @example Report the current user count:
303
198
  # $statsd.histogram('user.count', User.count)
304
- def histogram(stat, value, opts=EMPTY_OPTIONS)
305
- send_stats stat, value, HISTOGRAM_TYPE, opts
199
+ def histogram(stat, value, opts = EMPTY_OPTIONS)
200
+ send_stats(stat, value, HISTOGRAM_TYPE, opts)
306
201
  end
307
202
 
308
203
  # 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
204
  #
313
205
  # @param [String] stat stat name.
314
206
  # @param [Numeric] value distribution value.
@@ -317,8 +209,8 @@ module Datadog
317
209
  # @option opts [Array<String>] :tags An array of tags
318
210
  # @example Report the current user count:
319
211
  # $statsd.distribution('user.count', User.count)
320
- def distribution(stat, value, opts=EMPTY_OPTIONS)
321
- send_stats stat, value, DISTRIBUTION_TYPE, opts
212
+ def distribution(stat, value, opts = EMPTY_OPTIONS)
213
+ send_stats(stat, value, DISTRIBUTION_TYPE, opts)
322
214
  end
323
215
 
324
216
  # Sends a timing (in ms) for the given stat to the statsd server. The
@@ -331,9 +223,9 @@ module Datadog
331
223
  # @param [Hash] opts the options to create the metric with
332
224
  # @option opts [Numeric] :sample_rate sample rate, 1 for always
333
225
  # @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
226
+ def timing(stat, ms, opts = EMPTY_OPTIONS)
227
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
228
+ send_stats(stat, ms, TIMING_TYPE, opts)
337
229
  end
338
230
 
339
231
  # Reports execution time of the provided block using {#timing}.
@@ -349,13 +241,12 @@ module Datadog
349
241
  # @see #timing
350
242
  # @example Report the time (in ms) taken to activate an account
351
243
  # $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
244
+ def time(stat, opts = EMPTY_OPTIONS)
245
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
246
+ start = now
247
+ yield
356
248
  ensure
357
- finished = (PROCESS_TIME_SUPPORTED ? Process.clock_gettime(Process::CLOCK_MONOTONIC) : Time.now.to_f)
358
- timing(stat, ((finished - start) * 1000).round, opts)
249
+ timing(stat, ((now - start) * 1000).round, opts)
359
250
  end
360
251
 
361
252
  # Sends a value to be tracked as a set to the statsd server.
@@ -367,9 +258,9 @@ module Datadog
367
258
  # @option opts [Array<String>] :tags An array of tags
368
259
  # @example Record a unique visitory by id:
369
260
  # $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
261
+ def set(stat, value, opts = EMPTY_OPTIONS)
262
+ opts = { sample_rate: opts } if opts.is_a?(Numeric)
263
+ send_stats(stat, value, SET_TYPE, opts)
373
264
  end
374
265
 
375
266
  # This method allows you to send custom service check statuses.
@@ -377,14 +268,16 @@ module Datadog
377
268
  # @param [String] name Service check name
378
269
  # @param [String] status Service check status.
379
270
  # @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.
271
+ # @option opts [Integer, String, nil] :timestamp (nil) Assign a timestamp to the service check. Default is now when none
272
+ # @option opts [String, nil] :hostname (nil) Assign a hostname to the service check.
382
273
  # @option opts [Array<String>, nil] :tags (nil) An array of tags
383
274
  # @option opts [String, nil] :message (nil) A message to associate with this service check status
384
275
  # @example Report a critical service check status
385
276
  # $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)
277
+ def service_check(name, status, opts = EMPTY_OPTIONS)
278
+ telemetry.sent(service_checks: 1) if telemetry
279
+
280
+ forwarder.send_message(serializer.to_service_check(name, status, opts))
388
281
  end
389
282
 
390
283
  # 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,23 +287,33 @@ module Datadog
394
287
  # it will be grouped with other events that don't have an event type.
395
288
  #
396
289
  # @param [String] title Event title
397
- # @param [String] text Event text. Supports \n
290
+ # @param [String] text Event text. Supports newlines (+\n+)
398
291
  # @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
292
+ # @option opts [Integer, String, nil] :date_happened (nil) Assign a timestamp to the event. Default is now when none
400
293
  # @option opts [String, nil] :hostname (nil) Assign a hostname to the event.
401
294
  # @option opts [String, nil] :aggregation_key (nil) Assign an aggregation key to the event, to group it with some others
402
295
  # @option opts [String, nil] :priority ('normal') Can be "normal" or "low"
403
296
  # @option opts [String, nil] :source_type_name (nil) Assign a source type to the event
404
297
  # @option opts [String, nil] :alert_type ('info') Can be "error", "warning", "info" or "success".
298
+ # @option opts [Boolean, false] :truncate_if_too_long (false) Truncate the event if it is too long
405
299
  # @option opts [Array<String>] :tags tags to be added to every metric
406
300
  # @example Report an awful event:
407
301
  # $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)
302
+ def event(title, text, opts = EMPTY_OPTIONS)
303
+ telemetry.sent(events: 1) if telemetry
304
+
305
+ forwarder.send_message(serializer.to_event(title, text, opts))
410
306
  end
411
307
 
412
- # Send several metrics in the same UDP Packet
413
- # They will be buffered and flushed when the block finishes
308
+ # Send several metrics in the same packet.
309
+ # They will be buffered and flushed when the block finishes.
310
+ #
311
+ # This method exists for compatibility with v4.x versions, it is not needed
312
+ # anymore since the batching is now automatically done internally.
313
+ # It also means that an automatic flush could occur if the buffer is filled
314
+ # during the execution of the batch block.
315
+ #
316
+ # This method is DEPRECATED and will be removed in future v6.x API.
414
317
  #
415
318
  # @example Send several metrics in one packet:
416
319
  # $statsd.batch do |s|
@@ -418,140 +321,73 @@ module Datadog
418
321
  # s.increment('page.views')
419
322
  # end
420
323
  def batch
421
- @batch.open { yield self }
324
+ yield self
325
+ flush(sync: true)
422
326
  end
423
327
 
424
328
  # Close the underlying socket
425
- def close
426
- @connection.close
329
+ #
330
+ # @param [Boolean, true] flush Should we flush the metrics before closing
331
+ def close(flush: true)
332
+ flush(sync: true) if flush
333
+ forwarder.close
427
334
  end
428
335
 
429
- private
430
-
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")
439
- EMPTY_OPTIONS = {}.freeze
440
-
441
- private_constant :NEW_LINE, :ESC_NEW_LINE, :COMMA, :PIPE, :DOT,
442
- :DOUBLE_COLON, :UNDERSCORE, :EMPTY_OPTIONS
443
-
444
- def format_service_check(name, status, opts=EMPTY_OPTIONS)
445
- sc_string = "_sc|#{name}|#{status}".dup
446
-
447
- SC_OPT_KEYS.each do |key, shorthand_key|
448
- next unless opts[key]
449
-
450
- if key == :tags
451
- if tags_string = tags_as_string(opts)
452
- sc_string << "|##{tags_string}"
453
- end
454
- elsif key == :message
455
- message = remove_pipes(opts[:message])
456
- escaped_message = escape_service_check_message(message)
457
- sc_string << "|m:#{escaped_message}"
458
- else
459
- value = remove_pipes(opts[key])
460
- sc_string << "|#{shorthand_key}#{value}"
461
- end
462
- end
463
- sc_string
336
+ def sync_with_outbound_io
337
+ forwarder.sync_with_outbound_io
464
338
  end
465
339
 
466
- def format_event(title, text, opts=EMPTY_OPTIONS)
467
- escaped_title = escape_event_content(title)
468
- escaped_text = escape_event_content(text)
469
- event_string_data = "_e{#{escaped_title.length},#{escaped_text.length}}:#{escaped_title}|#{escaped_text}".dup
470
-
471
- # We construct the string to be sent by adding '|key:value' parts to it when needed
472
- # All pipes ('|') in the metadata are removed. Title and Text can keep theirs
473
- OPTS_KEYS.each do |key, shorthand_key|
474
- if key != :tags && opts[key]
475
- value = remove_pipes(opts[key])
476
- event_string_data << "|#{shorthand_key}:#{value}"
477
- end
478
- end
479
-
480
- # Tags are joined and added as last part to the string to be sent
481
- if tags_string = tags_as_string(opts)
482
- event_string_data << "|##{tags_string}"
483
- end
484
-
485
- raise "Event #{title} payload is too big (more that 8KB), event discarded" if event_string_data.length > MAX_EVENT_SIZE
486
- event_string_data
340
+ # Flush the buffer into the connection
341
+ def flush(flush_telemetry: false, sync: false)
342
+ forwarder.flush(flush_telemetry: flush_telemetry, sync: sync)
487
343
  end
488
344
 
489
- def tags_as_string(opts)
490
- if tag_arr = opts[:tags]
491
- tag_arr = tag_arr.map { |tag| escape_tag_content(tag) }
492
- tag_arr = tags + tag_arr # @tags are normalized when set, so not need to normalize them again
493
- else
494
- tag_arr = tags
495
- end
496
- tag_arr.join(COMMA) unless tag_arr.empty?
345
+ def telemetry
346
+ forwarder.telemetry
497
347
  end
498
348
 
499
- def escape_event_content(msg)
500
- msg.gsub NEW_LINE, ESC_NEW_LINE
349
+ def host
350
+ forwarder.host
501
351
  end
502
352
 
503
- def escape_tag_content(tag)
504
- tag = remove_pipes(tag.to_s)
505
- tag.delete! COMMA
506
- tag
353
+ def port
354
+ forwarder.port
507
355
  end
508
356
 
509
- def remove_pipes(msg)
510
- msg.delete PIPE
357
+ def socket_path
358
+ forwarder.socket_path
511
359
  end
512
360
 
513
- def escape_service_check_message(msg)
514
- escape_event_content(msg).gsub('m:'.freeze, 'm\:'.freeze)
361
+ def transport_type
362
+ forwarder.transport_type
515
363
  end
516
364
 
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
520
- full_stat = ''.dup
521
- full_stat << @prefix if @prefix
522
-
523
- stat = stat.is_a?(String) ? stat.dup : stat.to_s
524
- # Replace Ruby module scoping with '.' and reserved chars (: | @) with underscores.
525
- stat.gsub!(DOUBLE_COLON, DOT)
526
- stat.tr!(':|@'.freeze, UNDERSCORE)
527
- full_stat << stat
528
-
529
- full_stat << ':'.freeze
530
- full_stat << delta.to_s
531
- full_stat << PIPE
532
- full_stat << type
533
-
534
- unless sample_rate == 1
535
- full_stat << PIPE
536
- full_stat << '@'.freeze
537
- full_stat << sample_rate.to_s
538
- end
539
-
540
- if tags_string = tags_as_string(opts)
541
- full_stat << PIPE
542
- full_stat << '#'.freeze
543
- full_stat << tags_string
544
- end
545
-
546
- send_stat(full_stat)
365
+ private
366
+ attr_reader :serializer
367
+ attr_reader :forwarder
368
+
369
+ PROCESS_TIME_SUPPORTED = (RUBY_VERSION >= '2.1.0')
370
+ EMPTY_OPTIONS = {}.freeze
371
+
372
+ if PROCESS_TIME_SUPPORTED
373
+ def now
374
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
375
+ end
376
+ else
377
+ def now
378
+ Time.now.to_f
547
379
  end
548
380
  end
549
381
 
550
- def send_stat(message)
551
- if @batch.open?
552
- @batch.add message
553
- else
554
- @connection.write(message)
382
+ def send_stats(stat, delta, type, opts = EMPTY_OPTIONS)
383
+ telemetry.sent(metrics: 1) if telemetry
384
+
385
+ sample_rate = opts[:sample_rate] || @sample_rate || 1
386
+
387
+ if sample_rate == 1 || rand <= sample_rate
388
+ full_stat = serializer.to_stat(stat, delta, type, tags: opts[:tags], sample_rate: sample_rate)
389
+
390
+ forwarder.send_message(full_stat)
555
391
  end
556
392
  end
557
393
  end