datadog-statsd-schema 0.1.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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +20 -0
- data/.rubocop_todo.yml +227 -0
- data/CHANGELOG.md +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +360 -0
- data/Rakefile +29 -0
- data/examples/README.md +44 -0
- data/examples/schema_emitter.png +0 -0
- data/examples/schema_emitter.rb +54 -0
- data/examples/shared.rb +43 -0
- data/examples/simple_emitter.rb +23 -0
- data/exe/datadog-statsd-schema +3 -0
- data/lib/datadog/statsd/emitter.rb +471 -0
- data/lib/datadog/statsd/schema/errors.rb +35 -0
- data/lib/datadog/statsd/schema/metric_definition.rb +106 -0
- data/lib/datadog/statsd/schema/namespace.rb +200 -0
- data/lib/datadog/statsd/schema/schema_builder.rb +227 -0
- data/lib/datadog/statsd/schema/tag_definition.rb +71 -0
- data/lib/datadog/statsd/schema/version.rb +9 -0
- data/lib/datadog/statsd/schema.rb +71 -0
- metadata +192 -0
data/examples/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Examples
|
2
|
+
|
3
|
+
To run the examples, inspect them first, then run from the project's root:
|
4
|
+
|
5
|
+
```bash
|
6
|
+
bundle install
|
7
|
+
bundle exec examples/simple_emitter.rb
|
8
|
+
```
|
9
|
+
|
10
|
+
This should print what would have been methods on $statsd instance (of type `Datadog::Statsd`)
|
11
|
+
|
12
|
+
Now let's run the schema example, which first sends several metrics that follow the schema, and then send one that does not:
|
13
|
+
|
14
|
+
```bash
|
15
|
+
bundle exec examples/schema_emitter.rb
|
16
|
+
```
|
17
|
+
|
18
|
+
Since the script prints parseable Ruby to STDOUT, and errors to STDERR, you could make the output a bit more colorful by installing a `bat` tool (on MacOS) or `batcat` on Linux:
|
19
|
+
|
20
|
+
## Linux
|
21
|
+
|
22
|
+
```bash
|
23
|
+
$ sudo apt-get update -yqq && sudo apt-get install batcat
|
24
|
+
```
|
25
|
+
|
26
|
+
## MacOS
|
27
|
+
|
28
|
+
```bash
|
29
|
+
brew install bat
|
30
|
+
```
|
31
|
+
|
32
|
+
And then you can run it like so:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
# Linux
|
36
|
+
bundle exec examples/schema_emitter.rb | batcat --language=ruby
|
37
|
+
|
38
|
+
# MacOS
|
39
|
+
bundle exec examples/schema_emitter.rb | bat --language=ruby
|
40
|
+
```
|
41
|
+
|
42
|
+
You should see something like this:
|
43
|
+
|
44
|
+
]
|
Binary file
|
@@ -0,0 +1,54 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# vim: ft=ruby
|
5
|
+
|
6
|
+
require_relative "shared"
|
7
|
+
|
8
|
+
# This schema defines a namespace for marathon metrics and tags.
|
9
|
+
marathon_schema = Datadog.schema do
|
10
|
+
namespace "marathon" do
|
11
|
+
namespace "started" do
|
12
|
+
tags do
|
13
|
+
tag :course, values: %w[sf-marathon new-york]
|
14
|
+
tag :length, values: [26.212, 42.195]
|
15
|
+
tag :units, values: %w[miles km]
|
16
|
+
end
|
17
|
+
metrics do
|
18
|
+
counter "total", description: "Marathon started"
|
19
|
+
distribution "duration", description: "Marathon duration"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
emitter = Datadog.emitter(
|
26
|
+
"simple_emitter",
|
27
|
+
schema: marathon_schema,
|
28
|
+
validation_mode: :strict,
|
29
|
+
tags: {
|
30
|
+
course: "sf-marathon",
|
31
|
+
length: 26.212,
|
32
|
+
units: "miles"
|
33
|
+
}
|
34
|
+
)
|
35
|
+
|
36
|
+
emitter.increment("marathon.started.total", by: 3, tags: { course: "new-york" })
|
37
|
+
emitter.increment("marathon.started.total", by: 8, tags: { course: "new-york" })
|
38
|
+
|
39
|
+
emitter.distribution("marathon.started.duration", 43.13, tags: { course: "new-york" })
|
40
|
+
emitter.distribution("marathon.started.duration", 41.01, tags: { course: "new-york" })
|
41
|
+
|
42
|
+
def send_invalid
|
43
|
+
yield
|
44
|
+
rescue Datadog::Statsd::Schema::SchemaError
|
45
|
+
# ignore
|
46
|
+
end
|
47
|
+
|
48
|
+
send_invalid do
|
49
|
+
emitter.distribution("marathon.finished.duration", 21.23, tags: { course: "new-york" })
|
50
|
+
end
|
51
|
+
|
52
|
+
send_invalid do
|
53
|
+
emitter.distribution("marathon.started.duration", 21.23, tags: { course: "austin" })
|
54
|
+
end
|
data/examples/shared.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# vim: ft=ruby
|
5
|
+
|
6
|
+
require "bundler/setup"
|
7
|
+
require "datadog/statsd/schema"
|
8
|
+
|
9
|
+
require "git"
|
10
|
+
require "etc"
|
11
|
+
require "datadog/statsd"
|
12
|
+
require "amazing_print"
|
13
|
+
|
14
|
+
STATSD = Datadog::Statsd.new(
|
15
|
+
"localhost", 8125
|
16
|
+
)
|
17
|
+
|
18
|
+
class FakeStatsd # rubocop:disable Style/Documentation
|
19
|
+
def initialize(...); end
|
20
|
+
|
21
|
+
def method_missing(m, *args, **opts)
|
22
|
+
puts "$statsd.#{m}(\n '#{args.first}',#{args.drop(1).join(", ")}#{args.size > 1 ? "," : ""}\n #{opts.inspect} -> { #{if block_given?
|
23
|
+
yield
|
24
|
+
end} } "
|
25
|
+
end
|
26
|
+
|
27
|
+
def respond_to_missing?(m, *)
|
28
|
+
STATSD.respond_to?(m)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
FAKE_STATSD = FakeStatsd.new
|
33
|
+
|
34
|
+
Datadog::Statsd::Schema.configure do |config|
|
35
|
+
# This configures the global tags that will be attached to all methods
|
36
|
+
config.tags = {
|
37
|
+
env: "development",
|
38
|
+
arch: Etc.uname[:machine],
|
39
|
+
version: Git.open(".").object("HEAD").sha
|
40
|
+
}
|
41
|
+
|
42
|
+
config.statsd = FAKE_STATSD
|
43
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# vim: ft=ruby
|
5
|
+
|
6
|
+
require_relative "shared"
|
7
|
+
|
8
|
+
# This emitter has no schema and allows any combination of metrics and tags.
|
9
|
+
emitter = Datadog.emitter(
|
10
|
+
"simple_emitter",
|
11
|
+
metric: "marathon.started",
|
12
|
+
tags: {
|
13
|
+
course: "sf-marathon",
|
14
|
+
length: 26.212,
|
15
|
+
units: "miles"
|
16
|
+
}
|
17
|
+
)
|
18
|
+
|
19
|
+
emitter.increment("total", by: 3, tags: { course: "new-york" })
|
20
|
+
emitter.increment("total", by: 8, tags: { course: "new-york" })
|
21
|
+
|
22
|
+
emitter.distribution("duration", 43.13, tags: { course: "new-york" })
|
23
|
+
emitter.distribution("duration", 41.01, tags: { course: "new-york" })
|
@@ -0,0 +1,471 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
require "datadog/statsd"
|
5
|
+
require "ostruct"
|
6
|
+
require "active_support/core_ext/string/inflections"
|
7
|
+
|
8
|
+
# Load colored2 for error formatting if available
|
9
|
+
begin
|
10
|
+
require "colored2"
|
11
|
+
rescue LoadError
|
12
|
+
# colored2 not available, use plain text
|
13
|
+
end
|
14
|
+
|
15
|
+
# Load schema classes if available
|
16
|
+
begin
|
17
|
+
require_relative "schema"
|
18
|
+
require_relative "schema/namespace"
|
19
|
+
require_relative "schema/errors"
|
20
|
+
rescue LoadError
|
21
|
+
# Schema classes not available, validation will be skipped
|
22
|
+
end
|
23
|
+
|
24
|
+
module Datadog
|
25
|
+
class Statsd
|
26
|
+
class Emitter
|
27
|
+
MUTEX = Mutex.new
|
28
|
+
|
29
|
+
DEFAULT_HOST = "127.0.0.1"
|
30
|
+
DEFAULT_PORT = 8125
|
31
|
+
DEFAULT_NAMESPACE = nil
|
32
|
+
DEFAULT_ARGUMENTS = { delay_serialization: true }
|
33
|
+
DEFAULT_SAMPLE_RATE = 1.0
|
34
|
+
DEFAULT_VALIDATION_MODE = :strict
|
35
|
+
|
36
|
+
# @description This class is a wrapper around the Datadog::Statsd class. It provides a
|
37
|
+
# simple interface for sending metrics to Datadog. It also supports AB testing.
|
38
|
+
# When initialized with a schema, it validates metrics and tags against the schema.
|
39
|
+
#
|
40
|
+
# @see Datadog::Statsd::Emitter.new for more details.
|
41
|
+
#
|
42
|
+
class << self
|
43
|
+
attr_accessor :datadog_statsd
|
44
|
+
|
45
|
+
# @return [Datadog::Statsd, NilClass] The Datadog Statsd client instance or nil if not
|
46
|
+
# currently connected.
|
47
|
+
def statsd
|
48
|
+
return @datadog_statsd if defined?(@datadog_statsd)
|
49
|
+
|
50
|
+
@datadog_statsd = ::Datadog::Statsd::Schema.configuration.statsd
|
51
|
+
end
|
52
|
+
|
53
|
+
extend Forwardable
|
54
|
+
def_delegators :datadog_statsd,
|
55
|
+
:increment,
|
56
|
+
:decrement,
|
57
|
+
:gauge,
|
58
|
+
:histogram,
|
59
|
+
:distribution,
|
60
|
+
:set,
|
61
|
+
:flush
|
62
|
+
|
63
|
+
def global_tags
|
64
|
+
@global_tags ||= OpenStruct.new
|
65
|
+
end
|
66
|
+
|
67
|
+
def configure
|
68
|
+
yield(global_tags)
|
69
|
+
end
|
70
|
+
|
71
|
+
def connect(
|
72
|
+
host: DEFAULT_HOST,
|
73
|
+
port: DEFAULT_PORT,
|
74
|
+
tags: {},
|
75
|
+
sample_rate: DEFAULT_SAMPLE_RATE,
|
76
|
+
namespace: DEFAULT_NAMESPACE,
|
77
|
+
**opts
|
78
|
+
)
|
79
|
+
return @datadog_statsd if defined?(@datadog_statsd) && @datadog_statsd
|
80
|
+
|
81
|
+
tags ||= {}
|
82
|
+
tags = tags.merge(global_tags.to_h)
|
83
|
+
tags = tags.map { |k, v| "#{k}:#{v}" }
|
84
|
+
|
85
|
+
opts ||= {}
|
86
|
+
# Remove any unknown parameters that Datadog::Statsd doesn't support
|
87
|
+
opts = opts.except(:emitter) if opts.key?(:emitter)
|
88
|
+
opts = DEFAULT_ARGUMENTS.merge(opts)
|
89
|
+
|
90
|
+
MUTEX.synchronize do
|
91
|
+
unless defined?(@datadog_statsd)
|
92
|
+
@datadog_statsd =
|
93
|
+
::Datadog::Statsd.new(host, port, namespace:, tags:, sample_rate:, **opts)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
yield(datadog_statsd) if block_given?
|
98
|
+
end
|
99
|
+
|
100
|
+
def close
|
101
|
+
begin
|
102
|
+
@datadog_statsd&.close
|
103
|
+
rescue StandardError
|
104
|
+
nil
|
105
|
+
end
|
106
|
+
@datadog_statsd = nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
attr_reader :tags, :ab_test, :sample_rate, :metric, :schema, :validation_mode
|
111
|
+
|
112
|
+
def initialize(
|
113
|
+
emitter = nil,
|
114
|
+
metric: nil,
|
115
|
+
tags: nil,
|
116
|
+
ab_test: nil,
|
117
|
+
sample_rate: nil,
|
118
|
+
schema: nil,
|
119
|
+
validation_mode: DEFAULT_VALIDATION_MODE
|
120
|
+
)
|
121
|
+
if emitter.nil? && metric.nil? && tags.nil? && ab_test.nil? && sample_rate.nil? && schema.nil?
|
122
|
+
raise ArgumentError,
|
123
|
+
"Datadog::Statsd::Emitter: use class methods if you are passing nothing to the constructor."
|
124
|
+
end
|
125
|
+
@sample_rate = sample_rate || 1.0
|
126
|
+
@tags = tags || nil
|
127
|
+
@tags.merge!(self.class.global_tags.to_h) if self.class.global_tags.present?
|
128
|
+
|
129
|
+
@ab_test = ab_test || {}
|
130
|
+
@metric = metric
|
131
|
+
@schema = schema
|
132
|
+
@validation_mode = validation_mode
|
133
|
+
|
134
|
+
emitter =
|
135
|
+
case emitter
|
136
|
+
when String, Symbol
|
137
|
+
emitter.to_s
|
138
|
+
when Module, Class
|
139
|
+
emitter.name
|
140
|
+
else
|
141
|
+
emitter&.class&.name
|
142
|
+
end
|
143
|
+
|
144
|
+
emitter = nil if emitter == "Object"
|
145
|
+
emitter = emitter&.gsub("::", ".")&.underscore&.downcase
|
146
|
+
|
147
|
+
return unless emitter
|
148
|
+
|
149
|
+
@tags ||= {}
|
150
|
+
@tags[:emitter] = emitter
|
151
|
+
end
|
152
|
+
|
153
|
+
def method_missing(m, *args, **opts, &)
|
154
|
+
args, opts = normalize_arguments(*args, **opts)
|
155
|
+
|
156
|
+
# If schema validation fails, handle based on validation mode
|
157
|
+
if @schema && should_validate?(args)
|
158
|
+
validation_result = validate_metric_call(m, *args, **opts)
|
159
|
+
return if validation_result == :drop
|
160
|
+
end
|
161
|
+
|
162
|
+
if ENV.fetch("DATADOG_DEBUG", false)
|
163
|
+
warn "<CUSTOM METRIC to STATSD>: #{self}->#{m}(#{args.join(", ")}, #{opts.inspect})"
|
164
|
+
end
|
165
|
+
statsd&.send(m, *args, **opts, &)
|
166
|
+
end
|
167
|
+
|
168
|
+
def respond_to_missing?(method, *)
|
169
|
+
statsd&.respond_to? method
|
170
|
+
end
|
171
|
+
|
172
|
+
def normalize_arguments(*args, **opts)
|
173
|
+
# Handle metric name - use constructor metric if none provided in method call
|
174
|
+
normalized_args = args.dup
|
175
|
+
|
176
|
+
if @metric
|
177
|
+
if normalized_args.empty?
|
178
|
+
normalized_args = [@metric]
|
179
|
+
elsif normalized_args.first.nil?
|
180
|
+
normalized_args[0] = @metric
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
# Start with instance tags
|
185
|
+
merged_tags = (@tags || {}).dup
|
186
|
+
|
187
|
+
# Convert instance ab_test to tags
|
188
|
+
(@ab_test || {}).each do |test_name, group|
|
189
|
+
merged_tags[:ab_test_name] = test_name
|
190
|
+
merged_tags[:ab_test_group] = group
|
191
|
+
end
|
192
|
+
|
193
|
+
# Handle ab_test from method call opts and remove it from opts
|
194
|
+
normalized_opts = opts.dup
|
195
|
+
if normalized_opts[:ab_test]
|
196
|
+
normalized_opts[:ab_test].each do |test_name, group|
|
197
|
+
merged_tags[:ab_test_name] = test_name
|
198
|
+
merged_tags[:ab_test_group] = group
|
199
|
+
end
|
200
|
+
normalized_opts.delete(:ab_test)
|
201
|
+
end
|
202
|
+
|
203
|
+
# Merge with method call tags (method call tags take precedence)
|
204
|
+
merged_tags = merged_tags.merge(normalized_opts[:tags]) if normalized_opts[:tags]
|
205
|
+
|
206
|
+
# Set merged tags in opts if there are any
|
207
|
+
normalized_opts[:tags] = merged_tags unless merged_tags.empty?
|
208
|
+
|
209
|
+
# Handle sample_rate - use instance sample_rate if not provided in method call
|
210
|
+
if @sample_rate && @sample_rate != 1.0 && !normalized_opts.key?(:sample_rate)
|
211
|
+
normalized_opts[:sample_rate] = @sample_rate
|
212
|
+
end
|
213
|
+
|
214
|
+
[normalized_args, normalized_opts]
|
215
|
+
end
|
216
|
+
|
217
|
+
private
|
218
|
+
|
219
|
+
def should_validate?(args)
|
220
|
+
!args.empty? && args.first && @validation_mode != :off
|
221
|
+
end
|
222
|
+
|
223
|
+
def validate_metric_call(metric_method, *args, **opts)
|
224
|
+
return unless @schema && !args.empty?
|
225
|
+
|
226
|
+
metric_name = args.first
|
227
|
+
return unless metric_name
|
228
|
+
|
229
|
+
metric_type = normalize_metric_type(metric_method)
|
230
|
+
provided_tags = opts[:tags] || {}
|
231
|
+
|
232
|
+
begin
|
233
|
+
validate_metric_exists(metric_name, metric_type)
|
234
|
+
validate_metric_tags(metric_name, provided_tags)
|
235
|
+
rescue Datadog::Statsd::Schema::SchemaError => e
|
236
|
+
handle_validation_error(e)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def validate_metric_exists(metric_name, metric_type)
|
241
|
+
# Try to find the metric in the schema
|
242
|
+
all_metrics = @schema.all_metrics
|
243
|
+
|
244
|
+
# Look for exact match first
|
245
|
+
metric_info = all_metrics[metric_name.to_s]
|
246
|
+
|
247
|
+
unless metric_info
|
248
|
+
# Look for partial matches to provide better error messages
|
249
|
+
suggestions = find_metric_suggestions(metric_name, all_metrics.keys)
|
250
|
+
error_message = "Unknown metric '#{metric_name}'"
|
251
|
+
error_message += ". Did you mean: #{suggestions.join(", ")}?" if suggestions.any?
|
252
|
+
error_message += ". Available metrics: #{all_metrics.keys.first(5).join(", ")}"
|
253
|
+
error_message += ", ..." if all_metrics.size > 5
|
254
|
+
|
255
|
+
raise Datadog::Statsd::Schema::UnknownMetricError.new(error_message, metric: metric_name)
|
256
|
+
end
|
257
|
+
|
258
|
+
# Validate metric type matches
|
259
|
+
expected_type = metric_info[:definition].type
|
260
|
+
return unless expected_type != metric_type
|
261
|
+
|
262
|
+
error_message = "Invalid metric type for '#{metric_name}'. Expected '#{expected_type}', got '#{metric_type}'"
|
263
|
+
raise Datadog::Statsd::Schema::InvalidMetricTypeError.new(
|
264
|
+
error_message,
|
265
|
+
namespace: metric_info[:namespace_path].join("."),
|
266
|
+
metric: metric_name
|
267
|
+
)
|
268
|
+
end
|
269
|
+
|
270
|
+
def validate_metric_tags(metric_name, provided_tags)
|
271
|
+
all_metrics = @schema.all_metrics
|
272
|
+
metric_info = all_metrics[metric_name.to_s]
|
273
|
+
return unless metric_info
|
274
|
+
|
275
|
+
metric_definition = metric_info[:definition]
|
276
|
+
namespace = metric_info[:namespace]
|
277
|
+
|
278
|
+
# Get effective tags including inherited ones from namespace
|
279
|
+
effective_tags = namespace.effective_tags
|
280
|
+
|
281
|
+
# Check for missing required tags
|
282
|
+
missing_required = metric_definition.missing_required_tags(provided_tags)
|
283
|
+
if missing_required.any?
|
284
|
+
error_message = "Missing required tags for metric '#{metric_name}': #{missing_required.join(", ")}"
|
285
|
+
error_message += ". Required tags: #{metric_definition.required_tags.join(", ")}"
|
286
|
+
|
287
|
+
raise Datadog::Statsd::Schema::MissingRequiredTagError.new(
|
288
|
+
error_message,
|
289
|
+
namespace: metric_info[:namespace_path].join("."),
|
290
|
+
metric: metric_name
|
291
|
+
)
|
292
|
+
end
|
293
|
+
|
294
|
+
# Check for invalid tags (if metric has allowed_tags restrictions)
|
295
|
+
# Exclude framework tags like 'emitter' from validation
|
296
|
+
framework_tags = %i[emitter ab_test_name ab_test_group]
|
297
|
+
user_provided_tags = provided_tags.reject { |key, _| framework_tags.include?(key.to_sym) }
|
298
|
+
|
299
|
+
invalid_tags = metric_definition.invalid_tags(user_provided_tags)
|
300
|
+
if invalid_tags.any?
|
301
|
+
error_message = "Invalid tags for metric '#{metric_name}': #{invalid_tags.join(", ")}"
|
302
|
+
if metric_definition.allowed_tags.any?
|
303
|
+
error_message += ". Allowed tags: #{metric_definition.allowed_tags.join(", ")}"
|
304
|
+
end
|
305
|
+
|
306
|
+
raise Datadog::Statsd::Schema::InvalidTagError.new(
|
307
|
+
error_message,
|
308
|
+
namespace: metric_info[:namespace_path].join("."),
|
309
|
+
metric: metric_name
|
310
|
+
)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Validate tag values against schema definitions (including framework tags)
|
314
|
+
provided_tags.each do |tag_name, tag_value|
|
315
|
+
# Skip validation for framework tags that don't have schema definitions
|
316
|
+
next if framework_tags.include?(tag_name.to_sym) && !effective_tags[tag_name.to_sym]
|
317
|
+
|
318
|
+
tag_definition = effective_tags[tag_name.to_sym]
|
319
|
+
next unless tag_definition
|
320
|
+
|
321
|
+
validate_tag_value(metric_name, tag_name, tag_value, tag_definition, metric_info)
|
322
|
+
end
|
323
|
+
end
|
324
|
+
|
325
|
+
def validate_tag_value(metric_name, tag_name, tag_value, tag_definition, metric_info)
|
326
|
+
# Type validation
|
327
|
+
case tag_definition.type
|
328
|
+
when :integer
|
329
|
+
unless tag_value.is_a?(Integer) || (tag_value.is_a?(String) && tag_value.match?(/^\d+$/))
|
330
|
+
raise Datadog::Statsd::Schema::InvalidTagError.new(
|
331
|
+
"Tag '#{tag_name}' for metric '#{metric_name}' must be an integer, got #{tag_value.class}",
|
332
|
+
namespace: metric_info[:namespace_path].join("."),
|
333
|
+
metric: metric_name,
|
334
|
+
tag: tag_name
|
335
|
+
)
|
336
|
+
end
|
337
|
+
when :symbol
|
338
|
+
unless tag_value.is_a?(Symbol) || tag_value.is_a?(String)
|
339
|
+
raise Datadog::Statsd::Schema::InvalidTagError.new(
|
340
|
+
"Tag '#{tag_name}' for metric '#{metric_name}' must be a symbol or string, got #{tag_value.class}",
|
341
|
+
namespace: metric_info[:namespace_path].join("."),
|
342
|
+
metric: metric_name,
|
343
|
+
tag: tag_name
|
344
|
+
)
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
# Value validation
|
349
|
+
if tag_definition.values
|
350
|
+
normalized_value = tag_value.to_s
|
351
|
+
allowed_values = Array(tag_definition.values).map(&:to_s)
|
352
|
+
|
353
|
+
unless allowed_values.include?(normalized_value) ||
|
354
|
+
value_matches_pattern?(normalized_value, tag_definition.values)
|
355
|
+
raise Datadog::Statsd::Schema::InvalidTagError.new(
|
356
|
+
"Invalid value '#{tag_value}' for tag '#{tag_name}' in metric '#{metric_name}'. Allowed values: #{allowed_values.join(", ")}",
|
357
|
+
namespace: metric_info[:namespace_path].join("."),
|
358
|
+
metric: metric_name,
|
359
|
+
tag: tag_name
|
360
|
+
)
|
361
|
+
end
|
362
|
+
end
|
363
|
+
|
364
|
+
# Custom validation
|
365
|
+
return unless tag_definition.validate && tag_definition.validate.respond_to?(:call)
|
366
|
+
return if tag_definition.validate.call(tag_value)
|
367
|
+
|
368
|
+
raise Datadog::Statsd::Schema::InvalidTagError.new(
|
369
|
+
"Custom validation failed for tag '#{tag_name}' with value '#{tag_value}' in metric '#{metric_name}'",
|
370
|
+
namespace: metric_info[:namespace_path].join("."),
|
371
|
+
metric: metric_name,
|
372
|
+
tag: tag_name
|
373
|
+
)
|
374
|
+
end
|
375
|
+
|
376
|
+
def value_matches_pattern?(value, patterns)
|
377
|
+
Array(patterns).any? do |pattern|
|
378
|
+
case pattern
|
379
|
+
when Regexp
|
380
|
+
value.match?(pattern)
|
381
|
+
else
|
382
|
+
false
|
383
|
+
end
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
def find_metric_suggestions(metric_name, available_metrics)
|
388
|
+
# Simple fuzzy matching - find metrics that contain the metric name or vice versa
|
389
|
+
suggestions = available_metrics.select do |available|
|
390
|
+
available.include?(metric_name) || metric_name.include?(available) ||
|
391
|
+
levenshtein_distance(metric_name, available) <= 2
|
392
|
+
end
|
393
|
+
suggestions.first(3) # Limit to 3 suggestions
|
394
|
+
end
|
395
|
+
|
396
|
+
def levenshtein_distance(str1, str2)
|
397
|
+
# Simple Levenshtein distance implementation
|
398
|
+
return str2.length if str1.empty?
|
399
|
+
return str1.length if str2.empty?
|
400
|
+
|
401
|
+
matrix = Array.new(str1.length + 1) { Array.new(str2.length + 1) }
|
402
|
+
|
403
|
+
(0..str1.length).each { |i| matrix[i][0] = i }
|
404
|
+
(0..str2.length).each { |j| matrix[0][j] = j }
|
405
|
+
|
406
|
+
(1..str1.length).each do |i|
|
407
|
+
(1..str2.length).each do |j|
|
408
|
+
cost = str1[i - 1] == str2[j - 1] ? 0 : 1
|
409
|
+
matrix[i][j] = [
|
410
|
+
matrix[i - 1][j] + 1, # deletion
|
411
|
+
matrix[i][j - 1] + 1, # insertion
|
412
|
+
matrix[i - 1][j - 1] + cost # substitution
|
413
|
+
].min
|
414
|
+
end
|
415
|
+
end
|
416
|
+
|
417
|
+
matrix[str1.length][str2.length]
|
418
|
+
end
|
419
|
+
|
420
|
+
def normalize_metric_type(method_name)
|
421
|
+
case method_name.to_sym
|
422
|
+
when :increment, :decrement, :count
|
423
|
+
:counter
|
424
|
+
when :gauge
|
425
|
+
:gauge
|
426
|
+
when :histogram
|
427
|
+
:histogram
|
428
|
+
when :distribution
|
429
|
+
:distribution
|
430
|
+
when :set
|
431
|
+
:set
|
432
|
+
when :timing
|
433
|
+
:timing
|
434
|
+
else
|
435
|
+
method_name.to_sym
|
436
|
+
end
|
437
|
+
end
|
438
|
+
|
439
|
+
def handle_validation_error(error)
|
440
|
+
case @validation_mode
|
441
|
+
when :strict
|
442
|
+
# Only show colored output if not in test and colored2 is available
|
443
|
+
if Datadog::Statsd::Schema.in_test
|
444
|
+
warn "Schema Validation Error: #{error.message}"
|
445
|
+
else
|
446
|
+
warn "Schema Validation Error:\n • ".yellow + error.message.to_s.red
|
447
|
+
end
|
448
|
+
raise error
|
449
|
+
when :warn
|
450
|
+
# Only show colored output if not in test and colored2 is available
|
451
|
+
if Datadog::Statsd::Schema.in_test
|
452
|
+
warn "Schema Validation Warning: #{error.message}"
|
453
|
+
else
|
454
|
+
warn "Schema Validation Warning:\n • ".yellow + error.message.to_s.bold.yellow
|
455
|
+
end
|
456
|
+
nil # Continue execution
|
457
|
+
when :drop
|
458
|
+
:drop # Signal to drop the metric
|
459
|
+
when :off
|
460
|
+
nil # No validation - continue execution
|
461
|
+
else
|
462
|
+
raise error
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
delegate :flush, to: :class
|
467
|
+
|
468
|
+
delegate :statsd, to: :class
|
469
|
+
end
|
470
|
+
end
|
471
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "colored2"
|
4
|
+
require "active_support/core_ext/string/inflections"
|
5
|
+
|
6
|
+
module Datadog
|
7
|
+
class Statsd
|
8
|
+
module Schema
|
9
|
+
class SchemaError < StandardError
|
10
|
+
attr_reader :namespace, :metric, :tag
|
11
|
+
|
12
|
+
def initialize(message = nil, namespace: "<-no-namespace->", metric: "<-no-metric->", tag: "<-no-tag->")
|
13
|
+
@namespace = namespace
|
14
|
+
@metric = metric
|
15
|
+
@tag = tag
|
16
|
+
message ||= "#{self.class.name.underscore.gsub("_", " ").split(".").map(&:capitalize).join(" ")} Error " \
|
17
|
+
"{ namespace: #{namespace}, metric: #{metric}, tag: #{tag} }"
|
18
|
+
super(message)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class UnknownMetricError < SchemaError; end
|
23
|
+
|
24
|
+
class InvalidTagError < SchemaError; end
|
25
|
+
|
26
|
+
class MissingRequiredTagError < SchemaError; end
|
27
|
+
|
28
|
+
class InvalidMetricTypeError < SchemaError; end
|
29
|
+
|
30
|
+
class DuplicateMetricError < SchemaError; end
|
31
|
+
|
32
|
+
class InvalidNamespaceError < SchemaError; end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|