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.
@@ -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
+ ![schema_emiter](./schema_emitter.png)]
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
@@ -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,3 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "datadog/statsd/schema"
@@ -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