statsd-instrument 1.7.2 → 2.0.0beta
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.travis.yml +5 -3
- data/Gemfile +1 -4
- data/README.md +97 -29
- data/lib/statsd/instrument/assertions.rb +46 -0
- data/lib/statsd/instrument/backend.rb +10 -0
- data/lib/statsd/instrument/backends/capture_backend.rb +17 -0
- data/lib/statsd/instrument/backends/logger_backend.rb +14 -0
- data/lib/statsd/instrument/backends/null_backend.rb +6 -0
- data/lib/statsd/instrument/backends/udp_backend.rb +88 -0
- data/lib/statsd/instrument/environment.rb +27 -0
- data/lib/statsd/instrument/metric.rb +50 -0
- data/lib/statsd/instrument/version.rb +1 -1
- data/lib/statsd/instrument.rb +19 -86
- data/statsd-instrument.gemspec +2 -1
- data/test/assertions_test.rb +126 -0
- data/test/capture_backend_test.rb +24 -0
- data/test/environment_test.rb +35 -0
- data/test/integration_test.rb +20 -0
- data/test/logger_backend_test.rb +20 -0
- data/test/metric_test.rb +38 -0
- data/test/statsd_instrumentation_test.rb +69 -46
- data/test/statsd_test.rb +56 -220
- data/test/test_helper.rb +5 -3
- data/test/udp_backend_test.rb +135 -0
- metadata +49 -18
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: d50bba4fad7c7d60e2272ec23888004b0cbebbd1
|
4
|
+
data.tar.gz: b36122aa5c275370c1a0e3e9edd3803321a4a960
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 0e060d63fb216442bd387a131adca06326560aec60813efe34a608182b5066e698fdf217f13bee3ffbdcd8489e105079b0e99aca507729627a73014b5e2749e6
|
7
|
+
data.tar.gz: 39f3c512927e0c8ac41ff7c21936853d1d7e048b02ed3030c2c21bd67e3f07f703d6093fa54062617e102b9b273a809c23c218675b1a6280b349a46f71630856
|
data/.travis.yml
CHANGED
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -4,9 +4,9 @@
|
|
4
4
|
|
5
5
|
This is a ruby client for statsd (http://github.com/etsy/statsd). It provides a lightweight way to track and measure metrics in your application.
|
6
6
|
|
7
|
-
We call out to statsd by sending data over a UDP socket. UDP sockets are fast, but unreliable, there is no guarantee that your data will ever arrive at
|
7
|
+
We call out to statsd by sending data over a UDP socket. UDP sockets are fast, but unreliable, there is no guarantee that your data will ever arrive at its location. In other words, fire and forget. This is perfect for this use case because it means your code doesn't get bogged down trying to log statistics. We send data to statsd several times per request and haven't noticed a performance hit.
|
8
8
|
|
9
|
-
The fact that all of your stats data may not make it into statsd is no issue. Graphite (the graph database that statsd is built on) will only show you trends in your data. Internally it only keeps enough data to satisfy the levels of granularity we specify. As well as satisfying
|
9
|
+
The fact that all of your stats data may not make it into statsd is no issue. Graphite (the graph database that statsd is built on) will only show you trends in your data. Internally it only keeps enough data to satisfy the levels of granularity we specify. As well as satisfying its requirement as a fixed size database. We can throw as much data at it as we want it and it will do its best to show us the trends over time and get rid of the fluff.
|
10
10
|
|
11
11
|
For Shopify, our retention periods are:
|
12
12
|
|
@@ -18,39 +18,48 @@ This is the same as what Etsy uses (mentioned in the README for http://github.co
|
|
18
18
|
|
19
19
|
## Configuration
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
# This is set to the environment variable STATSD_ADDR if it is set.
|
24
|
-
StatsD.server = 'statsd.myservice.com:8125'
|
21
|
+
The library comes with different backends. Based on your environment (detected using environment
|
22
|
+
variables), it will select one of the following backends by default:
|
25
23
|
|
26
|
-
|
27
|
-
|
28
|
-
|
24
|
+
- **Production** environment: `StatsD::Instrument::Backend::UDPBackend` will actually send UDP packets.
|
25
|
+
It will configure itself using environment variables: it uses `STATSD_ADDR` for the address to connect
|
26
|
+
to (default `"localhost:8125"`), and `STATSD_IMPLEMENTATION` to set the protocol variant. (See below)
|
27
|
+
- **Test** environment: `StatsD::Instrument::Backend::NullBackend` will swallow all calls. See below for
|
28
|
+
notes on writing tests.
|
29
|
+
- **Development**, and all other, environments: `StatsD::Instrument::Backend::LoggerBackend` will log all
|
30
|
+
calls to stdout.
|
29
31
|
|
30
|
-
|
31
|
-
# In production only errors are logged to the console.
|
32
|
-
StatsD.logger = Rails.logger
|
32
|
+
You can override the currently active backend by setting `StatsD.backend`:
|
33
33
|
|
34
|
-
|
35
|
-
|
34
|
+
``` ruby
|
35
|
+
# Sets up a UDP backend. First argument is the UDP address to send StatsD packets to,
|
36
|
+
# second argument specifies the protocol variant (i.e. `:statsd`, `:statsite`, or `:datadog`).
|
37
|
+
StatsD.backend = StatsD::Instrument::Backend::UDPBackend.new("1.2.3.4:8125", :statsite)
|
36
38
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
39
|
+
# Sets up a logger backend
|
40
|
+
StatsD.backend = StatsD::Instrument::Backend::LoggerBackend.new(Rails.logger)
|
41
|
+
```
|
40
42
|
|
43
|
+
The other available settings, with their default, are
|
41
44
|
|
42
|
-
```
|
45
|
+
``` ruby
|
46
|
+
# Logger to which commands are logged when using the LoggerBackend, which is
|
47
|
+
# the default in development environment. Also, any errors or warnings will
|
48
|
+
# be logged here.
|
49
|
+
StatsD.logger = defiend?(Rails) : Rails.logger ? Logger.new($stderr)
|
43
50
|
|
44
|
-
|
51
|
+
# An optional prefix to be added to each metric.
|
52
|
+
StatsD.prefix = nil # but can be set to any string
|
45
53
|
|
46
|
-
|
47
|
-
|
48
|
-
StatsD.
|
54
|
+
# Sample 10% of events. By default all events are reported, which may overload your network or server.
|
55
|
+
# You can, and should vary this on a per metric basis, depending on frequency and accuracy requirements
|
56
|
+
StatsD.default_sample_rate = (ENV['STATSD_SAMPLE_RATE'] || 0.1 ).to_f
|
49
57
|
```
|
50
58
|
|
51
59
|
## StatsD keys
|
52
60
|
|
53
|
-
StatsD keys look like 'admin.logins.api.success'.
|
61
|
+
StatsD keys look like 'admin.logins.api.success'. Dots are used as namespace separators.
|
62
|
+
In Graphite, they will show up as folders.
|
54
63
|
|
55
64
|
## Usage
|
56
65
|
|
@@ -185,7 +194,7 @@ the object the function is being called on and the array of arguments
|
|
185
194
|
passed.
|
186
195
|
|
187
196
|
```ruby
|
188
|
-
GoogleBase.statsd_count :insert,
|
197
|
+
GoogleBase.statsd_count :insert, lambda{|object, args| object.class.to_s.downcase + "." + args.first.to_s + ".insert" }
|
189
198
|
```
|
190
199
|
|
191
200
|
### Tags
|
@@ -197,9 +206,70 @@ StatsD.increment('my.counter', tags: ['env:production', 'unicorn'])
|
|
197
206
|
GoogleBase.statsd_count :insert, 'GoogleBase.insert', tags: ['env:production']
|
198
207
|
```
|
199
208
|
|
209
|
+
If implementation is not set to `:datadog`, tags will not be included in the UDP packets, and a
|
210
|
+
warning is logged to `StatsD.logger`.
|
211
|
+
|
212
|
+
## Testing
|
213
|
+
|
214
|
+
This library come swith a module called `StatsD::Instrument::Assertions` to help you write tests
|
215
|
+
to verify StatsD is called properly.
|
216
|
+
|
217
|
+
``` ruby
|
218
|
+
class MyTestcase < Minitest::Test
|
219
|
+
include StatsD::Instrument::Assertions
|
220
|
+
|
221
|
+
def test_some_metrics
|
222
|
+
# This will pass if there is exactly one matching StatsD call
|
223
|
+
# it will ignore any other, non matching calls.
|
224
|
+
assert_statsd_increment('counter.name', sample_rate: 1.0) do
|
225
|
+
StatsD.increment('unrelated') # doesn't match
|
226
|
+
StatsD.increment('counter.name', sample_rate: 1.0) # matches
|
227
|
+
StatsD.increment('counter.name', sample_rate: 0.1) # doesn't match
|
228
|
+
end
|
229
|
+
|
230
|
+
# Set `times` if there will be multiple matches:
|
231
|
+
assert_statsd_increment('counter.name', times: 2) do
|
232
|
+
StatsD.increment('unrelated') # doesn't match
|
233
|
+
StatsD.increment('counter.name', sample_rate: 1.0) # matches
|
234
|
+
StatsD.increment('counter.name', sample_rate: 0.1) # matches too
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def test_no_udp_traffic
|
239
|
+
# Verifies no StatsD calls occured at all.
|
240
|
+
assert_no_statsd_calls do
|
241
|
+
do_some_work
|
242
|
+
end
|
243
|
+
|
244
|
+
# Verifies no StatsD calls occured for the given metric.
|
245
|
+
assert_no_statsd_calls('metric_name') do
|
246
|
+
do_some_work
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_more_complicated_stuff
|
251
|
+
# capture_statsd_calls will capture all the StatsD calls in the
|
252
|
+
# given block, and returns them as an array. You can then run your
|
253
|
+
# own assertions on it.
|
254
|
+
metrics = capture_statsd_calls do
|
255
|
+
StatsD.increment('mycounter', sample_rate: 0.01)
|
256
|
+
end
|
257
|
+
|
258
|
+
assert_equal 1, metrics.length
|
259
|
+
assert_equal 'mycounter', metrics[0].name
|
260
|
+
assert_equal :c, metrics[0].type
|
261
|
+
assert_equal 1, metrics[0].value
|
262
|
+
assert_equal 0.01, metrics[0].sample_rate
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
```
|
267
|
+
|
200
268
|
## Reliance on DNS
|
201
269
|
|
202
|
-
Out of the box StatsD is set up to be unidirectional fire-and-forget over UDP. Configuring
|
270
|
+
Out of the box StatsD is set up to be unidirectional fire-and-forget over UDP. Configuring
|
271
|
+
the StatsD host to be a non-ip will trigger a DNS lookup (i.e. a synchronous TCP round trip).
|
272
|
+
This can be particularly problematic in clouds that have a shared DNS infrastructure such as AWS.
|
203
273
|
|
204
274
|
### Common Workarounds
|
205
275
|
|
@@ -211,11 +281,9 @@ Out of the box StatsD is set up to be unidirectional fire-and-forget over UDP. C
|
|
211
281
|
|
212
282
|
Tested on several Ruby versions using Travis CI:
|
213
283
|
|
214
|
-
* Ruby 1.8.7
|
215
|
-
* Ruby Enterprise Edition 1.8.7
|
216
284
|
* Ruby 1.9.3
|
217
285
|
* Ruby 2.0.0
|
218
|
-
* Ruby 2.1.
|
286
|
+
* Ruby 2.1.1
|
219
287
|
|
220
288
|
## Contributing
|
221
289
|
|
@@ -224,5 +292,5 @@ This project is MIT licensed and welcomes outside contributions.
|
|
224
292
|
1. Fork the repository, and create a feature branch.
|
225
293
|
2. Implement the feature, and add tests that cover the new changes functionality.
|
226
294
|
3. Update the README.
|
227
|
-
4. Create a pull request. Make sure that you get a CI pass on it.
|
295
|
+
4. Create a pull request. Make sure that you get a Travis CI pass on it.
|
228
296
|
5. Ping @jstorimer and/or @wvanbergen for review.
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module StatsD::Instrument::Assertions
|
2
|
+
|
3
|
+
def capture_statsd_calls(&block)
|
4
|
+
mock_backend = StatsD::Instrument::Backends::CaptureBackend.new
|
5
|
+
old_backend, StatsD.backend = StatsD.backend, mock_backend
|
6
|
+
block.call
|
7
|
+
mock_backend.collected_metrics
|
8
|
+
ensure
|
9
|
+
StatsD.backend = old_backend
|
10
|
+
end
|
11
|
+
|
12
|
+
def assert_no_statsd_calls(metric_name = nil, &block)
|
13
|
+
metrics = capture_statsd_calls(&block)
|
14
|
+
metrics.select! { |m| m.name == metric_name } if metric_name
|
15
|
+
assert metrics.empty?, "No StatsD calls for metric #{metric_name} expected."
|
16
|
+
end
|
17
|
+
|
18
|
+
def assert_statsd_increment(metric_name, options = {}, &block)
|
19
|
+
assert_statsd_call(:c, metric_name, options, &block)
|
20
|
+
end
|
21
|
+
|
22
|
+
def assert_statsd_measure(metric_name, options = {}, &block)
|
23
|
+
assert_statsd_call(:ms, metric_name, options, &block)
|
24
|
+
end
|
25
|
+
|
26
|
+
def assert_statsd_gauge(metric_name, options = {}, &block)
|
27
|
+
assert_statsd_call(:g, metric_name, options, &block)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def assert_statsd_call(metric_type, metric_name, options = {}, &block)
|
33
|
+
options[:times] ||= 1
|
34
|
+
metrics = capture_statsd_calls(&block)
|
35
|
+
metrics = metrics.select { |m| m.type == metric_type && m.name == metric_name }
|
36
|
+
assert metrics.length > 0, "No StatsD calls for metric #{metric_name} were made."
|
37
|
+
assert options[:times] === metrics.length, "The amount of StatsD calls for metric #{metric_name} was unexpected"
|
38
|
+
metric = metrics.first
|
39
|
+
|
40
|
+
assert_equal options[:sample_rate], metric.sample_rate, "Unexpected value submitted for StatsD metric #{metric_name}" if options[:sample_rate]
|
41
|
+
assert_equal options[:value], metric.value, "Unexpected StatsD sample rate for metric #{metric_name}" if options[:value]
|
42
|
+
assert_equal Set.new(options[:tags]), Set.new(metric.tags), "Unexpected StatsD tags for metric #{metric_name}" if options[:tags]
|
43
|
+
|
44
|
+
metric
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,10 @@
|
|
1
|
+
class StatsD::Instrument::Backend
|
2
|
+
def collect_metric(metric)
|
3
|
+
raise NotImplementedError, "Use a concerete backend implementation"
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
require 'statsd/instrument/backends/logger_backend'
|
8
|
+
require 'statsd/instrument/backends/null_backend'
|
9
|
+
require 'statsd/instrument/backends/capture_backend'
|
10
|
+
require 'statsd/instrument/backends/udp_backend'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module StatsD::Instrument::Backends
|
2
|
+
class CaptureBackend < StatsD::Instrument::Backend
|
3
|
+
attr_reader :collected_metrics
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
reset
|
7
|
+
end
|
8
|
+
|
9
|
+
def collect_metric(metric)
|
10
|
+
@collected_metrics << metric
|
11
|
+
end
|
12
|
+
|
13
|
+
def reset
|
14
|
+
@collected_metrics = []
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module StatsD::Instrument::Backends
|
2
|
+
class LoggerBackend < StatsD::Instrument::Backend
|
3
|
+
|
4
|
+
attr_accessor :logger
|
5
|
+
|
6
|
+
def initialize(logger)
|
7
|
+
@logger = logger
|
8
|
+
end
|
9
|
+
|
10
|
+
def collect_metric(metric)
|
11
|
+
logger.info "[StatsD] #{metric}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module StatsD::Instrument::Backends
|
2
|
+
class UDPBackend < StatsD::Instrument::Backend
|
3
|
+
|
4
|
+
DEFAULT_IMPLEMENTATION = :statsd
|
5
|
+
|
6
|
+
attr_reader :host, :port
|
7
|
+
attr_accessor :implementation
|
8
|
+
|
9
|
+
def initialize(server = nil, implementation = nil)
|
10
|
+
self.server = server || "localhost:8125"
|
11
|
+
self.implementation = (implementation || DEFAULT_IMPLEMENTATION).to_sym
|
12
|
+
end
|
13
|
+
|
14
|
+
def collect_metric(metric)
|
15
|
+
unless implementation_supports_metric_type?(metric.type)
|
16
|
+
StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} not supported on #{implementation} implementation.")
|
17
|
+
return false
|
18
|
+
end
|
19
|
+
|
20
|
+
if metric.sample_rate < 1.0 && rand > metric.sample_rate
|
21
|
+
return false
|
22
|
+
end
|
23
|
+
|
24
|
+
write_packet(generate_packet(metric))
|
25
|
+
end
|
26
|
+
|
27
|
+
def implementation_supports_metric_type?(type)
|
28
|
+
case type
|
29
|
+
when :h; implementation == :datadog
|
30
|
+
when :kv; implementation == :statsite
|
31
|
+
else true
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def server=(connection_string)
|
36
|
+
self.host, port = connection_string.split(':', 2)
|
37
|
+
self.port = port.to_i
|
38
|
+
invalidate_socket
|
39
|
+
end
|
40
|
+
|
41
|
+
def host=(host)
|
42
|
+
@host = host
|
43
|
+
invalidate_socket
|
44
|
+
end
|
45
|
+
|
46
|
+
def port=(port)
|
47
|
+
@port = port
|
48
|
+
invalidate_socket
|
49
|
+
end
|
50
|
+
|
51
|
+
def socket
|
52
|
+
if @socket.nil?
|
53
|
+
@socket = UDPSocket.new
|
54
|
+
@socket.connect(host, port)
|
55
|
+
end
|
56
|
+
@socket
|
57
|
+
end
|
58
|
+
|
59
|
+
def generate_packet(metric)
|
60
|
+
command = "#{metric.name}:#{metric.value}|#{metric.type}"
|
61
|
+
command << "|@#{metric.sample_rate}" if metric.sample_rate < 1 || (implementation == :statsite && metric.sample_rate > 1)
|
62
|
+
if metric.tags
|
63
|
+
if tags_supported?
|
64
|
+
command << "|##{metric.tags.join(',')}"
|
65
|
+
else
|
66
|
+
StatsD.logger.warn("[StatsD] Tags are only supported on Datadog implementation.")
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
command << "\n" if implementation == :statsite
|
71
|
+
command
|
72
|
+
end
|
73
|
+
|
74
|
+
def tags_supported?
|
75
|
+
implementation == :datadog
|
76
|
+
end
|
77
|
+
|
78
|
+
def write_packet(command)
|
79
|
+
socket.send(command, 0) > 0
|
80
|
+
rescue SocketError, IOError, SystemCallError => e
|
81
|
+
StatsD.logger.error "[StatsD] #{e.class.name}: #{e.message}"
|
82
|
+
end
|
83
|
+
|
84
|
+
def invalidate_socket
|
85
|
+
@socket = nil
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module StatsD::Instrument::Environment
|
4
|
+
extend self
|
5
|
+
|
6
|
+
def default_backend
|
7
|
+
case environment
|
8
|
+
when 'production'
|
9
|
+
StatsD::Instrument::Backends::UDPBackend.new(ENV['STATSD_ADDR'], ENV['STATSD_IMPLEMENTATION'])
|
10
|
+
when 'test'
|
11
|
+
StatsD::Instrument::Backends::NullBackend.new
|
12
|
+
else
|
13
|
+
StatsD::Instrument::Backends::LoggerBackend.new(StatsD.logger)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def environment
|
18
|
+
if defined?(Rails)
|
19
|
+
Rails.env.to_s
|
20
|
+
else
|
21
|
+
ENV['RAILS_ENV'] || ENV['RACK_ENV'] || ENV['ENV'] || 'development'
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
StatsD.default_sample_rate = ENV.fetch('STATSD_SAMPLE_RATE', 1.0).to_f
|
27
|
+
StatsD.logger = defined?(Rails) ? Rails.logger : Logger.new($stderr)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class StatsD::Instrument::Metric
|
2
|
+
|
3
|
+
attr_accessor :type, :name, :value, :sample_rate, :tags
|
4
|
+
|
5
|
+
def initialize(options = {})
|
6
|
+
@type = options[:type] or raise ArgumentError, "Metric :type is required."
|
7
|
+
@name = options[:name] or raise ArgumentError, "Metric :name is required."
|
8
|
+
@name = StatsD.prefix ? "#{StatsD.prefix}.#{@name}" : @name unless options[:no_prefix]
|
9
|
+
|
10
|
+
@value = options[:value] || default_value
|
11
|
+
@sample_rate = options[:sample_rate] || StatsD.default_sample_rate
|
12
|
+
@tags = StatsD::Instrument::Metric.normalize_tags(options[:tags])
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_value
|
16
|
+
case type
|
17
|
+
when :c; 1
|
18
|
+
else raise ArgumentError, "A value is required for metric type #{type.inspect}."
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
str = "#{TYPES[type]} #{name}:#{value}"
|
24
|
+
str << " @#{sample_rate}" if sample_rate != 1.0
|
25
|
+
str << " " << tags.map { |t| "##{t}"}.join(' ') if tags
|
26
|
+
str
|
27
|
+
end
|
28
|
+
|
29
|
+
def inspect
|
30
|
+
"#<StatsD::Instrument::Metric #{self.to_s}>"
|
31
|
+
end
|
32
|
+
|
33
|
+
TYPES = {
|
34
|
+
c: 'increment',
|
35
|
+
ms: 'measure',
|
36
|
+
g: 'gauge',
|
37
|
+
h: 'histogram',
|
38
|
+
kv: 'key/value',
|
39
|
+
s: 'set',
|
40
|
+
}
|
41
|
+
|
42
|
+
def self.normalize_tags(tags)
|
43
|
+
return if tags.nil?
|
44
|
+
tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
|
45
|
+
tags.map do |tag|
|
46
|
+
components = tag.split(':', 2)
|
47
|
+
components.map { |c| c.gsub(/[^\w\.-]+/, '_') }.join(':')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/lib/statsd/instrument.rb
CHANGED
@@ -2,8 +2,6 @@ require 'socket'
|
|
2
2
|
require 'benchmark'
|
3
3
|
require 'logger'
|
4
4
|
|
5
|
-
require 'statsd/instrument/version'
|
6
|
-
|
7
5
|
module StatsD
|
8
6
|
module Instrument
|
9
7
|
|
@@ -110,41 +108,23 @@ module StatsD
|
|
110
108
|
end
|
111
109
|
|
112
110
|
class << self
|
113
|
-
attr_accessor :
|
114
|
-
|
115
|
-
def server=(conn)
|
116
|
-
self.host, port = conn.split(':')
|
117
|
-
self.port = port.to_i
|
118
|
-
invalidate_socket
|
119
|
-
end
|
111
|
+
attr_accessor :logger, :default_sample_rate, :prefix
|
112
|
+
attr_writer :backend
|
120
113
|
|
121
|
-
def
|
122
|
-
@
|
123
|
-
invalidate_socket
|
124
|
-
end
|
125
|
-
|
126
|
-
def port=(port)
|
127
|
-
@port = port
|
128
|
-
invalidate_socket
|
129
|
-
end
|
130
|
-
|
131
|
-
def invalidate_socket
|
132
|
-
@socket = nil
|
114
|
+
def backend
|
115
|
+
@backend ||= StatsD::Instrument::Environment.default_backend
|
133
116
|
end
|
134
117
|
|
135
118
|
# glork:320|ms
|
136
|
-
def measure(key, value = nil, *metric_options)
|
119
|
+
def measure(key, value = nil, *metric_options, &block)
|
137
120
|
if value.is_a?(Hash) && metric_options.empty?
|
138
121
|
metric_options = [value]
|
139
122
|
value = nil
|
140
123
|
end
|
141
124
|
|
142
125
|
result = nil
|
143
|
-
ms = value || 1000 * Benchmark.realtime
|
144
|
-
|
145
|
-
end
|
146
|
-
|
147
|
-
collect(:ms, key, ms, hash_argument(metric_options))
|
126
|
+
ms = value || 1000 * Benchmark.realtime { result = block.call }
|
127
|
+
collect_metric(hash_argument(metric_options).merge(type: :ms, name: key, value: ms))
|
148
128
|
result
|
149
129
|
end
|
150
130
|
|
@@ -155,29 +135,27 @@ module StatsD
|
|
155
135
|
value = 1
|
156
136
|
end
|
157
137
|
|
158
|
-
|
138
|
+
collect_metric(hash_argument(metric_options).merge(type: :c, name: key, value: value))
|
159
139
|
end
|
160
140
|
|
161
141
|
# gaugor:333|g
|
162
142
|
# guagor:1234|kv|@1339864935 (statsite)
|
163
143
|
def gauge(key, value, *metric_options)
|
164
|
-
|
144
|
+
collect_metric(hash_argument(metric_options).merge(type: :g, name: key, value: value))
|
165
145
|
end
|
166
146
|
|
167
147
|
# histogram:123.45|h
|
168
148
|
def histogram(key, value, *metric_options)
|
169
|
-
|
170
|
-
collect(:h, key, value, hash_argument(metric_options))
|
149
|
+
collect_metric(hash_argument(metric_options).merge(type: :h, name: key, value: value))
|
171
150
|
end
|
172
151
|
|
173
152
|
def key_value(key, value, *metric_options)
|
174
|
-
|
175
|
-
collect(:kv, key, value, hash_argument(metric_options))
|
153
|
+
collect_metric(hash_argument(metric_options).merge(type: :kv, name: key, value: value))
|
176
154
|
end
|
177
155
|
|
178
156
|
# uniques:765|s
|
179
157
|
def set(key, value, *metric_options)
|
180
|
-
|
158
|
+
collect_metric(hash_argument(metric_options).merge(type: :s, name: key, value: value))
|
181
159
|
end
|
182
160
|
|
183
161
|
private
|
@@ -195,59 +173,14 @@ module StatsD
|
|
195
173
|
return hash
|
196
174
|
end
|
197
175
|
|
198
|
-
def
|
199
|
-
|
200
|
-
@socket = UDPSocket.new
|
201
|
-
@socket.connect(host, port)
|
202
|
-
end
|
203
|
-
@socket
|
204
|
-
end
|
205
|
-
|
206
|
-
def collect(type, k, v, options = {})
|
207
|
-
return unless enabled
|
208
|
-
sample_rate = options[:sample_rate] || StatsD.default_sample_rate
|
209
|
-
return if sample_rate < 1 && rand > sample_rate
|
210
|
-
|
211
|
-
packet = generate_packet(type, k, v, sample_rate, options[:tags])
|
212
|
-
write_packet(packet)
|
213
|
-
end
|
214
|
-
|
215
|
-
def write_packet(command)
|
216
|
-
if mode.to_s == 'production'
|
217
|
-
socket.send(command, 0)
|
218
|
-
else
|
219
|
-
logger.info "[StatsD] #{command}"
|
220
|
-
end
|
221
|
-
rescue SocketError, IOError, SystemCallError => e
|
222
|
-
logger.error e
|
223
|
-
end
|
224
|
-
|
225
|
-
def clean_tags(tags)
|
226
|
-
tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
|
227
|
-
tags.map do |tag|
|
228
|
-
components = tag.split(':', 2)
|
229
|
-
components.map { |c| c.gsub(/[^\w\.-]+/, '_') }.join(':')
|
230
|
-
end
|
231
|
-
end
|
232
|
-
|
233
|
-
def generate_packet(type, k, v, sample_rate = default_sample_rate, tags = nil)
|
234
|
-
command = self.prefix ? self.prefix + '.' : ''
|
235
|
-
command << "#{k}:#{v}|#{type}"
|
236
|
-
command << "|@#{sample_rate}" if sample_rate < 1 || (self.implementation == :statsite && sample_rate > 1)
|
237
|
-
if tags
|
238
|
-
raise ArgumentError, "Tags are only supported on :datadog implementation" unless self.implementation == :datadog
|
239
|
-
command << "|##{clean_tags(tags).join(',')}"
|
240
|
-
end
|
241
|
-
|
242
|
-
command << "\n" if self.implementation == :statsite
|
243
|
-
command
|
176
|
+
def collect_metric(options)
|
177
|
+
backend.collect_metric(StatsD::Instrument::Metric.new(options))
|
244
178
|
end
|
245
179
|
end
|
246
180
|
end
|
247
181
|
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
StatsD.logger = Logger.new($stderr)
|
182
|
+
require 'statsd/instrument/metric'
|
183
|
+
require 'statsd/instrument/backend'
|
184
|
+
require 'statsd/instrument/assertions'
|
185
|
+
require 'statsd/instrument/environment'
|
186
|
+
require 'statsd/instrument/version'
|
data/statsd-instrument.gemspec
CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
|
|
11
11
|
spec.homepage = "https://github.com/Shopify/statsd-instrument"
|
12
12
|
spec.summary = %q{A StatsD client for Ruby apps}
|
13
13
|
spec.description = %q{A StatsD client for Ruby appspec. Provides metaprogramming methods to inject StatsD instrumentation into your code.}
|
14
|
-
spec.license
|
14
|
+
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files`.split($/)
|
17
17
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
@@ -19,5 +19,6 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
21
|
spec.add_development_dependency 'rake'
|
22
|
+
spec.add_development_dependency 'minitest'
|
22
23
|
spec.add_development_dependency 'mocha'
|
23
24
|
end
|