statsd-instrument 2.5.1 → 2.6.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 (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