statsd-instrument 2.8.0 → 2.9.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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +16 -7
  3. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +6 -6
  4. data/CHANGELOG.md +28 -6
  5. data/CONTRIBUTING.md +3 -3
  6. data/README.md +11 -11
  7. data/lib/statsd/instrument.rb +33 -18
  8. data/lib/statsd/instrument/assertions.rb +7 -7
  9. data/lib/statsd/instrument/backend.rb +1 -1
  10. data/lib/statsd/instrument/client.rb +11 -10
  11. data/lib/statsd/instrument/datagram.rb +1 -2
  12. data/lib/statsd/instrument/datagram_builder.rb +1 -1
  13. data/lib/statsd/instrument/dogstatsd_datagram.rb +88 -0
  14. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +4 -0
  15. data/lib/statsd/instrument/expectation.rb +37 -1
  16. data/lib/statsd/instrument/legacy_client.rb +5 -5
  17. data/lib/statsd/instrument/metric.rb +2 -2
  18. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +1 -1
  19. data/lib/statsd/instrument/rubocop/positional_arguments.rb +6 -6
  20. data/lib/statsd/instrument/rubocop/singleton_configuration.rb +1 -1
  21. data/lib/statsd/instrument/strict.rb +37 -17
  22. data/lib/statsd/instrument/version.rb +1 -1
  23. data/test/assertions_on_legacy_client_test.rb +1 -1
  24. data/test/assertions_test.rb +26 -1
  25. data/test/capture_sink_test.rb +1 -1
  26. data/test/client_test.rb +5 -5
  27. data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +1 -1
  28. data/test/datagram_builder_test.rb +1 -1
  29. data/test/deprecations_test.rb +8 -1
  30. data/test/dogstatsd_datagram_builder_test.rb +41 -4
  31. data/test/environment_test.rb +1 -1
  32. data/test/integration_test.rb +1 -1
  33. data/test/log_sink_test.rb +1 -1
  34. data/test/null_sink_test.rb +1 -1
  35. data/test/rubocop/metric_prefix_argument_test.rb +1 -1
  36. data/test/rubocop/positional_arguments_test.rb +3 -3
  37. data/test/statsd_instrumentation_test.rb +11 -0
  38. data/test/statsd_test.rb +2 -9
  39. data/test/udp_backend_test.rb +3 -0
  40. data/test/udp_sink_test.rb +1 -1
  41. metadata +3 -2
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The Datagram class parses and inspects a StatsD datagrans
3
+ # The Datagram class parses and inspects a StatsD datagrams
4
4
  #
5
5
  # @note This class is part of the new Client implementation that is intended
6
6
  # to become the new default in the next major release of this library.
@@ -72,7 +72,6 @@ class StatsD::Instrument::Datagram
72
72
  \n? # In some implementations, the datagram may include a trailing newline.
73
73
  \z
74
74
  }x
75
- private_constant :PARSER
76
75
 
77
76
  def parsed_datagram
78
77
  @parsed ||= if (match_info = PARSER.match(@source))
@@ -18,7 +18,7 @@ class StatsD::Instrument::DatagramBuilder
18
18
  def self.unsupported_datagram_types(*types)
19
19
  types.each do |type|
20
20
  define_method(type) do |_, _, _, _|
21
- raise NotImplementedError, "Type #{type} metrics are not suppered by #{self.class.name}."
21
+ raise NotImplementedError, "Type #{type} metrics are not supported by #{self.class.name}."
22
22
  end
23
23
  end
24
24
  end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Datagram class parses and inspects a StatsD datagrams
4
+ #
5
+ # @note This class is part of the new Client implementation that is intended
6
+ # to become the new default in the next major release of this library.
7
+ class StatsD::Instrument::DogStatsDDatagram < StatsD::Instrument::Datagram
8
+ def name
9
+ @name ||= case type
10
+ when :_e then parsed_datagram[:name].gsub('\n', "\n")
11
+ else super
12
+ end
13
+ end
14
+
15
+ def value
16
+ @value ||= case type
17
+ when :_sc then Integer(parsed_datagram[:value])
18
+ when :_e then parsed_datagram[:value].gsub('\n', "\n")
19
+ else super
20
+ end
21
+ end
22
+
23
+ def hostname
24
+ parsed_datagram[:hostname]
25
+ end
26
+
27
+ def timestamp
28
+ Time.at(Integer(parsed_datagram[:timestamp])).utc
29
+ end
30
+
31
+ def aggregation_key
32
+ parsed_datagram[:aggregation_key]
33
+ end
34
+
35
+ def source_type_name
36
+ parsed_datagram[:source_type_name]
37
+ end
38
+
39
+ def priority
40
+ parsed_datagram[:priority]
41
+ end
42
+
43
+ def alert_type
44
+ parsed_datagram[:alert_type]
45
+ end
46
+
47
+ def message
48
+ parsed_datagram[:message]
49
+ end
50
+
51
+ protected
52
+
53
+ def parsed_datagram
54
+ @parsed ||= if (match_info = PARSER.match(@source))
55
+ match_info
56
+ else
57
+ raise ArgumentError, "Invalid DogStatsD datagram: #{@source}"
58
+ end
59
+ end
60
+
61
+ SERVICE_CHECK_PARSER = %r{
62
+ \A
63
+ (?<type>_sc)\|(?<name>[^\|]+)\|(?<value>\d+)
64
+ (?:\|h:(?<hostname>[^\|]+))?
65
+ (?:\|d:(?<timestamp>\d+))?
66
+ (?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
67
+ (?:\|m:(?<message>[^\|]+))?
68
+ \n? # In some implementations, the datagram may include a trailing newline.
69
+ \z
70
+ }x
71
+
72
+ # |k:my-key|p:low|s:source|t:success|
73
+ EVENT_PARSER = %r{
74
+ \A
75
+ (?<type>_e)\{\d+\,\d+\}:(?<name>[^\|]+)\|(?<value>[^\|]+)
76
+ (?:\|h:(?<hostname>[^\|]+))?
77
+ (?:\|d:(?<timestamp>\d+))?
78
+ (?:\|k:(?<aggregation_key>[^\|]+))?
79
+ (?:\|p:(?<priority>[^\|]+))?
80
+ (?:\|s:(?<source_type_name>[^\|]+))?
81
+ (?:\|t:(?<alert_type>[^\|]+))?
82
+ (?:\|\#(?<tags>(?:[^\|,]+(?:,[^\|,]+)*)))?
83
+ \n? # In some implementations, the datagram may include a trailing newline.
84
+ \z
85
+ }x
86
+
87
+ PARSER = Regexp.union(StatsD::Instrument::Datagram::PARSER, SERVICE_CHECK_PARSER, EVENT_PARSER)
88
+ end
@@ -5,6 +5,10 @@
5
5
  class StatsD::Instrument::DogStatsDDatagramBuilder < StatsD::Instrument::DatagramBuilder
6
6
  unsupported_datagram_types :kv
7
7
 
8
+ def self.datagram_class
9
+ StatsD::Instrument::DogStatsDDatagram
10
+ end
11
+
8
12
  def latency_metric_type
9
13
  :d
10
14
  end
@@ -41,7 +41,7 @@ class StatsD::Instrument::Expectation
41
41
  @name = client.prefix ? "#{client.prefix}.#{name}" : name unless no_prefix
42
42
  @value = normalized_value_for_type(type, value) if value
43
43
  @sample_rate = sample_rate
44
- @tags = StatsD::Instrument::Metric.normalize_tags(tags)
44
+ @tags = normalize_tags(tags)
45
45
  @times = times
46
46
  end
47
47
 
@@ -77,6 +77,42 @@ class StatsD::Instrument::Expectation
77
77
  def inspect
78
78
  "#<StatsD::Instrument::Expectation:\"#{self}\">"
79
79
  end
80
+
81
+ private
82
+
83
+ # Needed for normalize_tags
84
+ unless Regexp.method_defined?(:match?) # for ruby 2.3
85
+ module RubyBackports
86
+ refine Regexp do
87
+ def match?(str)
88
+ (self =~ str) != nil
89
+ end
90
+ end
91
+ end
92
+
93
+ using RubyBackports
94
+ end
95
+
96
+ # @private
97
+ #
98
+ # Utility function to convert tags to the canonical form.
99
+ #
100
+ # - Tags specified as key value pairs will be converted into an array
101
+ # - Tags are normalized to remove unsupported characters
102
+ #
103
+ # @param tags [Array<String>, Hash<String, String>, nil] Tags specified in any form.
104
+ # @return [Array<String>, nil] the list of tags in canonical form.
105
+ #
106
+ # @todo We should delegate this to thje datagram builder of the current client,
107
+ # to ensure that this logic matches the logic of the active datagram builder.
108
+ def normalize_tags(tags)
109
+ return [] unless tags
110
+ tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
111
+
112
+ # Fast path when no string replacement is needed
113
+ return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
114
+ tags.map { |tag| tag.tr('|,', '') }
115
+ end
80
116
  end
81
117
 
82
118
  # For backwards compatibility
@@ -120,7 +120,7 @@ class StatsD::Instrument::LegacyClient
120
120
  #
121
121
  # Emits a set metric, which counts the number of distinct values that have occurred.
122
122
  #
123
- # @example Couning the number of unique visitors
123
+ # @example Counting the number of unique visitors
124
124
  # StatsD.set('visitors.unique', Current.user.id)
125
125
  #
126
126
  # @param key [String] The name of the metric.
@@ -222,7 +222,7 @@ class StatsD::Instrument::LegacyClient
222
222
  # @param title [String] Title of the event. A configured prefix may be applied to this title.
223
223
  # @param text [String] Body of the event. Can contain newlines.
224
224
  # @param [String] hostname The hostname to associate with the event.
225
- # @param [Time] timestamp The moment the status of the service was checkes. Defaults to now.
225
+ # @param [Time] timestamp The moment the status of the service was checked. Defaults to now.
226
226
  # @param [String] aggregation_key A key to aggregate similar events into groups.
227
227
  # @param [String] priority The event's priority, either `"low"` or `"normal"` (default).
228
228
  # @param [String] source_type_name The source type.
@@ -254,7 +254,7 @@ class StatsD::Instrument::LegacyClient
254
254
  # @param [String] name Name of the service. A configured prefix may be applied to this title.
255
255
  # @param [Symbol] status Current status of the service. Either `:ok`, `:warning`, `:critical`, or `:unknown`.
256
256
  # @param [String] hostname The hostname to associate with the event.
257
- # @param [Time] timestamp The moment the status of the service was checkes. Defaults to now.
257
+ # @param [Time] timestamp The moment the status of the service was checked. Defaults to now.
258
258
  # @param [String] message A message that describes the current status.
259
259
  # @param tags (see #increment)
260
260
  # @return [void]
@@ -273,7 +273,7 @@ class StatsD::Instrument::LegacyClient
273
273
  })
274
274
  end
275
275
 
276
- private
276
+ protected
277
277
 
278
278
  def measure_latency(type, key, sample_rate:, tags:, prefix:)
279
279
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -296,6 +296,6 @@ class StatsD::Instrument::LegacyClient
296
296
  metric = StatsD::Instrument::Metric.new(type: type, name: name, value: value,
297
297
  sample_rate: sample_rate, tags: tags, metadata: metadata)
298
298
  backend.collect_metric(metric)
299
- metric # TODO: return `nil` in the next major version
299
+ metric
300
300
  end
301
301
  end
@@ -54,11 +54,11 @@ class StatsD::Instrument::Metric
54
54
  # The default value for this metric, which will be used if it is not set.
55
55
  #
56
56
  # A default value is only defined for counter metrics (<tt>1</tt>). For all other
57
- # metric types, this emthod will raise an <tt>ArgumentError</tt>.
57
+ # metric types, this method will raise an <tt>ArgumentError</tt>.
58
58
  #
59
59
  #
60
60
  # A default value is only defined for counter metrics (<tt>1</tt>). For all other
61
- # metric types, this emthod will raise an <tt>ArgumentError</tt>.
61
+ # metric types, this method will raise an <tt>ArgumentError</tt>.
62
62
  #
63
63
  # @return [Numeric, String] The default value for this metric.
64
64
  # @raise ArgumentError if the metric type doesn't have a default value
@@ -6,7 +6,7 @@ module RuboCop
6
6
  module Cop
7
7
  module StatsD
8
8
  # This Rubocop will check for using the metaprogramming macros for positional
9
- # argument usage, which is deprecated. These macros include `statd_count_if`,
9
+ # argument usage, which is deprecated. These macros include `statsd_count_if`,
10
10
  # `statsd_measure`, etc.
11
11
  #
12
12
  # Use the following Rubocop invocation to check your project's codebase:
@@ -42,16 +42,16 @@ module RuboCop
42
42
 
43
43
  def autocorrect(node)
44
44
  -> (corrector) do
45
- positial_arguments = if node.arguments.last.type == :block_pass
45
+ positional_arguments = if node.arguments.last.type == :block_pass
46
46
  node.arguments[2...node.arguments.length - 1]
47
47
  else
48
48
  node.arguments[2...node.arguments.length]
49
49
  end
50
50
 
51
- case positial_arguments[0].type
51
+ case positional_arguments[0].type
52
52
  when *UNKNOWN_ARGUMENT_TYPES
53
53
  # We don't know whether the method returns a hash, in which case it would be interpreted
54
- # as keyword arguments. In this case, the fix would be to add a keywordf splat:
54
+ # as keyword arguments. In this case, the fix would be to add a keyword splat:
55
55
  #
56
56
  # `StatsD.instrument('foo', 1, method_call)`
57
57
  # => `StatsD.instrument('foo', 1, **method_call)`
@@ -68,17 +68,17 @@ module RuboCop
68
68
  when *POSITIONAL_ARGUMENT_TYPES
69
69
  value_argument = node.arguments[1]
70
70
  from = value_argument.source_range.end_pos
71
- to = positial_arguments.last.source_range.end_pos
71
+ to = positional_arguments.last.source_range.end_pos
72
72
  range = Parser::Source::Range.new(node.source_range.source_buffer, from, to)
73
73
  corrector.remove(range)
74
74
 
75
75
  keyword_arguments = []
76
- sample_rate = positial_arguments[0]
76
+ sample_rate = positional_arguments[0]
77
77
  if sample_rate && sample_rate.type != :nil
78
78
  keyword_arguments << "sample_rate: #{sample_rate.source}"
79
79
  end
80
80
 
81
- tags = positial_arguments[1]
81
+ tags = positional_arguments[1]
82
82
  if tags && tags.type != :nil
83
83
  keyword_arguments << if tags.type == :hash && tags.source[0] != '{'
84
84
  "tags: { #{tags.source} }"
@@ -5,7 +5,7 @@ require_relative '../rubocop' unless defined?(RuboCop::Cop::StatsD)
5
5
  module RuboCop
6
6
  module Cop
7
7
  module StatsD
8
- # This Rubocop will check for calls to StatsD singleton congfiguration methods
8
+ # This Rubocop will check for calls to StatsD singleton configuration methods
9
9
  # (e.g. `StatsD.prefix`). The library is moving away from having just a single
10
10
  # singleton client, so these methods are deprecated.
11
11
  #
@@ -15,7 +15,7 @@ module StatsD
15
15
  # - Only accept a position argument for value, rather than a keyword argument.
16
16
  # - The provided arguments have the right type.
17
17
  #
18
- # You can enable thois monkeypatch by changing your Gemfile as follows:
18
+ # You can enable this monkeypatch by changing your Gemfile as follows:
19
19
  #
20
20
  # gem 'statsd-instrument', require: 'statsd/instrument/strict'
21
21
  #
@@ -81,6 +81,11 @@ module StatsD
81
81
  super
82
82
  end
83
83
 
84
+ def key_value(*)
85
+ raise NotImplementedError, "The key_value metric type will be removed " \
86
+ "from the next major version of statsd-instrument"
87
+ end
88
+
84
89
  private
85
90
 
86
91
  def check_block_or_numeric_value(value)
@@ -99,49 +104,59 @@ module StatsD
99
104
  raise ArgumentError, "The tags argument should be a hash or an array, got #{tags.inspect}"
100
105
  end
101
106
  end
107
+ end
108
+
109
+ module VoidCollectMetric
110
+ protected
102
111
 
103
112
  def collect_metric(type, name, value, sample_rate:, tags: nil, prefix:, metadata: nil)
104
113
  super
105
- nil # We explicitly discard the return value, so people cannot depend on it.
114
+ StatsD::Instrument::VOID
106
115
  end
107
116
  end
108
117
 
109
118
  module StrictMetaprogramming
110
- def statsd_measure(method, name, sample_rate: nil, tags: nil, no_prefix: false)
119
+ def statsd_measure(method, name, sample_rate: nil, tags: nil,
120
+ no_prefix: false, client: StatsD.singleton_client)
121
+
111
122
  check_method_and_metric_name(method, name)
112
123
 
113
- # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
124
+ # Unfortunately, we have to inline the new method implementation because we have to fix the
114
125
  # Stats.measure call to not use the `as_dist` and `prefix` arguments.
115
126
  add_to_method(method, name, :measure) do
116
127
  define_method(method) do |*args, &block|
117
128
  key = StatsD::Instrument.generate_metric_name(nil, name, self, *args)
118
- StatsD.measure(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
129
+ client.measure(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
119
130
  super(*args, &block)
120
131
  end
121
132
  end
122
133
  end
123
134
  end
124
135
 
125
- def statsd_distribution(method, name, sample_rate: nil, tags: nil, no_prefix: false)
136
+ def statsd_distribution(method, name, sample_rate: nil, tags: nil,
137
+ no_prefix: false, client: StatsD.singleton_client)
138
+
126
139
  check_method_and_metric_name(method, name)
127
140
 
128
- # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
141
+ # Unfortunately, we have to inline the new method implementation because we have to fix the
129
142
  # Stats.distribution call to not use the `prefix` argument.
130
143
 
131
144
  add_to_method(method, name, :distribution) do
132
145
  define_method(method) do |*args, &block|
133
146
  key = StatsD::Instrument.generate_metric_name(nil, name, self, *args)
134
- StatsD.distribution(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
147
+ client.distribution(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix) do
135
148
  super(*args, &block)
136
149
  end
137
150
  end
138
151
  end
139
152
  end
140
153
 
141
- def statsd_count_success(method, name, sample_rate: nil, tags: nil, no_prefix: false)
154
+ def statsd_count_success(method, name, sample_rate: nil, tags: nil,
155
+ no_prefix: false, client: StatsD.singleton_client)
156
+
142
157
  check_method_and_metric_name(method, name)
143
158
 
144
- # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
159
+ # Unfortunately, we have to inline the new method implementation because we have to fix the
145
160
  # Stats.increment call to not use the `prefix` argument.
146
161
 
147
162
  add_to_method(method, name, :count_success) do
@@ -163,16 +178,18 @@ module StatsD
163
178
  ensure
164
179
  suffix = truthiness == false ? 'failure' : 'success'
165
180
  key = "#{StatsD::Instrument.generate_metric_name(nil, name, self, *args)}.#{suffix}"
166
- StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
181
+ client.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
167
182
  end
168
183
  end
169
184
  end
170
185
  end
171
186
 
172
- def statsd_count_if(method, name, sample_rate: nil, tags: nil, no_prefix: false)
187
+ def statsd_count_if(method, name, sample_rate: nil, tags: nil,
188
+ no_prefix: false, client: StatsD.singleton_client)
189
+
173
190
  check_method_and_metric_name(method, name)
174
191
 
175
- # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
192
+ # Unfortunately, we have to inline the new method implementation because we have to fix the
176
193
  # Stats.increment call to not use the `prefix` argument.
177
194
 
178
195
  add_to_method(method, name, :count_if) do
@@ -194,23 +211,25 @@ module StatsD
194
211
  ensure
195
212
  if truthiness
196
213
  key = StatsD::Instrument.generate_metric_name(nil, name, self, *args)
197
- StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
214
+ client.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
198
215
  end
199
216
  end
200
217
  end
201
218
  end
202
219
  end
203
220
 
204
- def statsd_count(method, name, sample_rate: nil, tags: nil, no_prefix: false)
221
+ def statsd_count(method, name, sample_rate: nil, tags: nil,
222
+ no_prefix: false, client: StatsD.singleton_client)
223
+
205
224
  check_method_and_metric_name(method, name)
206
225
 
207
- # Unfortunately, we have to inline the new method implementation ebcause we have to fix the
226
+ # Unfortunately, we have to inline the new method implementation because we have to fix the
208
227
  # Stats.increment call to not use the `prefix` argument.
209
228
 
210
229
  add_to_method(method, name, :count) do
211
230
  define_method(method) do |*args, &block|
212
231
  key = StatsD::Instrument.generate_metric_name(nil, name, self, *args)
213
- StatsD.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
232
+ client.increment(key, sample_rate: sample_rate, tags: tags, no_prefix: no_prefix)
214
233
  super(*args, &block)
215
234
  end
216
235
  end
@@ -232,4 +251,5 @@ module StatsD
232
251
  end
233
252
 
234
253
  StatsD.singleton_class.prepend(StatsD::Instrument::Strict)
254
+ StatsD::Instrument::LegacyClient.prepend(StatsD::Instrument::VoidCollectMetric)
235
255
  StatsD::Instrument.prepend(StatsD::Instrument::StrictMetaprogramming)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module StatsD
4
4
  module Instrument
5
- VERSION = "2.8.0"
5
+ VERSION = "2.9.0"
6
6
  end
7
7
  end