statsd-instrument 2.3.2 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/workflows/benchmark.yml +32 -0
  4. data/.github/workflows/ci.yml +47 -0
  5. data/.gitignore +1 -0
  6. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1027 -0
  7. data/.rubocop.yml +50 -0
  8. data/.yardopts +5 -0
  9. data/CHANGELOG.md +288 -2
  10. data/CONTRIBUTING.md +28 -6
  11. data/Gemfile +5 -0
  12. data/README.md +54 -46
  13. data/Rakefile +4 -2
  14. data/benchmark/README.md +29 -0
  15. data/benchmark/datagram-client +41 -0
  16. data/benchmark/send-metrics-to-dev-null-log +47 -0
  17. data/benchmark/send-metrics-to-local-udp-receiver +57 -0
  18. data/lib/statsd/instrument/assertions.rb +179 -30
  19. data/lib/statsd/instrument/backend.rb +3 -2
  20. data/lib/statsd/instrument/backends/capture_backend.rb +4 -1
  21. data/lib/statsd/instrument/backends/logger_backend.rb +3 -3
  22. data/lib/statsd/instrument/backends/null_backend.rb +2 -0
  23. data/lib/statsd/instrument/backends/udp_backend.rb +39 -45
  24. data/lib/statsd/instrument/capture_sink.rb +27 -0
  25. data/lib/statsd/instrument/client.rb +313 -0
  26. data/lib/statsd/instrument/datagram.rb +75 -0
  27. data/lib/statsd/instrument/datagram_builder.rb +101 -0
  28. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +71 -0
  29. data/lib/statsd/instrument/environment.rb +108 -29
  30. data/lib/statsd/instrument/helpers.rb +16 -8
  31. data/lib/statsd/instrument/log_sink.rb +24 -0
  32. data/lib/statsd/instrument/matchers.rb +14 -11
  33. data/lib/statsd/instrument/metric.rb +72 -45
  34. data/lib/statsd/instrument/metric_expectation.rb +32 -18
  35. data/lib/statsd/instrument/null_sink.rb +13 -0
  36. data/lib/statsd/instrument/railtie.rb +2 -1
  37. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +39 -0
  38. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +42 -0
  39. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +37 -0
  40. data/lib/statsd/instrument/rubocop/metric_return_value.rb +32 -0
  41. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +36 -0
  42. data/lib/statsd/instrument/rubocop/positional_arguments.rb +99 -0
  43. data/lib/statsd/instrument/rubocop/splat_arguments.rb +31 -0
  44. data/lib/statsd/instrument/rubocop.rb +64 -0
  45. data/lib/statsd/instrument/statsd_datagram_builder.rb +14 -0
  46. data/lib/statsd/instrument/strict.rb +235 -0
  47. data/lib/statsd/instrument/udp_sink.rb +62 -0
  48. data/lib/statsd/instrument/version.rb +3 -1
  49. data/lib/statsd/instrument.rb +340 -163
  50. data/lib/statsd-instrument.rb +2 -0
  51. data/statsd-instrument.gemspec +13 -10
  52. data/test/assertions_test.rb +167 -156
  53. data/test/benchmark/clock_gettime.rb +27 -0
  54. data/test/benchmark/default_tags.rb +47 -0
  55. data/test/benchmark/metrics.rb +9 -8
  56. data/test/benchmark/tags.rb +5 -3
  57. data/test/capture_backend_test.rb +4 -2
  58. data/test/capture_sink_test.rb +44 -0
  59. data/test/client_test.rb +164 -0
  60. data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +162 -0
  61. data/test/datagram_builder_test.rb +120 -0
  62. data/test/deprecations_test.rb +132 -0
  63. data/test/dogstatsd_datagram_builder_test.rb +32 -0
  64. data/test/environment_test.rb +75 -8
  65. data/test/helpers/rubocop_helper.rb +47 -0
  66. data/test/helpers_test.rb +2 -1
  67. data/test/integration_test.rb +31 -7
  68. data/test/log_sink_test.rb +37 -0
  69. data/test/logger_backend_test.rb +10 -8
  70. data/test/matchers_test.rb +42 -28
  71. data/test/metric_test.rb +18 -22
  72. data/test/null_sink_test.rb +13 -0
  73. data/test/rubocop/measure_as_dist_argument_test.rb +44 -0
  74. data/test/rubocop/metaprogramming_positional_arguments_test.rb +58 -0
  75. data/test/rubocop/metric_prefix_argument_test.rb +38 -0
  76. data/test/rubocop/metric_return_value_test.rb +78 -0
  77. data/test/rubocop/metric_value_keyword_argument_test.rb +39 -0
  78. data/test/rubocop/positional_arguments_test.rb +110 -0
  79. data/test/rubocop/splat_arguments_test.rb +27 -0
  80. data/test/statsd_datagram_builder_test.rb +22 -0
  81. data/test/statsd_instrumentation_test.rb +109 -100
  82. data/test/statsd_test.rb +113 -79
  83. data/test/test_helper.rb +12 -1
  84. data/test/udp_backend_test.rb +38 -36
  85. data/test/udp_sink_test.rb +85 -0
  86. metadata +85 -5
  87. data/.travis.yml +0 -12
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark/ips'
6
+ require 'logger'
7
+
8
+ revision = %x(git rev-parse HEAD).rstrip
9
+ master_revision = %x(git rev-parse origin/master).rstrip
10
+ branch = if revision == master_revision
11
+ 'master'
12
+ else
13
+ %x(git rev-parse --abbrev-ref HEAD).rstrip
14
+ end
15
+
16
+ intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}"
17
+ FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
18
+
19
+ ENV['ENV'] = "development"
20
+ require 'statsd-instrument'
21
+ StatsD.logger = Logger.new(File::NULL)
22
+
23
+ report = Benchmark.ips do |bench|
24
+ bench.report("StatsD metrics to /dev/null log (branch: #{branch}, sha: #{revision[0, 7]})") do
25
+ StatsD.increment('StatsD.increment', 10, sample_rate: 15)
26
+ StatsD.measure('StatsD.measure') { 1 + 1 }
27
+ StatsD.gauge('StatsD.gauge', 12.0, tags: ["foo:bar", "quc"])
28
+ StatsD.set('StatsD.set', 'value', tags: { foo: 'bar', baz: 'quc' })
29
+ StatsD.event('StasD.event', "12345")
30
+ StatsD.service_check("StatsD.service_check", "ok")
31
+ end
32
+
33
+ # Store the results in between runs
34
+ bench.save!(intermediate_results_filename)
35
+ bench.compare!
36
+ end
37
+
38
+ if report.entries.length == 1
39
+ puts
40
+ puts "To compare the performance of this revision against another revision (e.g. master),"
41
+ puts "check out a different branch and run this benchmark script again."
42
+ elsif ENV['KEEP_RESULTS']
43
+ puts
44
+ puts "The intermediate results have been stored in #{intermediate_results_filename}"
45
+ else
46
+ File.unlink(intermediate_results_filename)
47
+ end
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'benchmark/ips'
6
+ require 'socket'
7
+
8
+ revision = %x(git rev-parse HEAD).rstrip
9
+ master_revision = %x(git rev-parse origin/master).rstrip
10
+ branch = if revision == master_revision
11
+ 'master'
12
+ else
13
+ %x(git rev-parse --abbrev-ref HEAD).rstrip
14
+ end
15
+
16
+ intermediate_results_filename = "#{Dir.tmpdir}/statsd-instrument-benchmarks/#{File.basename($PROGRAM_NAME)}"
17
+ FileUtils.mkdir_p(File.dirname(intermediate_results_filename))
18
+
19
+ # Set up an UDP listener to which we can send StatsD packets
20
+ receiver = UDPSocket.new
21
+ receiver.bind('localhost', 0)
22
+
23
+ ENV['ENV'] = "production"
24
+ ENV['STATSD_ADDR'] = "#{receiver.addr[2]}:#{receiver.addr[1]}"
25
+ ENV['STATSD_IMPLEMENTATION'] ||= 'datadog'
26
+
27
+ require 'statsd-instrument'
28
+
29
+ report = Benchmark.ips do |bench|
30
+ bench.report("StatsD metrics to local UDP receiver (branch: #{branch}, sha: #{revision[0, 7]})") do
31
+ StatsD.increment('StatsD.increment', 10)
32
+ StatsD.measure('StatsD.measure') { 1 + 1 }
33
+ StatsD.gauge('StatsD.gauge', 12.0, tags: ["foo:bar", "quc"])
34
+ StatsD.set('StatsD.set', 'value', tags: { foo: 'bar', baz: 'quc' })
35
+ if StatsD.backend.implementation == :datadog
36
+ StatsD.event('StasD.event', "12345")
37
+ StatsD.service_check("StatsD.service_check", "ok")
38
+ end
39
+ end
40
+
41
+ # Store the results in between runs
42
+ bench.save!(intermediate_results_filename)
43
+ bench.compare!
44
+ end
45
+
46
+ receiver.close
47
+
48
+ if report.entries.length == 1
49
+ puts
50
+ puts "To compare the performance of this revision against another revision (e.g. master),"
51
+ puts "check out a different branch and run this benchmark script again."
52
+ elsif ENV['KEEP_RESULTS']
53
+ puts
54
+ puts "The intermediate results have been stored in #{intermediate_results_filename}"
55
+ else
56
+ File.unlink(intermediate_results_filename)
57
+ end
@@ -1,79 +1,228 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This module defines several assertion methods that can be used to verify that
4
+ # your application is emitting the right StatsD metrics.
5
+ #
6
+ # Every metric type has its own assertion method, like {#assert_statsd_increment}
7
+ # to assert `StatsD.increment` calls. You can also assert other properties of the
8
+ # metric that was emitted, lioke the sample rate or presence of tags.
9
+ # To check for the absence of metrics, use {#assert_no_statsd_calls}.
10
+ #
11
+ # @example Check for metric properties:
12
+ # assert_statsd_measure('foo', sample_rate: 0.1, tags: ["bar"]) do
13
+ # StatsD.measure('foo', sample_rate: 0.5, tags: ['bar','baz']) do
14
+ # some_code_to_measure
15
+ # end
16
+ # end
17
+ #
18
+ # @example Check for multiple occurrences:
19
+ # assert_statsd_increment('foo', times: 2) do
20
+ # StatsD.increment('foo')
21
+ # StatsD.increment('foo')
22
+ # end
23
+ #
24
+ # @example Absence of metrics
25
+ # assert_no_statsd_calls do
26
+ # foo
27
+ # end
28
+ #
29
+ # @example Handling exceptions
30
+ # assert_statsd_increment('foo.error') do
31
+ # # If we expect exceptions to occur, we have to handle them inside
32
+ # # the block we pass to assert_statsd_increment.
33
+ # assert_raises(RuntimeError) do
34
+ # begin
35
+ # attempt_foo
36
+ # rescue
37
+ # StatsD.increment('foo.error')
38
+ # raise 'foo failed'
39
+ # end
40
+ # end
41
+ # end
1
42
  module StatsD::Instrument::Assertions
2
43
  include StatsD::Instrument::Helpers
3
44
 
45
+ # Asserts no metric occurred during the execution of the provided block.
46
+ #
47
+ # @param [String] metric_name (default: nil) The metric name that is not allowed
48
+ # to happen inside the block. If this is set to `nil`, the assertion will fail
49
+ # if any metric occurs.
50
+ # @yield A block in which the specified metric should not occur. This block
51
+ # should not raise any exceptions.
52
+ # @return [void]
53
+ # @raise [Minitest::Assertion] If an exception occurs, or if any metric (with the
54
+ # provided name, or any), occurred during the execution of the provided block.
4
55
  def assert_no_statsd_calls(metric_name = nil, &block)
5
56
  metrics = capture_statsd_calls(&block)
6
57
  metrics.select! { |m| m.name == metric_name } if metric_name
7
- assert metrics.empty?, "No StatsD calls for metric #{metrics.map(&:name).join(', ')} expected."
58
+ assert(metrics.empty?, "No StatsD calls for metric #{metrics.map(&:name).join(', ')} expected.")
59
+ rescue => exception
60
+ flunk(<<~MESSAGE)
61
+ An exception occurred in the block provided to the StatsD assertion.
62
+
63
+ #{exception.class.name}: #{exception.message}
64
+ \t#{exception.backtrace.join("\n\t")}
65
+
66
+ If this exception is expected, make sure to handle it using `assert_raises`
67
+ inside the block provided to the StatsD assertion.
68
+ MESSAGE
8
69
  end
9
70
 
71
+ # Asserts that a given counter metric occurred inside the provided block.
72
+ #
73
+ # @param [String] metric_name The name of the metric that should occur.
74
+ # @param [Hash] options (see StatsD::Instrument::MetricExpectation.new)
75
+ # @yield A block in which the specified metric should occur. This block
76
+ # should not raise any exceptions.
77
+ # @return [void]
78
+ # @raise [Minitest::Assertion] If an exception occurs, or if the metric did
79
+ # not occur as specified during the execution the block.
10
80
  def assert_statsd_increment(metric_name, options = {}, &block)
11
81
  assert_statsd_call(:c, metric_name, options, &block)
12
82
  end
13
83
 
84
+ # Asserts that a given timing metric occurred inside the provided block.
85
+ #
86
+ # @param metric_name (see #assert_statsd_increment)
87
+ # @param options (see #assert_statsd_increment)
88
+ # @yield (see #assert_statsd_increment)
89
+ # @return [void]
90
+ # @raise (see #assert_statsd_increment)
14
91
  def assert_statsd_measure(metric_name, options = {}, &block)
15
92
  assert_statsd_call(:ms, metric_name, options, &block)
16
93
  end
17
94
 
95
+ # Asserts that a given gauge metric occurred inside the provided block.
96
+ #
97
+ # @param metric_name (see #assert_statsd_increment)
98
+ # @param options (see #assert_statsd_increment)
99
+ # @yield (see #assert_statsd_increment)
100
+ # @return [void]
101
+ # @raise (see #assert_statsd_increment)
18
102
  def assert_statsd_gauge(metric_name, options = {}, &block)
19
103
  assert_statsd_call(:g, metric_name, options, &block)
20
104
  end
21
105
 
106
+ # Asserts that a given histogram metric occurred inside the provided block.
107
+ #
108
+ # @param metric_name (see #assert_statsd_increment)
109
+ # @param options (see #assert_statsd_increment)
110
+ # @yield (see #assert_statsd_increment)
111
+ # @return [void]
112
+ # @raise (see #assert_statsd_increment)
22
113
  def assert_statsd_histogram(metric_name, options = {}, &block)
23
114
  assert_statsd_call(:h, metric_name, options, &block)
24
115
  end
25
116
 
117
+ # Asserts that a given distribution metric occurred inside the provided block.
118
+ #
119
+ # @param metric_name (see #assert_statsd_increment)
120
+ # @param options (see #assert_statsd_increment)
121
+ # @yield (see #assert_statsd_increment)
122
+ # @return [void]
123
+ # @raise (see #assert_statsd_increment)
26
124
  def assert_statsd_distribution(metric_name, options = {}, &block)
27
125
  assert_statsd_call(:d, metric_name, options, &block)
28
126
  end
29
127
 
128
+ # Asserts that a given set metric occurred inside the provided block.
129
+ #
130
+ # @param metric_name (see #assert_statsd_increment)
131
+ # @param options (see #assert_statsd_increment)
132
+ # @yield (see #assert_statsd_increment)
133
+ # @return [void]
134
+ # @raise (see #assert_statsd_increment)
30
135
  def assert_statsd_set(metric_name, options = {}, &block)
31
136
  assert_statsd_call(:s, metric_name, options, &block)
32
137
  end
33
138
 
139
+ # Asserts that a given key/value metric occurred inside the provided block.
140
+ #
141
+ # @param metric_name (see #assert_statsd_increment)
142
+ # @param options (see #assert_statsd_increment)
143
+ # @yield (see #assert_statsd_increment)
144
+ # @return [void]
145
+ # @raise (see #assert_statsd_increment)
34
146
  def assert_statsd_key_value(metric_name, options = {}, &block)
35
147
  assert_statsd_call(:kv, metric_name, options, &block)
36
148
  end
37
149
 
150
+ # Asserts that the set of provided metric expectations came true.
151
+ #
152
+ # Generally, it's recommended to use more specific assertion methods, like
153
+ # {#assert_statsd_increment} and others.
154
+ #
38
155
  # @private
39
- def assert_statsd_calls(expected_metrics, &block)
40
- unless block
41
- raise ArgumentError, "block must be given"
42
- end
156
+ # @param [Array<StatsD::Instrument::MetricExpectation>] expected_metrics The set of
157
+ # metric expectations to verify.
158
+ # @yield (see #assert_statsd_increment)
159
+ # @return [void]
160
+ # @raise (see #assert_statsd_increment)
161
+ def assert_statsd_calls(expected_metrics)
162
+ raise ArgumentError, "block must be given" unless block_given?
163
+
164
+ capture_backend = StatsD::Instrument::Backends::CaptureBackend.new
165
+ with_capture_backend(capture_backend) do
166
+ begin
167
+ yield
168
+ rescue => exception
169
+ flunk(<<~MESSAGE)
170
+ An exception occurred in the block provided to the StatsD assertion.
171
+
172
+ #{exception.class.name}: #{exception.message}
173
+ \t#{exception.backtrace.join("\n\t")}
174
+
175
+ If this exception is expected, make sure to handle it using `assert_raises`
176
+ inside the block provided to the StatsD assertion.
177
+ MESSAGE
178
+ end
179
+
180
+ metrics = capture_backend.collected_metrics
181
+ matched_expected_metrics = []
182
+ expected_metrics.each do |expected_metric|
183
+ expected_metric_times = expected_metric.times
184
+ expected_metric_times_remaining = expected_metric.times
185
+ filtered_metrics = metrics.select { |m| m.type == expected_metric.type && m.name == expected_metric.name }
186
+
187
+ if filtered_metrics.empty?
188
+ flunk("No StatsD calls for metric #{expected_metric.name} of type #{expected_metric.type} were made.")
189
+ end
190
+
191
+ filtered_metrics.each do |metric|
192
+ next unless expected_metric.matches(metric)
193
+
194
+ assert(within_numeric_range?(metric.sample_rate),
195
+ "Unexpected sample rate type for metric #{metric.name}, must be numeric")
196
+
197
+ if expected_metric_times_remaining == 0
198
+ flunk("Unexpected StatsD call; number of times this metric " \
199
+ "was expected exceeded: #{expected_metric.inspect}")
200
+ end
43
201
 
44
- metrics = capture_statsd_calls(&block)
45
- matched_expected_metrics = []
46
-
47
- expected_metrics.each do |expected_metric|
48
- expected_metric_times = expected_metric.times
49
- expected_metric_times_remaining = expected_metric.times
50
- 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
-
54
- filtered_metrics.each do |metric|
55
- assert within_numeric_range?(metric.sample_rate),
56
- "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
202
  expected_metric_times_remaining -= 1
61
203
  metrics.delete(metric)
62
204
  if expected_metric_times_remaining == 0
63
205
  matched_expected_metrics << expected_metric
64
206
  end
65
207
  end
208
+
209
+ next if expected_metric_times_remaining == 0
210
+
211
+ msg = +"Metric expected #{expected_metric_times} times but seen " \
212
+ "#{expected_metric_times - expected_metric_times_remaining} " \
213
+ "times: #{expected_metric.inspect}."
214
+ msg << "\nCaptured metrics with the same key: #{filtered_metrics}" if filtered_metrics.any?
215
+ flunk(msg)
66
216
  end
217
+ expected_metrics -= matched_expected_metrics
67
218
 
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
- end
73
- expected_metrics -= matched_expected_metrics
219
+ unless expected_metrics.empty?
220
+ flunk("Unexpected StatsD calls; the following metric expectations " \
221
+ "were not satisfied: #{expected_metrics.inspect}")
222
+ end
74
223
 
75
- assert expected_metrics.empty?,
76
- "Unexpected StatsD calls; the following metric expectations were not satisfied: #{expected_metrics.inspect}"
224
+ pass
225
+ end
77
226
  end
78
227
 
79
228
  private
@@ -87,6 +236,6 @@ module StatsD::Instrument::Assertions
87
236
  end
88
237
 
89
238
  def within_numeric_range?(object)
90
- object.kind_of?(Numeric) && (0.0..1.0).cover?(object)
239
+ object.is_a?(Numeric) && (0.0..1.0).cover?(object)
91
240
  end
92
241
  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
  #
@@ -8,6 +9,7 @@ module StatsD::Instrument::Backends
8
9
  # @see StatsD::Instrument::Assertions
9
10
  class CaptureBackend < StatsD::Instrument::Backend
10
11
  attr_reader :collected_metrics
12
+ attr_accessor :parent
11
13
 
12
14
  def initialize
13
15
  reset
@@ -17,6 +19,7 @@ module StatsD::Instrument::Backends
17
19
  # @param metric [StatsD::Instrument::Metric] The metric to collect.
18
20
  # @return [void]
19
21
  def collect_metric(metric)
22
+ parent&.collect_metric(metric)
20
23
  @collected_metrics << metric
21
24
  end
22
25
 
@@ -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,63 +1,56 @@
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
9
- EVENT_OPTIONS = {
10
- date_happened: 'd',
11
- hostname: 'h',
12
- aggregation_key: 'k',
13
- priority: 'p',
14
- source_type_name: 's',
15
- alert_type: 't',
16
- }
17
-
18
- SERVICE_CHECK_OPTIONS = {
19
- timestamp: 'd',
20
- hostname: 'h',
21
- message: 'm',
22
- }
23
-
24
10
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES.merge(h: true, _e: true, _sc: true, d: true)
25
11
 
12
+ SERVICE_CHECK_STATUSES = { ok: 0, warning: 1, critical: 2, unknown: 3 }
13
+
26
14
  def generate_packet(metric)
27
- packet = ""
15
+ packet = +""
28
16
 
29
17
  if metric.type == :_e
30
- escaped_title = metric.name.tr('\n', '\\n')
31
- escaped_text = metric.value.tr('\n', '\\n')
18
+ escaped_title = metric.name.gsub("\n", "\\n")
19
+ escaped_text = metric.value.gsub("\n", "\\n")
32
20
 
33
21
  packet << "_e{#{escaped_title.size},#{escaped_text.size}}:#{escaped_title}|#{escaped_text}"
34
- packet << generate_metadata(metric, EVENT_OPTIONS)
22
+ packet << "|h:#{metric.metadata[:hostname]}" if metric.metadata[:hostname]
23
+ packet << "|d:#{metric.metadata[:timestamp].to_i}" if metric.metadata[:timestamp]
24
+ packet << "|k:#{metric.metadata[:aggregation_key]}" if metric.metadata[:aggregation_key]
25
+ packet << "|p:#{metric.metadata[:priority]}" if metric.metadata[:priority]
26
+ packet << "|s:#{metric.metadata[:source_type_name]}" if metric.metadata[:source_type_name]
27
+ packet << "|t:#{metric.metadata[:alert_type]}" if metric.metadata[:alert_type]
28
+ packet << "|##{metric.tags.join(',')}" if metric.tags
29
+
35
30
  elsif metric.type == :_sc
36
- packet << "_sc|#{metric.name}|#{metric.value}"
37
- packet << generate_metadata(metric, SERVICE_CHECK_OPTIONS)
31
+ status = metric.value.is_a?(Integer) ? metric.value : SERVICE_CHECK_STATUSES.fetch(metric.value.to_sym)
32
+
33
+ packet << "_sc|#{metric.name}|#{status}"
34
+ packet << "|h:#{metric.metadata[:hostname]}" if metric.metadata[:hostname]
35
+ packet << "|d:#{metric.metadata[:timestamp].to_i}" if metric.metadata[:timestamp]
36
+ packet << "|##{metric.tags.join(',')}" if metric.tags
37
+ packet << "|m:#{metric.metadata[:message]}" if metric.metadata[:message]
38
+
38
39
  else
39
40
  packet << "#{metric.name}:#{metric.value}|#{metric.type}"
41
+ packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
42
+ packet << "|##{metric.tags.join(',')}" if metric.tags
40
43
  end
41
44
 
42
- packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
43
- packet << "|##{metric.tags.join(',')}" if metric.tags
44
45
  packet
45
46
  end
46
-
47
- private
48
-
49
- def generate_metadata(metric, options)
50
- (metric.metadata.keys & options.keys).map do |key|
51
- "|#{options[key]}:#{metric.metadata[key]}"
52
- end.join
53
- end
54
47
  end
55
48
 
56
49
  class StatsiteStatsDProtocol
57
50
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES.merge(kv: true)
58
51
 
59
52
  def generate_packet(metric)
60
- packet = "#{metric.name}:#{metric.value}|#{metric.type}"
53
+ packet = +"#{metric.name}:#{metric.value}|#{metric.type}"
61
54
  packet << "|@#{metric.sample_rate}" unless metric.sample_rate == 1
62
55
  packet << "\n"
63
56
  packet
@@ -68,7 +61,7 @@ module StatsD::Instrument::Backends
68
61
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES
69
62
 
70
63
  def generate_packet(metric)
71
- packet = "#{metric.name}:#{metric.value}|#{metric.type}"
64
+ packet = +"#{metric.name}:#{metric.value}|#{metric.type}"
72
65
  packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
73
66
  packet
74
67
  end
@@ -88,19 +81,20 @@ module StatsD::Instrument::Backends
88
81
 
89
82
  def implementation=(value)
90
83
  @packet_factory = case value
91
- when :datadog
92
- DogStatsDProtocol.new
93
- when :statsite
94
- StatsiteStatsDProtocol.new
95
- else
96
- StatsDProtocol.new
97
- end
84
+ when :datadog
85
+ DogStatsDProtocol.new
86
+ when :statsite
87
+ StatsiteStatsDProtocol.new
88
+ else
89
+ StatsDProtocol.new
90
+ end
98
91
  @implementation = value
99
92
  end
100
93
 
101
94
  def collect_metric(metric)
102
95
  unless @packet_factory.class::SUPPORTED_METRIC_TYPES[metric.type]
103
- StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} not supported on #{implementation} implementation.")
96
+ StatsD.logger.warn("[StatsD] Metric type #{metric.type.inspect} is not supported " \
97
+ "on #{implementation} implementation.")
104
98
  return false
105
99
  end
106
100
 
@@ -139,13 +133,13 @@ module StatsD::Instrument::Backends
139
133
  synchronize do
140
134
  socket.send(command, 0) > 0
141
135
  end
142
- rescue ThreadError => e
136
+ rescue ThreadError
143
137
  # In cases where a TERM or KILL signal has been sent, and we send stats as
144
138
  # part of a signal handler, locks cannot be acquired, so we do our best
145
139
  # to try and send the command without a lock.
146
140
  socket.send(command, 0) > 0
147
- rescue SocketError, IOError, SystemCallError, Errno::ECONNREFUSED => e
148
- StatsD.logger.error "[StatsD] #{e.class.name}: #{e.message}"
141
+ rescue SocketError, IOError, SystemCallError => e
142
+ StatsD.logger.error("[StatsD] #{e.class.name}: #{e.message}")
149
143
  invalidate_socket
150
144
  end
151
145
 
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ # @note This class is part of the new Client implementation that is intended
4
+ # to become the new default in the next major release of this library.
5
+ class StatsD::Instrument::CaptureSink
6
+ attr_reader :parent, :datagrams, :datagram_class
7
+
8
+ def initialize(parent:, datagram_class: StatsD::Instrument::Datagram)
9
+ @parent = parent
10
+ @datagram_class = datagram_class
11
+ @datagrams = []
12
+ end
13
+
14
+ def sample?(_sample_rate)
15
+ true
16
+ end
17
+
18
+ def <<(datagram)
19
+ @datagrams << datagram_class.new(datagram)
20
+ parent << datagram
21
+ self
22
+ end
23
+
24
+ def clear
25
+ @datagrams.clear
26
+ end
27
+ end