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.
- checksums.yaml +4 -4
- data/.rubocop-https---shopify-github-io-ruby-style-guide-rubocop-yml +1 -1
- data/.rubocop.yml +11 -6
- data/.yardopts +5 -0
- data/CHANGELOG.md +75 -6
- data/README.md +54 -46
- data/benchmark/datagram-client +41 -0
- data/lib/statsd/instrument/assertions.rb +168 -57
- data/lib/statsd/instrument/backends/udp_backend.rb +20 -29
- data/lib/statsd/instrument/capture_sink.rb +27 -0
- data/lib/statsd/instrument/client.rb +313 -0
- data/lib/statsd/instrument/datagram.rb +75 -0
- data/lib/statsd/instrument/datagram_builder.rb +101 -0
- data/lib/statsd/instrument/dogstatsd_datagram_builder.rb +71 -0
- data/lib/statsd/instrument/environment.rb +106 -29
- data/lib/statsd/instrument/log_sink.rb +24 -0
- data/lib/statsd/instrument/null_sink.rb +13 -0
- data/lib/statsd/instrument/rubocop/measure_as_dist_argument.rb +39 -0
- data/lib/statsd/instrument/rubocop/metaprogramming_positional_arguments.rb +6 -10
- data/lib/statsd/instrument/rubocop/metric_prefix_argument.rb +37 -0
- data/lib/statsd/instrument/rubocop/metric_return_value.rb +7 -6
- data/lib/statsd/instrument/rubocop/metric_value_keyword_argument.rb +11 -20
- data/lib/statsd/instrument/rubocop/positional_arguments.rb +13 -13
- data/lib/statsd/instrument/rubocop/splat_arguments.rb +8 -14
- data/lib/statsd/instrument/rubocop.rb +64 -0
- data/lib/statsd/instrument/statsd_datagram_builder.rb +14 -0
- data/lib/statsd/instrument/strict.rb +112 -22
- data/lib/statsd/instrument/udp_sink.rb +62 -0
- data/lib/statsd/instrument/version.rb +1 -1
- data/lib/statsd/instrument.rb +191 -100
- data/test/assertions_test.rb +139 -176
- data/test/capture_sink_test.rb +44 -0
- data/test/client_test.rb +164 -0
- data/test/compatibility/dogstatsd_datagram_compatibility_test.rb +162 -0
- data/test/datagram_builder_test.rb +120 -0
- data/test/deprecations_test.rb +56 -10
- data/test/dogstatsd_datagram_builder_test.rb +32 -0
- data/test/environment_test.rb +73 -7
- data/test/log_sink_test.rb +37 -0
- data/test/null_sink_test.rb +13 -0
- data/test/rubocop/measure_as_dist_argument_test.rb +44 -0
- data/test/rubocop/metaprogramming_positional_arguments_test.rb +1 -1
- data/test/rubocop/metric_prefix_argument_test.rb +38 -0
- data/test/rubocop/metric_return_value_test.rb +1 -1
- data/test/rubocop/metric_value_keyword_argument_test.rb +1 -1
- data/test/rubocop/positional_arguments_test.rb +1 -1
- data/test/rubocop/splat_arguments_test.rb +1 -1
- data/test/statsd_datagram_builder_test.rb +22 -0
- data/test/statsd_instrumentation_test.rb +0 -24
- data/test/statsd_test.rb +0 -23
- data/test/test_helper.rb +0 -2
- data/test/udp_backend_test.rb +25 -8
- data/test/udp_sink_test.rb +85 -0
- 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 <<
|
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
|
-
|
38
|
-
|
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
|