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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +7 -0
  3. data/.travis.yml +12 -0
  4. data/CHANGELOG.md +89 -0
  5. data/CONTRIBUTING.md +34 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE +20 -0
  8. data/README.md +319 -0
  9. data/Rakefile +10 -0
  10. data/lib/statsd/instrument/assertions.rb +88 -0
  11. data/lib/statsd/instrument/backend.rb +17 -0
  12. data/lib/statsd/instrument/backends/capture_backend.rb +29 -0
  13. data/lib/statsd/instrument/backends/logger_backend.rb +20 -0
  14. data/lib/statsd/instrument/backends/null_backend.rb +7 -0
  15. data/lib/statsd/instrument/backends/udp_backend.rb +106 -0
  16. data/lib/statsd/instrument/environment.rb +54 -0
  17. data/lib/statsd/instrument/helpers.rb +14 -0
  18. data/lib/statsd/instrument/matchers.rb +96 -0
  19. data/lib/statsd/instrument/metric.rb +117 -0
  20. data/lib/statsd/instrument/metric_expectation.rb +67 -0
  21. data/lib/statsd/instrument/railtie.rb +14 -0
  22. data/lib/statsd/instrument/version.rb +5 -0
  23. data/lib/statsd/instrument.rb +407 -0
  24. data/lib/statsd-instrument.rb +1 -0
  25. data/shipit.rubygems.yml +1 -0
  26. data/statsd-instrument.gemspec +27 -0
  27. data/test/assertions_test.rb +329 -0
  28. data/test/benchmark/tags.rb +34 -0
  29. data/test/capture_backend_test.rb +24 -0
  30. data/test/environment_test.rb +46 -0
  31. data/test/helpers_test.rb +24 -0
  32. data/test/integration_test.rb +20 -0
  33. data/test/logger_backend_test.rb +20 -0
  34. data/test/matchers_test.rb +102 -0
  35. data/test/metric_test.rb +45 -0
  36. data/test/statsd_instrumentation_test.rb +328 -0
  37. data/test/statsd_test.rb +136 -0
  38. data/test/test_helper.rb +10 -0
  39. data/test/udp_backend_test.rb +167 -0
  40. 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'
@@ -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