statsd-instrument 2.3.5 → 2.4.0

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/ci.yml +31 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
  6. data/.rubocop.yml +21 -0
  7. data/CHANGELOG.md +9 -0
  8. data/CONTRIBUTING.md +25 -5
  9. data/Gemfile +2 -0
  10. data/Rakefile +3 -1
  11. data/lib/statsd-instrument.rb +2 -0
  12. data/lib/statsd/instrument.rb +51 -18
  13. data/lib/statsd/instrument/assertions.rb +24 -18
  14. data/lib/statsd/instrument/backend.rb +3 -2
  15. data/lib/statsd/instrument/backends/capture_backend.rb +2 -1
  16. data/lib/statsd/instrument/backends/logger_backend.rb +3 -3
  17. data/lib/statsd/instrument/backends/null_backend.rb +2 -0
  18. data/lib/statsd/instrument/backends/udp_backend.rb +18 -15
  19. data/lib/statsd/instrument/environment.rb +2 -0
  20. data/lib/statsd/instrument/helpers.rb +6 -2
  21. data/lib/statsd/instrument/matchers.rb +14 -11
  22. data/lib/statsd/instrument/metric.rb +34 -21
  23. data/lib/statsd/instrument/metric_expectation.rb +32 -18
  24. data/lib/statsd/instrument/railtie.rb +2 -1
  25. data/lib/statsd/instrument/version.rb +3 -1
  26. data/statsd-instrument.gemspec +13 -10
  27. data/test/assertions_test.rb +15 -4
  28. data/test/benchmark/default_tags.rb +47 -0
  29. data/test/benchmark/metrics.rb +9 -8
  30. data/test/benchmark/tags.rb +5 -3
  31. data/test/capture_backend_test.rb +4 -2
  32. data/test/environment_test.rb +2 -1
  33. data/test/helpers_test.rb +2 -1
  34. data/test/integration_test.rb +27 -7
  35. data/test/logger_backend_test.rb +10 -8
  36. data/test/matchers_test.rb +34 -20
  37. data/test/metric_test.rb +15 -4
  38. data/test/statsd_instrumentation_test.rb +7 -7
  39. data/test/statsd_test.rb +24 -15
  40. data/test/test_helper.rb +2 -0
  41. data/test/udp_backend_test.rb +3 -26
  42. metadata +22 -3
  43. data/.travis.yml +0 -12
data/.rubocop.yml ADDED
@@ -0,0 +1,21 @@
1
+ inherit_from:
2
+ - https://shopify.github.io/ruby-style-guide/rubocop.yml
3
+
4
+ AllCops:
5
+ TargetRubyVersion: 2.3
6
+ UseCache: true
7
+ CacheRootDirectory: tmp/rubocop
8
+ Exclude:
9
+ - statsd-instrument.gemspec
10
+
11
+ Naming/FileName:
12
+ Enabled: true
13
+ Exclude:
14
+ - lib/statsd-instrument.rb
15
+
16
+ Style/ClassAndModuleChildren:
17
+ Enabled: false # TODO: enable later
18
+
19
+
20
+ Style/MethodCallWithArgsParentheses:
21
+ Enabled: false # TODO: enable later
data/CHANGELOG.md CHANGED
@@ -5,6 +5,15 @@ please at an entry to the "unreleased changes" section below.
5
5
 
6
6
  ### Unreleased changes
7
7
 
8
+ ## Version 2.4.0
9
+
10
+ - Add `StatsD.default_tags` to specify tags that should be included in all metrics. (#159)
11
+ - Improve assertion message when assertying metrics whose tags do not match. (#100)
12
+ - Enforce the Shopify Ruby style guide. (#164)
13
+ - Migrate CI to Github actions. (#158)
14
+ - Make the library frozen string literal-compatible. (#161, #163)
15
+ - Fix all Ruby warnings. (#162)
16
+
8
17
  ## Version 2.3.5
9
18
 
10
19
  - Re-add `StatsD::Instrument.duration`, which was accidentally removed since verison 2.5.3 (#157)
data/CONTRIBUTING.md CHANGED
@@ -17,21 +17,41 @@ When reporting issues, please include the following information:
17
17
  - The statsd-instrument version. **Note:** only the latest version is supported.
18
18
  - The StatsD backend you are using.
19
19
 
20
- ## Pull request
20
+ ## Opening pull requests
21
21
 
22
22
  1. Fork the repository, and create a branch.
23
23
  2. Implement the feature or bugfix, and add tests that cover the changed functionality.
24
- 3. Create a pull request. Make sure that you get Travis CI passes.
24
+ 3. Create a pull request. Make sure that you get a green CI status on your commit.
25
25
 
26
26
  Some notes:
27
27
 
28
- - Make sure to follow to coding style.
28
+ - Make sure to follow to coding style. This is enforced by Rubocop
29
29
  - Make sure your changes are properly documented using [yardoc syntax](http://www.rubydoc.info/gems/yard/file/docs/GettingStarted.md).
30
30
  - Add an entry to the "unreleased changes" section of [CHANGELOG.md](./CHANGELOG.md).
31
31
  - **Do not** update `StatsD::Instrument::VERSION`. This will be done during the release prodecure.
32
32
 
33
- > **Important:** if you change anything in the hot code path (sending a StatsD metric), please
34
- > include benchmarks to show the performance impact of your changes.
33
+ ### On perfomance & benchmarking
34
+
35
+ This gem is used in production at Shopify, and is used to instrument some of
36
+ our hottest code paths. This means that we are very careful about not
37
+ introducing performance regressions in this library.
38
+
39
+ **Important:** Whenever you make changes to the metric emission code path in this library,
40
+ you **must** include benchmark results to show the impact of your changes.
41
+
42
+ The `test/benchmark/` folder contains some example benchmark script that you
43
+ can use, or can serve as a starting point. Please run your benchmark on your
44
+ pull request revision, as well as the latest revision on `master`.
45
+
46
+ ### On backwards compatibility
47
+
48
+ Shopify's codebases are heavily instrumented using this library. As a result, we cannot
49
+ accept changes that are backwards incompatible:
50
+
51
+ - Changes that will require us to update our codebases.
52
+ - Changes that will cause metrics emitted by this library to change in form or shape.
53
+
54
+ This means that we may not be able to accept fixes for what you consider a bug.
35
55
 
36
56
  ## Release procedure
37
57
 
data/Gemfile CHANGED
@@ -1,2 +1,4 @@
1
+ # frozen_string_literal: true
2
+
1
3
  source "https://rubygems.org"
2
4
  gemspec
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'bundler/gem_tasks'
2
4
  require 'rake/testtask'
3
5
 
@@ -7,4 +9,4 @@ Rake::TestTask.new('test') do |t|
7
9
  t.test_files = FileList['test/*.rb']
8
10
  end
9
11
 
10
- task :default => :test
12
+ task default: :test
@@ -1 +1,3 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'statsd/instrument'
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'socket'
2
4
  require 'logger'
3
5
 
@@ -28,6 +30,10 @@ require 'logger'
28
30
  # The logger to use in case of any errors. The logger is also used as default logger
29
31
  # for the LoggerBackend (although this can be overwritten).
30
32
  #
33
+ # @!attribute default_tags
34
+ # The tags to apply to all metrics.
35
+ # @return [Array<String>, Hash<String, String>, nil] The default tags, or <tt>nil</tt> when no default tags is used
36
+ #
31
37
  # @see StatsD::Instrument::Backends::LoggerBackend
32
38
  # @return [Logger]
33
39
  #
@@ -88,7 +94,8 @@ module StatsD
88
94
  def statsd_measure(method, name, *metric_options)
89
95
  add_to_method(method, name, :measure) do
90
96
  define_method(method) do |*args, &block|
91
- StatsD.measure(StatsD::Instrument.generate_metric_name(name, self, *args), *metric_options) { super(*args, &block) }
97
+ metric_name = StatsD::Instrument.generate_metric_name(name, self, *args)
98
+ StatsD.measure(metric_name, *metric_options) { super(*args, &block) }
92
99
  end
93
100
  end
94
101
  end
@@ -104,7 +111,8 @@ module StatsD
104
111
  def statsd_distribution(method, name, *metric_options)
105
112
  add_to_method(method, name, :distribution) do
106
113
  define_method(method) do |*args, &block|
107
- StatsD.distribution(StatsD::Instrument.generate_metric_name(name, self, *args), *metric_options) { super(*args, &block) }
114
+ metric_name = StatsD::Instrument.generate_metric_name(name, self, *args)
115
+ StatsD.distribution(metric_name, *metric_options) { super(*args, &block) }
108
116
  end
109
117
  end
110
118
  end
@@ -133,20 +141,27 @@ module StatsD
133
141
  truthiness = false
134
142
  raise
135
143
  else
136
- truthiness = (yield(result) rescue false) if block_given?
144
+ if block_given?
145
+ begin
146
+ truthiness = yield(result)
147
+ rescue
148
+ truthiness = false
149
+ end
150
+ end
137
151
  result
138
152
  ensure
139
153
  suffix = truthiness == false ? 'failure' : 'success'
140
- StatsD.increment("#{StatsD::Instrument.generate_metric_name(name, self, *args)}.#{suffix}", 1, *metric_options)
154
+ metric_name = "#{StatsD::Instrument.generate_metric_name(name, self, *args)}.#{suffix}"
155
+ StatsD.increment(metric_name, 1, *metric_options)
141
156
  end
142
157
  end
143
158
  end
144
159
  end
145
160
 
146
- # Adds success and failure counter instrumentation to a method.
161
+ # Adds success counter instrumentation to a method.
147
162
  #
148
163
  # A method call will be considered successful if it does not raise an exception, and the result is true-y.
149
- # Only for successful calls, the metric will be icnremented
164
+ # Only for successful calls, the metric will be incremented.
150
165
  #
151
166
  # @param method (see #statsd_measure)
152
167
  # @param name (see #statsd_measure)
@@ -165,10 +180,19 @@ module StatsD
165
180
  truthiness = false
166
181
  raise
167
182
  else
168
- truthiness = (yield(result) rescue false) if block_given?
183
+ if block_given?
184
+ begin
185
+ truthiness = yield(result)
186
+ rescue
187
+ truthiness = false
188
+ end
189
+ end
169
190
  result
170
191
  ensure
171
- StatsD.increment(StatsD::Instrument.generate_metric_name(name, self, *args), *metric_options) if truthiness
192
+ if truthiness
193
+ metric_name = StatsD::Instrument.generate_metric_name(name, self, *args)
194
+ StatsD.increment(metric_name, *metric_options)
195
+ end
172
196
  end
173
197
  end
174
198
  end
@@ -186,7 +210,8 @@ module StatsD
186
210
  def statsd_count(method, name, *metric_options)
187
211
  add_to_method(method, name, :count) do
188
212
  define_method(method) do |*args, &block|
189
- StatsD.increment(StatsD::Instrument.generate_metric_name(name, self, *args), 1, *metric_options)
213
+ metric_name = StatsD::Instrument.generate_metric_name(name, self, *args)
214
+ StatsD.increment(metric_name, 1, *metric_options)
190
215
  super(*args, &block)
191
216
  end
192
217
  end
@@ -254,8 +279,13 @@ module StatsD
254
279
  def add_to_method(method, name, action, &block)
255
280
  instrumentation_module = statsd_instrumentation_for(method, name, action)
256
281
 
257
- raise ArgumentError, "already instrumented #{method} for #{self.name}" if instrumentation_module.method_defined?(method)
258
- raise ArgumentError, "could not find method #{method} for #{self.name}" unless method_defined?(method) || private_method_defined?(method)
282
+ if instrumentation_module.method_defined?(method)
283
+ raise ArgumentError, "Already instrumented #{method} for #{self.name}"
284
+ end
285
+
286
+ unless method_defined?(method) || private_method_defined?(method)
287
+ raise ArgumentError, "could not find method #{method} for #{self.name}"
288
+ end
259
289
 
260
290
  method_scope = method_visibility(method)
261
291
 
@@ -269,10 +299,9 @@ module StatsD
269
299
  end
270
300
 
271
301
  def method_visibility(method)
272
- case
273
- when private_method_defined?(method)
302
+ if private_method_defined?(method)
274
303
  :private
275
- when protected_method_defined?(method)
304
+ elsif protected_method_defined?(method)
276
305
  :protected
277
306
  else
278
307
  :public
@@ -282,6 +311,11 @@ module StatsD
282
311
 
283
312
  attr_accessor :logger, :default_sample_rate, :prefix
284
313
  attr_writer :backend
314
+ attr_reader :default_tags
315
+
316
+ def default_tags=(tags)
317
+ @default_tags = StatsD::Instrument::Metric.normalize_tags(tags)
318
+ end
285
319
 
286
320
  def backend
287
321
  @backend ||= StatsD::Instrument::Environment.default_backend
@@ -382,7 +416,7 @@ module StatsD
382
416
  # http_response = StatsD.distribution('HTTP.call.duration') do
383
417
  # HTTP.get(url)
384
418
  # end
385
- def distribution(key, value=nil, *metric_options, &block)
419
+ def distribution(key, value = nil, *metric_options, &block)
386
420
  value, metric_options = parse_options(value, metric_options)
387
421
 
388
422
  return collect_metric(:d, key, value, metric_options) unless block_given?
@@ -442,7 +476,7 @@ module StatsD
442
476
  # @param args [Array] The list of non-required arguments.
443
477
  # @return [Hash] The hash of optional arguments.
444
478
  def hash_argument(args)
445
- return {} if args.length == 0
479
+ return {} if args.empty?
446
480
  return args.first if args.length == 1 && args.first.is_a?(Hash)
447
481
 
448
482
  order = [:sample_rate, :tags]
@@ -450,8 +484,7 @@ module StatsD
450
484
  args.each_with_index do |value, index|
451
485
  hash[order[index]] = value
452
486
  end
453
-
454
- return hash
487
+ hash
455
488
  end
456
489
 
457
490
  def parse_options(value, metric_options)
@@ -1,10 +1,12 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StatsD::Instrument::Assertions
2
4
  include StatsD::Instrument::Helpers
3
5
 
4
6
  def assert_no_statsd_calls(metric_name = nil, &block)
5
7
  metrics = capture_statsd_calls(&block)
6
8
  metrics.select! { |m| m.name == metric_name } if metric_name
7
- assert metrics.empty?, "No StatsD calls for metric #{metrics.map(&:name).join(', ')} expected."
9
+ assert(metrics.empty?, "No StatsD calls for metric #{metrics.map(&:name).join(', ')} expected.")
8
10
  end
9
11
 
10
12
  def assert_statsd_increment(metric_name, options = {}, &block)
@@ -48,32 +50,36 @@ module StatsD::Instrument::Assertions
48
50
  expected_metric_times = expected_metric.times
49
51
  expected_metric_times_remaining = expected_metric.times
50
52
  filtered_metrics = metrics.select { |m| m.type == expected_metric.type && m.name == expected_metric.name }
51
- assert filtered_metrics.length > 0,
52
- "No StatsD calls for metric #{expected_metric.name} of type #{expected_metric.type} were made."
53
+ refute(filtered_metrics.empty?,
54
+ "No StatsD calls for metric #{expected_metric.name} of type #{expected_metric.type} were made.")
53
55
 
54
56
  filtered_metrics.each do |metric|
57
+ next unless expected_metric.matches(metric)
58
+
55
59
  assert within_numeric_range?(metric.sample_rate),
56
60
  "Unexpected sample rate type for metric #{metric.name}, must be numeric"
57
- if expected_metric.matches(metric)
58
- assert expected_metric_times_remaining > 0,
59
- "Unexpected StatsD call; number of times this metric was expected exceeded: #{expected_metric.inspect}"
60
- expected_metric_times_remaining -= 1
61
- metrics.delete(metric)
62
- if expected_metric_times_remaining == 0
63
- matched_expected_metrics << expected_metric
64
- end
61
+
62
+ assert(expected_metric_times_remaining > 0,
63
+ "Unexpected StatsD call; number of times this metric was expected exceeded: #{expected_metric.inspect}")
64
+
65
+ expected_metric_times_remaining -= 1
66
+ metrics.delete(metric)
67
+ if expected_metric_times_remaining == 0
68
+ matched_expected_metrics << expected_metric
65
69
  end
66
70
  end
67
71
 
68
- assert expected_metric_times_remaining == 0,
69
- "Metric expected #{expected_metric_times} times but seen"\
70
- " #{expected_metric_times-expected_metric_times_remaining}"\
71
- " times: #{expected_metric.inspect}"
72
+ msg = +"Metric expected #{expected_metric_times} times but seen " \
73
+ "#{expected_metric_times - expected_metric_times_remaining} " \
74
+ "times: #{expected_metric.inspect}."
75
+ msg << "\nCaptured metrics with the same key: #{filtered_metrics}" if filtered_metrics.any?
76
+
77
+ assert(expected_metric_times_remaining == 0, msg)
72
78
  end
73
79
  expected_metrics -= matched_expected_metrics
74
80
 
75
- assert expected_metrics.empty?,
76
- "Unexpected StatsD calls; the following metric expectations were not satisfied: #{expected_metrics.inspect}"
81
+ assert(expected_metrics.empty?,
82
+ "Unexpected StatsD calls; the following metric expectations were not satisfied: #{expected_metrics.inspect}")
77
83
  end
78
84
 
79
85
  private
@@ -87,6 +93,6 @@ module StatsD::Instrument::Assertions
87
93
  end
88
94
 
89
95
  def within_numeric_range?(object)
90
- object.kind_of?(Numeric) && (0.0..1.0).cover?(object)
96
+ object.is_a?(Numeric) && (0.0..1.0).cover?(object)
91
97
  end
92
98
  end
@@ -1,12 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This abstract class specifies the interface a backend implementation should conform to.
2
4
  # @abstract
3
5
  class StatsD::Instrument::Backend
4
-
5
6
  # Collects a metric.
6
7
  #
7
8
  # @param metric [StatsD::Instrument::Metric] The metric to collect
8
9
  # @return [void]
9
- def collect_metric(metric)
10
+ def collect_metric(_metric)
10
11
  raise NotImplementedError, "Use a concerete backend implementation"
11
12
  end
12
13
  end
@@ -1,5 +1,6 @@
1
- module StatsD::Instrument::Backends
1
+ # frozen_string_literal: true
2
2
 
3
+ module StatsD::Instrument::Backends
3
4
  # The capture backend is used to capture the metrics that are collected, so you can
4
5
  # run assertions on them.
5
6
  #
@@ -1,10 +1,10 @@
1
- module StatsD::Instrument::Backends
1
+ # frozen_string_literal: true
2
2
 
3
+ module StatsD::Instrument::Backends
3
4
  # The logger backend simply logs every metric to a logger
4
5
  # @!attribute logger
5
6
  # @return [Logger]
6
7
  class LoggerBackend < StatsD::Instrument::Backend
7
-
8
8
  attr_accessor :logger
9
9
 
10
10
  def initialize(logger)
@@ -14,7 +14,7 @@ module StatsD::Instrument::Backends
14
14
  # @param metric [StatsD::Instrument::Metric]
15
15
  # @return [void]
16
16
  def collect_metric(metric)
17
- logger.info "[StatsD] #{metric}"
17
+ logger.info("[StatsD] #{metric}")
18
18
  end
19
19
  end
20
20
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StatsD::Instrument::Backends
2
4
  # The null backend does nothing when receiving a metric, effectively disabling the gem completely.
3
5
  class NullBackend < StatsD::Instrument::Backend
@@ -1,8 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'monitor'
2
4
 
3
5
  module StatsD::Instrument::Backends
4
6
  class UDPBackend < StatsD::Instrument::Backend
5
-
6
7
  BASE_SUPPORTED_METRIC_TYPES = { c: true, ms: true, g: true, s: true }
7
8
 
8
9
  class DogStatsDProtocol
@@ -24,7 +25,7 @@ module StatsD::Instrument::Backends
24
25
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES.merge(h: true, _e: true, _sc: true, d: true)
25
26
 
26
27
  def generate_packet(metric)
27
- packet = ""
28
+ packet = +""
28
29
 
29
30
  if metric.type == :_e
30
31
  escaped_title = metric.name.gsub("\n", "\\n")
@@ -41,6 +42,7 @@ module StatsD::Instrument::Backends
41
42
 
42
43
  packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
43
44
  packet << "|##{metric.tags.join(',')}" if metric.tags
45
+
44
46
  packet
45
47
  end
46
48
 
@@ -57,7 +59,7 @@ module StatsD::Instrument::Backends
57
59
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES.merge(kv: true)
58
60
 
59
61
  def generate_packet(metric)
60
- packet = "#{metric.name}:#{metric.value}|#{metric.type}"
62
+ packet = +"#{metric.name}:#{metric.value}|#{metric.type}"
61
63
  packet << "|@#{metric.sample_rate}" unless metric.sample_rate == 1
62
64
  packet << "\n"
63
65
  packet
@@ -68,7 +70,7 @@ module StatsD::Instrument::Backends
68
70
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES
69
71
 
70
72
  def generate_packet(metric)
71
- packet = "#{metric.name}:#{metric.value}|#{metric.type}"
73
+ packet = +"#{metric.name}:#{metric.value}|#{metric.type}"
72
74
  packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
73
75
  packet
74
76
  end
@@ -88,19 +90,20 @@ module StatsD::Instrument::Backends
88
90
 
89
91
  def implementation=(value)
90
92
  @packet_factory = case value
91
- when :datadog
92
- DogStatsDProtocol.new
93
- when :statsite
94
- StatsiteStatsDProtocol.new
95
- else
96
- StatsDProtocol.new
97
- end
93
+ when :datadog
94
+ DogStatsDProtocol.new
95
+ when :statsite
96
+ StatsiteStatsDProtocol.new
97
+ else
98
+ StatsDProtocol.new
99
+ end
98
100
  @implementation = value
99
101
  end
100
102
 
101
103
  def collect_metric(metric)
102
104
  unless @packet_factory.class::SUPPORTED_METRIC_TYPES[metric.type]
103
- StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} not supported on #{implementation} implementation.")
105
+ StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} is not supported " \
106
+ "on #{implementation} implementation.")
104
107
  return false
105
108
  end
106
109
 
@@ -139,13 +142,13 @@ module StatsD::Instrument::Backends
139
142
  synchronize do
140
143
  socket.send(command, 0) > 0
141
144
  end
142
- rescue ThreadError => e
145
+ rescue ThreadError
143
146
  # In cases where a TERM or KILL signal has been sent, and we send stats as
144
147
  # part of a signal handler, locks cannot be acquired, so we do our best
145
148
  # to try and send the command without a lock.
146
149
  socket.send(command, 0) > 0
147
- rescue SocketError, IOError, SystemCallError, Errno::ECONNREFUSED => e
148
- StatsD.logger.error "[StatsD] #{e.class.name}: #{e.message}"
150
+ rescue SocketError, IOError, SystemCallError => e
151
+ StatsD.logger.error("[StatsD] #{e.class.name}: #{e.message}")
149
152
  invalidate_socket
150
153
  end
151
154