statsd-instrument 2.5.1 → 2.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1 -1
  3. data/.rubocop.yml +11 -6
  4. data/.yardopts +5 -0
  5. data/CHANGELOG.md +75 -6
  6. data/README.md +54 -46
  7. data/benchmark/datagram-client +41 -0
  8. data/lib/statsd/instrument/assertions.rb +168 -57
  9. data/lib/statsd/instrument/backends/udp_backend.rb +20 -29
  10. data/lib/statsd/instrument/capture_sink.rb +27 -0
  11. data/lib/statsd/instrument/client.rb +313 -0
  12. data/lib/statsd/instrument/datagram.rb +75 -0
  13. data/lib/statsd/instrument/datagram_builder.rb +101 -0
  14. data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +71 -0
  15. data/lib/statsd/instrument/environment.rb +106 -29
  16. data/lib/statsd/instrument/log_sink.rb +24 -0
  17. data/lib/statsd/instrument/null_sink.rb +13 -0
  18. data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +39 -0
  19. data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +6 -10
  20. data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +37 -0
  21. data/lib/statsd/instrument/rubocop/metric_return_value.rb +7 -6
  22. data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +11 -20
  23. data/lib/statsd/instrument/rubocop/positional_arguments.rb +13 -13
  24. data/lib/statsd/instrument/rubocop/splat_arguments.rb +8 -14
  25. data/lib/statsd/instrument/rubocop.rb +64 -0
  26. data/lib/statsd/instrument/statsd_datagram_builder.rb +14 -0
  27. data/lib/statsd/instrument/strict.rb +112 -22
  28. data/lib/statsd/instrument/udp_sink.rb +62 -0
  29. data/lib/statsd/instrument/version.rb +1 -1
  30. data/lib/statsd/instrument.rb +191 -100
  31. data/test/assertions_test.rb +139 -176
  32. data/test/capture_sink_test.rb +44 -0
  33. data/test/client_test.rb +164 -0
  34. data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +162 -0
  35. data/test/datagram_builder_test.rb +120 -0
  36. data/test/deprecations_test.rb +56 -10
  37. data/test/dogstatsd_datagram_builder_test.rb +32 -0
  38. data/test/environment_test.rb +73 -7
  39. data/test/log_sink_test.rb +37 -0
  40. data/test/null_sink_test.rb +13 -0
  41. data/test/rubocop/measure_as_dist_argument_test.rb +44 -0
  42. data/test/rubocop/metaprogramming_positional_arguments_test.rb +1 -1
  43. data/test/rubocop/metric_prefix_argument_test.rb +38 -0
  44. data/test/rubocop/metric_return_value_test.rb +1 -1
  45. data/test/rubocop/metric_value_keyword_argument_test.rb +1 -1
  46. data/test/rubocop/positional_arguments_test.rb +1 -1
  47. data/test/rubocop/splat_arguments_test.rb +1 -1
  48. data/test/statsd_datagram_builder_test.rb +22 -0
  49. data/test/statsd_instrumentation_test.rb +0 -24
  50. data/test/statsd_test.rb +0 -23
  51. data/test/test_helper.rb +0 -2
  52. data/test/udp_backend_test.rb +25 -8
  53. data/test/udp_sink_test.rb +85 -0
  54. metadata +38 -2
@@ -7,23 +7,10 @@ module StatsD::Instrument::Backends
7
7
  BASE_SUPPORTED_METRIC_TYPES = { c: true, ms: true, g: true, s: true }
8
8
 
9
9
  class DogStatsDProtocol
10
- EVENT_OPTIONS = {
11
- date_happened: 'd',
12
- hostname: 'h',
13
- aggregation_key: 'k',
14
- priority: 'p',
15
- source_type_name: 's',
16
- alert_type: 't',
17
- }
18
-
19
- SERVICE_CHECK_OPTIONS = {
20
- timestamp: 'd',
21
- hostname: 'h',
22
- message: 'm',
23
- }
24
-
25
10
  SUPPORTED_METRIC_TYPES = BASE_SUPPORTED_METRIC_TYPES.merge(h: true, _e: true, _sc: true, d: true)
26
11
 
12
+ SERVICE_CHECK_STATUSES = { ok: 0, warning: 1, critical: 2, unknown: 3 }
13
+
27
14
  def generate_packet(metric)
28
15
  packet = +""
29
16
 
@@ -32,27 +19,31 @@ module StatsD::Instrument::Backends
32
19
  escaped_text = metric.value.gsub("\n", "\\n")
33
20
 
34
21
  packet << "_e{#{escaped_title.size},#{escaped_text.size}}:#{escaped_title}|#{escaped_text}"
35
- 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
+
36
30
  elsif metric.type == :_sc
37
- packet << "_sc|#{metric.name}|#{metric.value}"
38
- 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
+
39
39
  else
40
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
41
43
  end
42
44
 
43
- packet << "|@#{metric.sample_rate}" if metric.sample_rate < 1
44
- packet << "|##{metric.tags.join(',')}" if metric.tags
45
-
46
45
  packet
47
46
  end
48
-
49
- private
50
-
51
- def generate_metadata(metric, options)
52
- (metric.metadata.keys & options.keys).map do |key|
53
- "|#{options[key]}:#{metric.metadata[key]}"
54
- end.join
55
- end
56
47
  end
57
48
 
58
49
  class StatsiteStatsDProtocol
@@ -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
@@ -0,0 +1,313 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'statsd/instrument/datagram'
4
+ require 'statsd/instrument/datagram_builder'
5
+ require 'statsd/instrument/statsd_datagram_builder'
6
+ require 'statsd/instrument/dogstatsd_datagram_builder'
7
+ require 'statsd/instrument/null_sink'
8
+ require 'statsd/instrument/udp_sink'
9
+ require 'statsd/instrument/capture_sink'
10
+ require 'statsd/instrument/log_sink'
11
+
12
+ # The Client is the main interface for using StatsD.
13
+ #
14
+ # @note This new new Client implementation that is intended to become the new default in
15
+ # the next major release of this library. While this class may already be functional,
16
+ # we provide no guarantees about the API and the behavior may change.
17
+ class StatsD::Instrument::Client
18
+ attr_reader :sink, :datagram_builder_class, :prefix, :default_tags, :default_sample_rate
19
+
20
+ def initialize(
21
+ sink: StatsD::Instrument::NullSink.new,
22
+ prefix: nil,
23
+ default_sample_rate: 1,
24
+ default_tags: nil,
25
+ datagram_builder_class: StatsD::Instrument::StatsDDatagramBuilder
26
+ )
27
+ @sink = sink
28
+ @datagram_builder_class = datagram_builder_class
29
+
30
+ @prefix = prefix
31
+ @default_tags = default_tags
32
+ @default_sample_rate = default_sample_rate
33
+
34
+ @datagram_builder = { false => nil, true => nil }
35
+ end
36
+
37
+ # @!group Metric Methods
38
+
39
+ # Emits a counter metric.
40
+ #
41
+ # You should use a counter metric to count the frequency of something happening. As a
42
+ # result, the value should generally be set to 1 (the default), unless you reporting
43
+ # about a batch of activity. E.g. `increment('messages.processed', messages.size)`
44
+ # For values that are not frequencies, you should use another metric type, e.g.
45
+ # {#histogram} or {#distribution}.
46
+ #
47
+ # @param name [String] The name of the metric.
48
+ #
49
+ # - We recommend using `snake_case.metric_names` as naming scheme.
50
+ # - A `.` should be used for namespacing, e.g. `foo.bar.baz`
51
+ # - A metric name should not include the following characters: `|`, `@`, and `:`.
52
+ # The library will convert these characters to `_`.
53
+ #
54
+ # @param value [Integer] (default: 1) The value to increment the counter by.
55
+ #
56
+ # You should not compensate for the sample rate using the counter increment. E.g., if
57
+ # your sample rate is set to `0.01`, you should not use 100 as increment to compensate
58
+ # for it. The sample rate is part of the packet that is being sent to the server, and
59
+ # the server should know how to compensate for it.
60
+ #
61
+ # @param [Float] sample_rate (default: `#default_sample_rate`) The rate at which to sample
62
+ # this metric call. This value should be between 0 and 1. This value can be used to reduce
63
+ # the amount of network I/O (and CPU cycles) is being used for very frequent metrics.
64
+ #
65
+ # - A value of `0.1` means that only 1 out of 10 calls will be emitted; the other 9 will
66
+ # be short-circuited.
67
+ # - When set to `1`, every metric will be emitted.
68
+ # - If this parameter is not set, the default sample rate for this client will be used.
69
+ #
70
+ # @param [Hash<Symbol, String>, Array<String>] tags (default: nil)
71
+ # @return [void]
72
+ def increment(name, value = 1, sample_rate: nil, tags: nil, no_prefix: false)
73
+ sample_rate ||= @default_sample_rate
74
+ return unless sample?(sample_rate)
75
+ emit(datagram_builder(no_prefix: no_prefix).c(name, value, sample_rate, tags))
76
+ end
77
+
78
+ # Emits a timing metric.
79
+ #
80
+ # @param name (see #increment)
81
+ # @param [Numeric] value The duration to record, in milliseconds.
82
+ # @param sample_rate (see #increment)
83
+ # @param tags (see #increment)
84
+ # @return [void]
85
+ def measure(name, value = nil, sample_rate: nil, tags: nil, no_prefix: false, &block)
86
+ if block_given?
87
+ return latency(name, sample_rate: sample_rate, tags: tags, metric_type: :ms, no_prefix: no_prefix, &block)
88
+ end
89
+
90
+ sample_rate ||= @default_sample_rate
91
+ return unless sample?(sample_rate)
92
+ emit(datagram_builder(no_prefix: no_prefix).ms(name, value, sample_rate, tags))
93
+ end
94
+
95
+ # Emits a gauge metric.
96
+ #
97
+ # You should use a gauge if you are reporting the current value of
98
+ # something that can only have one value at the time. E.g., the
99
+ # speed of your car. A newly reported value will repla e the previously
100
+ # reported value.
101
+ #
102
+ #
103
+ # @param name (see #increment)
104
+ # @param [Numeric] value The gauged value.
105
+ # @param sample_rate (see #increment)
106
+ # @param tags (see #increment)
107
+ # @return [void]
108
+ def gauge(name, value, sample_rate: nil, tags: nil, no_prefix: false)
109
+ sample_rate ||= @default_sample_rate
110
+ return unless sample?(sample_rate)
111
+ emit(datagram_builder(no_prefix: no_prefix).g(name, value, sample_rate, tags))
112
+ end
113
+
114
+ # Emits a set metric, which counts distinct values.
115
+ #
116
+ # @param name (see #increment)
117
+ # @param [Numeric, String] value The value to count for distinct occurrences.
118
+ # @param sample_rate (see #increment)
119
+ # @param tags (see #increment)
120
+ # @return [void]
121
+ def set(name, value, sample_rate: nil, tags: nil, no_prefix: false)
122
+ sample_rate ||= @default_sample_rate
123
+ return unless sample?(sample_rate)
124
+ emit(datagram_builder(no_prefix: no_prefix).s(name, value, sample_rate, tags))
125
+ end
126
+
127
+ # Emits a distribution metric, which builds a histogram of the reported
128
+ # values.
129
+ #
130
+ # @note The distribution metric type is not available on all implementations.
131
+ # A `NotImplemetedError` will be raised if you call this method, but
132
+ # the active implementation does not support it.
133
+ #
134
+ # @param name (see #increment)
135
+ # @param [Numeric] value The value to include in the distribution histogram.
136
+ # @param sample_rate (see #increment)
137
+ # @param tags (see #increment)
138
+ # @return [void]
139
+ def distribution(name, value = nil, sample_rate: nil, tags: nil, no_prefix: false, &block)
140
+ if block_given?
141
+ return latency(name, sample_rate: sample_rate, tags: tags, metric_type: :d, no_prefix: no_prefix, &block)
142
+ end
143
+
144
+ sample_rate ||= @default_sample_rate
145
+ return unless sample?(sample_rate)
146
+ emit(datagram_builder(no_prefix: no_prefix).d(name, value, sample_rate, tags))
147
+ end
148
+
149
+ # Emits a histogram metric, which builds a histogram of the reported values.
150
+ #
151
+ # @note The histogram metric type is not available on all implementations.
152
+ # A `NotImplemetedError` will be raised if you call this method, but
153
+ # the active implementation does not support it.
154
+ #
155
+ # @param name (see #increment)
156
+ # @param [Numeric] value The value to include in the histogram.
157
+ # @param sample_rate (see #increment)
158
+ # @param tags (see #increment)
159
+ # @return [void]
160
+ def histogram(name, value, sample_rate: nil, tags: nil, no_prefix: false)
161
+ sample_rate ||= @default_sample_rate
162
+ return unless sample?(sample_rate)
163
+ emit(datagram_builder(no_prefix: no_prefix).h(name, value, sample_rate, tags))
164
+ end
165
+
166
+ # @!endgroup
167
+
168
+ # Measures the latency of the given block in milliseconds, and emits it as a metric.
169
+ #
170
+ # @param name (see #increment)
171
+ # @param sample_rate (see #increment)
172
+ # @param tags (see #increment)
173
+ # @param [Symbol] metric_type The metric type to use. If not specified, we will
174
+ # use the preferred metric type of the implementation. The default is `:ms`.
175
+ # Generally, you should not have to set this.
176
+ # @yield The latency (execution time) of the block
177
+ # @return The return value of the proivded block will be passed through.
178
+ def latency(name, sample_rate: nil, tags: nil, metric_type: nil, no_prefix: false)
179
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
180
+ begin
181
+ yield
182
+ ensure
183
+ stop = Process.clock_gettime(Process::CLOCK_MONOTONIC)
184
+
185
+ sample_rate ||= @default_sample_rate
186
+ if sample?(sample_rate)
187
+ metric_type ||= datagram_builder(no_prefix: no_prefix).latency_metric_type
188
+ latency_in_ms = 1000.0 * (stop - start)
189
+ emit(datagram_builder(no_prefix: no_prefix).send(metric_type, name, latency_in_ms, sample_rate, tags))
190
+ end
191
+ end
192
+ end
193
+
194
+ # Emits a service check.
195
+ #
196
+ # @param [String] title Event title.
197
+ # @param [String] text Event description. Newlines are allowed.
198
+ # @param [Time] timestamp The of the event. If not provided,
199
+ # Datadog will interpret it as the current timestamp.
200
+ # @param [String] hostname A hostname to associate with the event.
201
+ # @param [String] aggregation_key An aggregation key to group events with the same key.
202
+ # @param [String] priority Priority of the event. Either "normal" (default) or "low".
203
+ # @param [String] source_type_name The source type of the event.
204
+ # @param [String] alert_type Either "error", "warning", "info" (default) or "success".
205
+ # @param [Array, Hash] tags Tags to associate with the event.
206
+ # @return [void]
207
+ #
208
+ # @note Supported by the Datadog implementation only.
209
+ def service_check(name, status, timestamp: nil, hostname: nil, tags: nil, message: nil, no_prefix: false)
210
+ emit(datagram_builder(no_prefix: no_prefix)._sc(name, status,
211
+ timestamp: timestamp, hostname: hostname, tags: tags, message: message))
212
+ end
213
+
214
+ # Emits an event.
215
+ #
216
+ # @param [String] name Name of the service
217
+ # @param [Symbol] status Either `:ok`, `:warning`, `:critical` or `:unknown`
218
+ # @param [Time] timestamp The moment when the service was checked. If not provided,
219
+ # Datadog will interpret it as the current timestamp.
220
+ # @param [String] hostname A hostname to associate with the check.
221
+ # @param [Array, Hash] tags Tags to associate with the check.
222
+ # @param [String] message A message describing the current state of the service check.
223
+ # @return [void]
224
+ #
225
+ # @note Supported by the Datadog implementation only.
226
+ def event(title, text, timestamp: nil, hostname: nil, aggregation_key: nil, priority: nil,
227
+ source_type_name: nil, alert_type: nil, tags: nil, no_prefix: false)
228
+
229
+ emit(datagram_builder(no_prefix: no_prefix)._e(title, text, timestamp: timestamp,
230
+ hostname: hostname, tags: tags, aggregation_key: aggregation_key, priority: priority,
231
+ source_type_name: source_type_name, alert_type: alert_type))
232
+ end
233
+
234
+ # Instantiates a new StatsD client that uses the settings of the current client,
235
+ # except for the provided overrides.
236
+ #
237
+ # @yield [client] A new client will be constructed with the altered settings, and
238
+ # yielded to the block. The original client will not be affected. The new client
239
+ # will be disposed after the block returns
240
+ # @return The return value of the block will be passed on as return value.
241
+ def with_options(
242
+ sink: nil,
243
+ prefix: nil,
244
+ default_sample_rate: nil,
245
+ default_tags: nil,
246
+ datagram_builder_class: nil
247
+ )
248
+ client = clone_with_options(sink: sink, prefix: prefix,
249
+ default_sample_rate: default_sample_rate, default_tags: default_tags,
250
+ datagram_builder_class: datagram_builder_class)
251
+
252
+ yield(client)
253
+ end
254
+
255
+ def clone_with_options(
256
+ sink: nil,
257
+ prefix: nil,
258
+ default_sample_rate: nil,
259
+ default_tags: nil,
260
+ datagram_builder_class: nil
261
+ )
262
+ self.class.new(
263
+ sink: sink || @sink,
264
+ prefix: prefix || @prefix,
265
+ default_sample_rate: default_sample_rate || @default_sample_rate,
266
+ default_tags: default_tags || @default_tags,
267
+ datagram_builder_class: datagram_builder_class || @datagram_builder_class,
268
+ )
269
+ end
270
+
271
+ def capture_sink
272
+ StatsD::Instrument::CaptureSink.new(
273
+ parent: @sink,
274
+ datagram_class: datagram_builder(no_prefix: false).datagram_class,
275
+ )
276
+ end
277
+
278
+ def with_capture_sink(capture_sink)
279
+ @sink = capture_sink
280
+ yield
281
+ ensure
282
+ @sink = @sink.parent
283
+ end
284
+
285
+ # Captures metrics that were emitted during the provided block.
286
+ #
287
+ # @yield During the execution of the provided block, metrics will be captured.
288
+ # @return [Array<StatsD::Instagram::Datagram>] The list of metrics that were
289
+ # emitted during the block, in the same order in which they were emitted.
290
+ def capture(&block)
291
+ sink = capture_sink
292
+ with_capture_sink(sink, &block)
293
+ sink.datagrams
294
+ end
295
+
296
+ protected
297
+
298
+ def datagram_builder(no_prefix:)
299
+ @datagram_builder[no_prefix] ||= @datagram_builder_class.new(
300
+ prefix: no_prefix ? nil : prefix,
301
+ default_tags: default_tags,
302
+ )
303
+ end
304
+
305
+ def sample?(sample_rate)
306
+ @sink.sample?(sample_rate)
307
+ end
308
+
309
+ def emit(datagram)
310
+ @sink << datagram
311
+ nil
312
+ end
313
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The Datagram class parses and inspects a StatsD datagrans
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::Datagram
8
+ attr_reader :source
9
+
10
+ def initialize(source)
11
+ @source = source
12
+ end
13
+
14
+ # @return [Float] The sample rate at which this datagram was emitted, between 0 and 1.
15
+ def sample_rate
16
+ parsed_datagram[:sample_rate] ? Float(parsed_datagram[:sample_rate]) : 1.0
17
+ end
18
+
19
+ def type
20
+ parsed_datagram[:type]
21
+ end
22
+
23
+ def name
24
+ parsed_datagram[:name]
25
+ end
26
+
27
+ def value
28
+ parsed_datagram[:value]
29
+ end
30
+
31
+ def tags
32
+ @tags ||= parsed_datagram[:tags] ? parsed_datagram[:tags].split(',') : nil
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class.name}:\"#{@source}\">"
37
+ end
38
+
39
+ def hash
40
+ source.hash
41
+ end
42
+
43
+ def eql?(other)
44
+ case other
45
+ when StatsD::Instrument::Datagram
46
+ source == other.source
47
+ when String
48
+ source == other
49
+ else
50
+ false
51
+ end
52
+ end
53
+
54
+ alias_method :==, :eql?
55
+
56
+ private
57
+
58
+ PARSER = %r{
59
+ \A
60
+ (?<name>[^\:\|\@]+)\:(?<value>[^\:\|\@]+)\|(?<type>c|ms|g|s|h|d)
61
+ (?:\|\@(?<sample_rate>\d*(?:\.\d*)?))?
62
+ (?:\|\#(?<tags>(?:[^\|\#,]+(?:,[^\|\#,]+)*)))?
63
+ \n? # In some implementations, the datagram may include a trailing newline.
64
+ \z
65
+ }x
66
+ private_constant :PARSER
67
+
68
+ def parsed_datagram
69
+ @parsed ||= if (match_info = PARSER.match(@source))
70
+ match_info
71
+ else
72
+ raise ArgumentError, "Invalid StatsD datagram: #{@source}"
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,101 @@
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::DatagramBuilder
6
+ unless Regexp.method_defined?(:match?) # for ruby 2.3
7
+ module RubyBackports
8
+ refine Regexp do
9
+ def match?(str)
10
+ match(str) != nil
11
+ end
12
+ end
13
+ end
14
+
15
+ using RubyBackports
16
+ end
17
+
18
+ def self.unsupported_datagram_types(*types)
19
+ types.each do |type|
20
+ define_method(type) do |_, _, _, _|
21
+ raise NotImplementedError, "Type #{type} metrics are not suppered by #{self.class.name}."
22
+ end
23
+ end
24
+ end
25
+
26
+ def initialize(prefix: nil, default_tags: nil)
27
+ @prefix = prefix.nil? ? "" : "#{normalize_name(prefix)}."
28
+ @default_tags = normalize_tags(default_tags)
29
+ end
30
+
31
+ def c(name, value, sample_rate, tags)
32
+ generate_generic_datagram(name, value, 'c', sample_rate, tags)
33
+ end
34
+
35
+ def g(name, value, sample_rate, tags)
36
+ generate_generic_datagram(name, value, 'g', sample_rate, tags)
37
+ end
38
+
39
+ def ms(name, value, sample_rate, tags)
40
+ generate_generic_datagram(name, value, 'ms', sample_rate, tags)
41
+ end
42
+
43
+ def s(name, value, sample_rate, tags)
44
+ generate_generic_datagram(name, value, 's', sample_rate, tags)
45
+ end
46
+
47
+ def h(name, value, sample_rate, tags)
48
+ generate_generic_datagram(name, value, 'h', sample_rate, tags)
49
+ end
50
+
51
+ def d(name, value, sample_rate, tags)
52
+ generate_generic_datagram(name, value, 'd', sample_rate, tags)
53
+ end
54
+
55
+ def kv(name, value, sample_rate, tags)
56
+ generate_generic_datagram(name, value, 'kv', sample_rate, tags)
57
+ end
58
+
59
+ def datagram_class
60
+ StatsD::Instrument::Datagram
61
+ end
62
+
63
+ def latency_metric_type
64
+ :ms
65
+ end
66
+
67
+ protected
68
+
69
+ attr_reader :prefix, :default_tags
70
+
71
+ # Utility function to convert tags to the canonical form.
72
+ #
73
+ # - Tags specified as key value pairs will be converted into an array
74
+ # - Tags are normalized to remove unsupported characters
75
+ #
76
+ # @param tags [Array<String>, Hash<String, String>, nil] Tags specified in any form.
77
+ # @return [Array<String>, nil] the list of tags in canonical form.
78
+ def normalize_tags(tags)
79
+ return [] unless tags
80
+ tags = tags.map { |k, v| "#{k}:#{v}" } if tags.is_a?(Hash)
81
+
82
+ # Fast path when no string replacement is needed
83
+ return tags unless tags.any? { |tag| /[|,]/.match?(tag) }
84
+ tags.map { |tag| tag.tr('|,', '') }
85
+ end
86
+
87
+ # Utility function to remove invalid characters from a StatsD metric name
88
+ def normalize_name(name)
89
+ # Fast path when no normalization is needed to avoid copying the string
90
+ return name unless /[:|@]/.match?(name)
91
+ name.tr(':|@', '_')
92
+ end
93
+
94
+ def generate_generic_datagram(name, value, type, sample_rate, tags)
95
+ tags = normalize_tags(tags) + default_tags
96
+ datagram = +"#{@prefix}#{normalize_name(name)}:#{value}|#{type}"
97
+ datagram << "|@#{sample_rate}" if sample_rate && sample_rate < 1
98
+ datagram << "|##{tags.join(',')}" unless tags.empty?
99
+ datagram
100
+ end
101
+ end
@@ -0,0 +1,71 @@
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::DogStatsDDatagramBuilder < StatsD::Instrument::DatagramBuilder
6
+ unsupported_datagram_types :kv
7
+
8
+ def latency_metric_type
9
+ :d
10
+ end
11
+
12
+ # Constricts an event datagram.
13
+ #
14
+ # @param [String] title Event title.
15
+ # @param [String] text Event description. Newlines are allowed.
16
+ # @param [Time] timestamp The of the event. If not provided,
17
+ # Datadog will interpret it as the current timestamp.
18
+ # @param [String] hostname A hostname to associate with the event.
19
+ # @param [String] aggregation_key An aggregation key to group events with the same key.
20
+ # @param [String] priority Priority of the event. Either "normal" (default) or "low".
21
+ # @param [String] source_type_name The source type of the event.
22
+ # @param [String] alert_type Either "error", "warning", "info" (default) or "success".
23
+ # @param [Array, Hash] tags Tags to associate with the event.
24
+ # @return [String] The correctly formatted service check datagram
25
+ #
26
+ # @see https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/#events
27
+ def _e(title, text, timestamp: nil, hostname: nil, aggregation_key: nil, priority: nil,
28
+ source_type_name: nil, alert_type: nil, tags: nil)
29
+
30
+ escaped_title = "#{@prefix}#{title}".gsub("\n", '\n')
31
+ escaped_text = text.gsub("\n", '\n')
32
+ tags = normalize_tags(tags) + default_tags
33
+
34
+ datagram = +"_e{#{escaped_title.length},#{escaped_text.length}}:#{escaped_title}|#{escaped_text}"
35
+ datagram << "|h:#{hostname}" if hostname
36
+ datagram << "|d:#{timestamp.to_i}" if timestamp
37
+ datagram << "|k:#{aggregation_key}" if aggregation_key
38
+ datagram << "|p:#{priority}" if priority
39
+ datagram << "|s:#{source_type_name}" if source_type_name
40
+ datagram << "|t:#{alert_type}" if alert_type
41
+ datagram << "|##{tags.join(',')}" unless tags.empty?
42
+ datagram
43
+ end
44
+
45
+ # Constricts a service check datagram.
46
+ #
47
+ # @param [String] name Name of the service
48
+ # @param [Symbol] status Either `:ok`, `:warning`, `:critical` or `:unknown`
49
+ # @param [Time] timestamp The moment when the service was checked. If not provided,
50
+ # Datadog will interpret it as the current timestamp.
51
+ # @param [String] hostname A hostname to associate with the check.
52
+ # @param [Array, Hash] tags Tags to associate with the check.
53
+ # @param [String] message A message describing the current state of the service check.
54
+ # @return [String] The correctly formatted service check datagram
55
+ #
56
+ # @see https://docs.datadoghq.com/developers/dogstatsd/datagram_shell/#service-checks
57
+ def _sc(name, status, timestamp: nil, hostname: nil, tags: nil, message: nil)
58
+ status_number = status.is_a?(Integer) ? status : SERVICE_CHECK_STATUS_VALUES.fetch(status.to_sym)
59
+ tags = normalize_tags(tags) + default_tags
60
+
61
+ datagram = +"_sc|#{@prefix}#{normalize_name(name)}|#{status_number}"
62
+ datagram << "|h:#{hostname}" if hostname
63
+ datagram << "|d:#{timestamp.to_i}" if timestamp
64
+ datagram << "|##{tags.join(',')}" unless tags.empty?
65
+ datagram << "|m:#{normalize_name(message)}" if message
66
+ datagram
67
+ end
68
+
69
+ SERVICE_CHECK_STATUS_VALUES = { ok: 0, warning: 1, critical: 2, unknown: 3 }.freeze
70
+ private_constant :SERVICE_CHECK_STATUS_VALUES
71
+ end