qubole-statsd-instrument 2.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +7 -0
- data/.travis.yml +12 -0
- data/CHANGELOG.md +89 -0
- data/CONTRIBUTING.md +34 -0
- data/Gemfile +2 -0
- data/LICENSE +20 -0
- data/README.md +319 -0
- data/Rakefile +10 -0
- data/lib/statsd/instrument/assertions.rb +88 -0
- data/lib/statsd/instrument/backend.rb +17 -0
- data/lib/statsd/instrument/backends/capture_backend.rb +29 -0
- data/lib/statsd/instrument/backends/logger_backend.rb +20 -0
- data/lib/statsd/instrument/backends/null_backend.rb +7 -0
- data/lib/statsd/instrument/backends/udp_backend.rb +106 -0
- data/lib/statsd/instrument/environment.rb +54 -0
- data/lib/statsd/instrument/helpers.rb +14 -0
- data/lib/statsd/instrument/matchers.rb +96 -0
- data/lib/statsd/instrument/metric.rb +117 -0
- data/lib/statsd/instrument/metric_expectation.rb +67 -0
- data/lib/statsd/instrument/railtie.rb +14 -0
- data/lib/statsd/instrument/version.rb +5 -0
- data/lib/statsd/instrument.rb +407 -0
- data/lib/statsd-instrument.rb +1 -0
- data/shipit.rubygems.yml +1 -0
- data/statsd-instrument.gemspec +27 -0
- data/test/assertions_test.rb +329 -0
- data/test/benchmark/tags.rb +34 -0
- data/test/capture_backend_test.rb +24 -0
- data/test/environment_test.rb +46 -0
- data/test/helpers_test.rb +24 -0
- data/test/integration_test.rb +20 -0
- data/test/logger_backend_test.rb +20 -0
- data/test/matchers_test.rb +102 -0
- data/test/metric_test.rb +45 -0
- data/test/statsd_instrumentation_test.rb +328 -0
- data/test/statsd_test.rb +136 -0
- data/test/test_helper.rb +10 -0
- data/test/udp_backend_test.rb +167 -0
- metadata +182 -0
@@ -0,0 +1,407 @@
|
|
1
|
+
require 'socket'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
# The StatsD module contains low-level metrics for collecting metrics and sending them to the backend.
|
5
|
+
#
|
6
|
+
# @!attribute backend
|
7
|
+
# The backend that is being used to emit the metrics.
|
8
|
+
# @return [StatsD::Instrument::Backend] the currently active backend. If there is no active backend
|
9
|
+
# yet, it will call {StatsD::Instrument::Environment#default_backend} to obtain a
|
10
|
+
# default backend for the environment.
|
11
|
+
# @see StatsD::Instrument::Environment#default_backend
|
12
|
+
#
|
13
|
+
# @!attribute prefix
|
14
|
+
# The prefix to apply to metric names. This can be useful to group all the metrics
|
15
|
+
# for an application in a shared StatsD server.
|
16
|
+
#
|
17
|
+
# When using a prefix a dot will be included automatically to separate the prefix
|
18
|
+
# from the metric name.
|
19
|
+
#
|
20
|
+
# @return [String, nil] The prefix, or <tt>nil</tt> when no prefix is used
|
21
|
+
# @see StatsD::Instrument::Metric#name
|
22
|
+
#
|
23
|
+
# @!attribute default_sample_rate
|
24
|
+
# The sample rate to use if the sample rate is unspecified for a metric call.
|
25
|
+
# @return [Float] Default is 1.0.
|
26
|
+
#
|
27
|
+
# @!attribute logger
|
28
|
+
# The logger to use in case of any errors. The logger is also used as default logger
|
29
|
+
# for the LoggerBackend (although this can be overwritten).
|
30
|
+
#
|
31
|
+
# @see StatsD::Instrument::Backends::LoggerBackend
|
32
|
+
# @return [Logger]
|
33
|
+
#
|
34
|
+
# @see StatsD::Instrument <tt>StatsD::Instrument</tt> contains module to instrument
|
35
|
+
# existing methods with StatsD metrics.
|
36
|
+
module StatsD
|
37
|
+
extend self
|
38
|
+
|
39
|
+
# The StatsD::Instrument module provides metaprogramming methods to instrument your methods with
|
40
|
+
# StatsD metrics. E.g., yopu can create counters on how often a method is called, how often it is
|
41
|
+
# successful, the duration of the methods call, etc.
|
42
|
+
module Instrument
|
43
|
+
# @private
|
44
|
+
def statsd_instrumentations
|
45
|
+
if defined?(@statsd_instrumentations)
|
46
|
+
@statsd_instrumentations
|
47
|
+
elsif respond_to?(:superclass) && superclass.respond_to?(:statsd_instrumentations)
|
48
|
+
superclass.statsd_instrumentations
|
49
|
+
else
|
50
|
+
@statsd_instrumentations = {}
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @private
|
55
|
+
def self.generate_metric_name(metric_name, callee, *args)
|
56
|
+
metric_name.respond_to?(:call) ? metric_name.call(callee, args).gsub('::', '.') : metric_name.gsub('::', '.')
|
57
|
+
end
|
58
|
+
|
59
|
+
if Process.respond_to?(:clock_gettime)
|
60
|
+
# @private
|
61
|
+
def self.duration
|
62
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
63
|
+
yield
|
64
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
65
|
+
end
|
66
|
+
else
|
67
|
+
# @private
|
68
|
+
def self.duration
|
69
|
+
start = Time.now
|
70
|
+
yield
|
71
|
+
Time.now - start
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Adds execution duration instrumentation to a method.
|
76
|
+
#
|
77
|
+
# @param method [Symbol] The name of the method to instrument.
|
78
|
+
# @param name [String, #call] The name of the metric to use. You can also pass in a
|
79
|
+
# callable to dynamically generate a metric name
|
80
|
+
# @param metric_options (see StatsD#measure)
|
81
|
+
# @return [void]
|
82
|
+
def statsd_measure(method, name, *metric_options)
|
83
|
+
add_to_method(method, name, :measure) do
|
84
|
+
define_method(method) do |*args, &block|
|
85
|
+
StatsD.measure(StatsD::Instrument.generate_metric_name(name, self, *args), nil, *metric_options) { super(*args, &block) }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Adds success and failure counter instrumentation to a method.
|
91
|
+
#
|
92
|
+
# A method call will be considered successful if it does not raise an exception, and the result is true-y.
|
93
|
+
# For successful calls, the metric <tt>[name].success</tt> will be incremented; for failed calls, the metric
|
94
|
+
# name is <tt>[name].failure</tt>.
|
95
|
+
#
|
96
|
+
# @param method (see #statsd_measure)
|
97
|
+
# @param name (see #statsd_measure)
|
98
|
+
# @param metric_options (see #statsd_measure)
|
99
|
+
# @yield You can pass a block to this method if you want to define yourself what is a successful call
|
100
|
+
# based on the return value of the method.
|
101
|
+
# @yieldparam result The return value of the instrumented method.
|
102
|
+
# @yieldreturn [Boolean] Return true iff the return value is consisered a success, false otherwise.
|
103
|
+
# @return [void]
|
104
|
+
# @see #statsd_count_if
|
105
|
+
def statsd_count_success(method, name, *metric_options)
|
106
|
+
add_to_method(method, name, :count_success) do
|
107
|
+
define_method(method) do |*args, &block|
|
108
|
+
begin
|
109
|
+
truthiness = result = super(*args, &block)
|
110
|
+
rescue
|
111
|
+
truthiness = false
|
112
|
+
raise
|
113
|
+
else
|
114
|
+
truthiness = (yield(result) rescue false) if block_given?
|
115
|
+
result
|
116
|
+
ensure
|
117
|
+
suffix = truthiness == false ? 'failure' : 'success'
|
118
|
+
StatsD.increment("#{StatsD::Instrument.generate_metric_name(name, self, *args)}.#{suffix}", 1, *metric_options)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
# Adds success and failure counter instrumentation to a method.
|
125
|
+
#
|
126
|
+
# A method call will be considered successful if it does not raise an exception, and the result is true-y.
|
127
|
+
# Only for successful calls, the metric will be icnremented
|
128
|
+
#
|
129
|
+
# @param method (see #statsd_measure)
|
130
|
+
# @param name (see #statsd_measure)
|
131
|
+
# @param metric_options (see #statsd_measure)
|
132
|
+
# @yield (see #statsd_count_success)
|
133
|
+
# @yieldparam result (see #statsd_count_success)
|
134
|
+
# @yieldreturn (see #statsd_count_success)
|
135
|
+
# @return [void]
|
136
|
+
# @see #statsd_count_success
|
137
|
+
def statsd_count_if(method, name, *metric_options)
|
138
|
+
add_to_method(method, name, :count_if) do
|
139
|
+
define_method(method) do |*args, &block|
|
140
|
+
begin
|
141
|
+
truthiness = result = super(*args, &block)
|
142
|
+
rescue
|
143
|
+
truthiness = false
|
144
|
+
raise
|
145
|
+
else
|
146
|
+
truthiness = (yield(result) rescue false) if block_given?
|
147
|
+
result
|
148
|
+
ensure
|
149
|
+
StatsD.increment(StatsD::Instrument.generate_metric_name(name, self, *args), *metric_options) if truthiness
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Adds counter instrumentation to a method.
|
156
|
+
#
|
157
|
+
# The metric will be incremented for every call of the instrumented method, no matter
|
158
|
+
# whether what the method returns, or whether it raises an exception.
|
159
|
+
#
|
160
|
+
# @param method (see #statsd_measure)
|
161
|
+
# @param name (see #statsd_measure)
|
162
|
+
# @param metric_options (see #statsd_measure)
|
163
|
+
# @return [void]
|
164
|
+
def statsd_count(method, name, *metric_options)
|
165
|
+
add_to_method(method, name, :count) do
|
166
|
+
define_method(method) do |*args, &block|
|
167
|
+
StatsD.increment(StatsD::Instrument.generate_metric_name(name, self, *args), 1, *metric_options)
|
168
|
+
super(*args, &block)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
# Removes StatsD counter instrumentation from a method
|
174
|
+
# @param method [Symbol] The method to remove instrumentation from.
|
175
|
+
# @param name [String] The name of the metric that was used.
|
176
|
+
# @return [void]
|
177
|
+
# @see #statsd_count
|
178
|
+
def statsd_remove_count(method, name)
|
179
|
+
remove_from_method(method, name, :count)
|
180
|
+
end
|
181
|
+
|
182
|
+
# Removes StatsD conditional counter instrumentation from a method
|
183
|
+
# @param method (see #statsd_remove_count)
|
184
|
+
# @param name (see #statsd_remove_count)
|
185
|
+
# @return [void]
|
186
|
+
# @see #statsd_count_if
|
187
|
+
def statsd_remove_count_if(method, name)
|
188
|
+
remove_from_method(method, name, :count_if)
|
189
|
+
end
|
190
|
+
|
191
|
+
# Removes StatsD success counter instrumentation from a method
|
192
|
+
# @param method (see #statsd_remove_count)
|
193
|
+
# @param name (see #statsd_remove_count)
|
194
|
+
# @return [void]
|
195
|
+
# @see #statsd_count_success
|
196
|
+
def statsd_remove_count_success(method, name)
|
197
|
+
remove_from_method(method, name, :count_success)
|
198
|
+
end
|
199
|
+
|
200
|
+
# Removes StatsD measure instrumentation from a method
|
201
|
+
# @param method (see #statsd_remove_count)
|
202
|
+
# @param name (see #statsd_remove_count)
|
203
|
+
# @return [void]
|
204
|
+
# @see #statsd_measure
|
205
|
+
def statsd_remove_measure(method, name)
|
206
|
+
remove_from_method(method, name, :measure)
|
207
|
+
end
|
208
|
+
|
209
|
+
private
|
210
|
+
|
211
|
+
def statsd_instrumentation_for(method, name, action)
|
212
|
+
unless statsd_instrumentations.key?([method, name, action])
|
213
|
+
mod = Module.new do
|
214
|
+
define_singleton_method(:inspect) do
|
215
|
+
"StatsD_Instrument_#{method}_for_#{action}_with_#{name}"
|
216
|
+
end
|
217
|
+
end
|
218
|
+
@statsd_instrumentations = statsd_instrumentations.merge([method, name, action] => mod)
|
219
|
+
end
|
220
|
+
@statsd_instrumentations[[method, name, action]]
|
221
|
+
end
|
222
|
+
|
223
|
+
def add_to_method(method, name, action, &block)
|
224
|
+
instrumentation_module = statsd_instrumentation_for(method, name, action)
|
225
|
+
|
226
|
+
raise ArgumentError, "already instrumented #{method} for #{self.name}" if instrumentation_module.method_defined?(method)
|
227
|
+
raise ArgumentError, "could not find method #{method} for #{self.name}" unless method_defined?(method) || private_method_defined?(method)
|
228
|
+
|
229
|
+
method_scope = method_visibility(method)
|
230
|
+
|
231
|
+
instrumentation_module.module_eval(&block)
|
232
|
+
instrumentation_module.send(method_scope, method)
|
233
|
+
prepend(instrumentation_module) unless self < instrumentation_module
|
234
|
+
end
|
235
|
+
|
236
|
+
def remove_from_method(method, name, action)
|
237
|
+
statsd_instrumentation_for(method, name, action).send(:remove_method, method)
|
238
|
+
end
|
239
|
+
|
240
|
+
def method_visibility(method)
|
241
|
+
case
|
242
|
+
when private_method_defined?(method)
|
243
|
+
:private
|
244
|
+
when protected_method_defined?(method)
|
245
|
+
:protected
|
246
|
+
else
|
247
|
+
:public
|
248
|
+
end
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
attr_accessor :logger, :default_sample_rate, :prefix
|
253
|
+
attr_writer :backend
|
254
|
+
|
255
|
+
def backend
|
256
|
+
@backend ||= StatsD::Instrument::Environment.default_backend
|
257
|
+
end
|
258
|
+
|
259
|
+
# Emits a duration metric.
|
260
|
+
#
|
261
|
+
# @overload measure(key, value, metric_options = {})
|
262
|
+
# Emits a measure metric, by providing a duration in milliseconds.
|
263
|
+
# @param key [String] The name of the metric.
|
264
|
+
# @param value [Float] The measured duration in milliseconds
|
265
|
+
# @param metric_options [Hash] Options for the metric
|
266
|
+
# @return [StatsD::Instrument::Metric] The metric that was sent to the backend.
|
267
|
+
#
|
268
|
+
# @overload measure(key, metric_options = {}, &block)
|
269
|
+
# Emits a measure metric, after measuring the execution duration of the
|
270
|
+
# block passed to this method.
|
271
|
+
# @param key [String] The name of the metric.
|
272
|
+
# @param metric_options [Hash] Options for the metric
|
273
|
+
# @yield The method will yield the block that was passed to this emthod to measure its duration.
|
274
|
+
# @return The value that was returns by the block passed to this method.
|
275
|
+
#
|
276
|
+
# @example
|
277
|
+
# http_response = StatsD.measure('HTTP.call.duration') do
|
278
|
+
# HTTP.get(url)
|
279
|
+
# end
|
280
|
+
def measure(key, value = nil, *metric_options, &block)
|
281
|
+
if value.is_a?(Hash) && metric_options.empty?
|
282
|
+
metric_options = [value]
|
283
|
+
value = value.fetch(:value, nil)
|
284
|
+
end
|
285
|
+
|
286
|
+
result = nil
|
287
|
+
value = 1000 * StatsD::Instrument.duration { result = block.call } if block_given?
|
288
|
+
metric = collect_metric(hash_argument(metric_options).merge(type: :ms, name: key, value: value))
|
289
|
+
result = metric unless block_given?
|
290
|
+
result
|
291
|
+
end
|
292
|
+
|
293
|
+
# Emits a counter metric.
|
294
|
+
# @param key [String] The name of the metric.
|
295
|
+
# @param value [Integer] The value to increment the counter by.
|
296
|
+
#
|
297
|
+
# You should not compensate for the sample rate using the counter increment. E.g., if
|
298
|
+
# your sample rate is 0.01, you should <b>not</b> use 100 as increment to compensate for it.
|
299
|
+
# The sample rate is part of the packet that is being sent to the server, and the server
|
300
|
+
# should know how to handle it.
|
301
|
+
#
|
302
|
+
# @param metric_options [Hash] (default: {}) Metric options
|
303
|
+
# @return (see #collect_metric)
|
304
|
+
def increment(key, value = 1, *metric_options)
|
305
|
+
if value.is_a?(Hash) && metric_options.empty?
|
306
|
+
metric_options = [value]
|
307
|
+
value = value.fetch(:value, 1)
|
308
|
+
end
|
309
|
+
|
310
|
+
collect_metric(hash_argument(metric_options).merge(type: :c, name: key, value: value))
|
311
|
+
end
|
312
|
+
|
313
|
+
# Emits a gauge metric.
|
314
|
+
# @param key [String] The name of the metric.
|
315
|
+
# @param value [Numeric] The current value to record.
|
316
|
+
# @param metric_options [Hash] (default: {}) Metric options
|
317
|
+
# @return (see #collect_metric)
|
318
|
+
def gauge(key, value, *metric_options)
|
319
|
+
if value.is_a?(Hash) && metric_options.empty?
|
320
|
+
metric_options = [value]
|
321
|
+
value = value.fetch(:value, nil)
|
322
|
+
end
|
323
|
+
|
324
|
+
collect_metric(hash_argument(metric_options).merge(type: :g, name: key, value: value))
|
325
|
+
end
|
326
|
+
|
327
|
+
# Emits a histogram metric.
|
328
|
+
# @param key [String] The name of the metric.
|
329
|
+
# @param value [Numeric] The value to record.
|
330
|
+
# @param metric_options [Hash] (default: {}) Metric options
|
331
|
+
# @return (see #collect_metric)
|
332
|
+
# @note Supported by the datadog implementation only.
|
333
|
+
def histogram(key, value, *metric_options)
|
334
|
+
if value.is_a?(Hash) && metric_options.empty?
|
335
|
+
metric_options = [value]
|
336
|
+
value = value.fetch(:value, nil)
|
337
|
+
end
|
338
|
+
|
339
|
+
collect_metric(hash_argument(metric_options).merge(type: :h, name: key, value: value))
|
340
|
+
end
|
341
|
+
|
342
|
+
# Emits a key/value metric.
|
343
|
+
# @param key [String] The name of the metric.
|
344
|
+
# @param value [Numeric] The value to record.
|
345
|
+
# @param metric_options [Hash] (default: {}) Metric options
|
346
|
+
# @return (see #collect_metric)
|
347
|
+
# @note Supported by the statsite implementation only.
|
348
|
+
def key_value(key, value, *metric_options)
|
349
|
+
if value.is_a?(Hash) && metric_options.empty?
|
350
|
+
metric_options = [value]
|
351
|
+
value = value.fetch(:value, nil)
|
352
|
+
end
|
353
|
+
|
354
|
+
collect_metric(hash_argument(metric_options).merge(type: :kv, name: key, value: value))
|
355
|
+
end
|
356
|
+
|
357
|
+
# Emits a set metric.
|
358
|
+
# @param key [String] The name of the metric.
|
359
|
+
# @param value [Numeric] The value to record.
|
360
|
+
# @param metric_options [Hash] (default: {}) Metric options
|
361
|
+
# @return (see #collect_metric)
|
362
|
+
# @note Supported by the datadog implementation only.
|
363
|
+
def set(key, value, *metric_options)
|
364
|
+
if value.is_a?(Hash) && metric_options.empty?
|
365
|
+
metric_options = [value]
|
366
|
+
value = value.fetch(:value, nil)
|
367
|
+
end
|
368
|
+
|
369
|
+
collect_metric(hash_argument(metric_options).merge(type: :s, name: key, value: value))
|
370
|
+
end
|
371
|
+
|
372
|
+
private
|
373
|
+
|
374
|
+
# Converts old-style ordered arguments in an argument hash for backwards compatibility.
|
375
|
+
# @param args [Array] The list of non-required arguments.
|
376
|
+
# @return [Hash] The hash of optional arguments.
|
377
|
+
def hash_argument(args)
|
378
|
+
return {} if args.length == 0
|
379
|
+
return args.first if args.length == 1 && args.first.is_a?(Hash)
|
380
|
+
|
381
|
+
order = [:sample_rate, :tags]
|
382
|
+
hash = {}
|
383
|
+
args.each_with_index do |value, index|
|
384
|
+
hash[order[index]] = value
|
385
|
+
end
|
386
|
+
|
387
|
+
return hash
|
388
|
+
end
|
389
|
+
|
390
|
+
# Instantiates a metric, and sends it to the backend for further processing.
|
391
|
+
# @param options (see StatsD::Instrument::Metric#initialize)
|
392
|
+
# @return [StatsD::Instrument::Metric] The meric that was sent to the backend.
|
393
|
+
def collect_metric(options)
|
394
|
+
backend.collect_metric(metric = StatsD::Instrument::Metric.new(options))
|
395
|
+
metric
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
require 'statsd/instrument/version'
|
400
|
+
require 'statsd/instrument/metric'
|
401
|
+
require 'statsd/instrument/backend'
|
402
|
+
require 'statsd/instrument/environment'
|
403
|
+
require 'statsd/instrument/helpers'
|
404
|
+
require 'statsd/instrument/assertions'
|
405
|
+
require 'statsd/instrument/metric_expectation'
|
406
|
+
require 'statsd/instrument/matchers' if defined?(::RSpec)
|
407
|
+
require 'statsd/instrument/railtie' if defined?(Rails)
|
@@ -0,0 +1 @@
|
|
1
|
+
require 'statsd/instrument'
|
data/shipit.rubygems.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
# using the default shipit config
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'statsd/instrument/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "qubole-statsd-instrument"
|
8
|
+
spec.version = StatsD::Instrument::VERSION
|
9
|
+
spec.authors = ["Jesse Storimer", "Tobias Lutke", "Willem van Bergen"]
|
10
|
+
spec.email = ["jesse@shopify.com"]
|
11
|
+
spec.homepage = "https://github.com/Shopify/statsd-instrument"
|
12
|
+
spec.summary = %q{A StatsD client for Ruby apps}
|
13
|
+
spec.description = %q{A StatsD client for Ruby apps. Provides metaprogramming methods to inject StatsD instrumentation into your code.}
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency 'rake'
|
22
|
+
spec.add_development_dependency 'minitest'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_development_dependency 'mocha'
|
25
|
+
spec.add_development_dependency 'yard'
|
26
|
+
spec.add_development_dependency 'benchmark-ips'
|
27
|
+
end
|