datadog-statsd-schema 0.1.0 → 0.1.2

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.
data/Rakefile CHANGED
@@ -1,20 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'bundler/gem_tasks'
4
- require 'rspec/core/rake_task'
5
- require 'timeout'
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+ require "timeout"
6
6
 
7
7
  def shell(*args)
8
- puts "running: #{args.join(' ')}"
9
- system(args.join(' '))
8
+ puts "running: #{args.join(" ")}"
9
+ system(args.join(" "))
10
10
  end
11
11
 
12
12
  task :clean do
13
- shell('rm -rf pkg/ tmp/ coverage/ doc/ ' )
13
+ shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
14
14
  end
15
15
 
16
16
  task gem: [:build] do
17
- shell('gem install pkg/*')
17
+ shell("gem install pkg/*")
18
18
  end
19
19
 
20
20
  task permissions: [:clean] do
data/examples/shared.rb CHANGED
@@ -15,7 +15,7 @@ STATSD = Datadog::Statsd.new(
15
15
  "localhost", 8125
16
16
  )
17
17
 
18
- class FakeStatsd # rubocop:disable Style/Documentation
18
+ class FakeStatsd
19
19
  def initialize(...); end
20
20
 
21
21
  def method_missing(m, *args, **opts)
@@ -51,14 +51,32 @@ module Datadog
51
51
  end
52
52
 
53
53
  extend Forwardable
54
- def_delegators :datadog_statsd,
55
- :increment,
56
- :decrement,
57
- :gauge,
58
- :histogram,
59
- :distribution,
60
- :set,
61
- :flush
54
+ def_delegators :datadog_statsd, :flush
55
+
56
+ # Override metric methods to ensure global tags are applied
57
+ def increment(*args, **opts)
58
+ send_metric_with_global_tags(:increment, *args, **opts)
59
+ end
60
+
61
+ def decrement(*args, **opts)
62
+ send_metric_with_global_tags(:decrement, *args, **opts)
63
+ end
64
+
65
+ def gauge(*args, **opts)
66
+ send_metric_with_global_tags(:gauge, *args, **opts)
67
+ end
68
+
69
+ def histogram(*args, **opts)
70
+ send_metric_with_global_tags(:histogram, *args, **opts)
71
+ end
72
+
73
+ def distribution(*args, **opts)
74
+ send_metric_with_global_tags(:distribution, *args, **opts)
75
+ end
76
+
77
+ def set(*args, **opts)
78
+ send_metric_with_global_tags(:set, *args, **opts)
79
+ end
62
80
 
63
81
  def global_tags
64
82
  @global_tags ||= OpenStruct.new
@@ -68,6 +86,32 @@ module Datadog
68
86
  yield(global_tags)
69
87
  end
70
88
 
89
+ private
90
+
91
+ def send_metric_with_global_tags(method_name, *args, **opts)
92
+ # Ensure connection is established with global tags
93
+ connect unless datadog_statsd
94
+
95
+ # Merge global tags with provided tags
96
+ merged_tags = {}
97
+
98
+ # Add global tags from Schema configuration
99
+ schema_global_tags = ::Datadog::Statsd::Schema.configuration.tags || {}
100
+ merged_tags.merge!(schema_global_tags)
101
+
102
+ # Add global tags from Emitter configuration
103
+ emitter_global_tags = global_tags.to_h
104
+ merged_tags.merge!(emitter_global_tags)
105
+
106
+ # Add method-specific tags (these take precedence)
107
+ merged_tags.merge!(opts[:tags]) if opts[:tags]
108
+
109
+ # Update opts with merged tags
110
+ opts = opts.merge(tags: merged_tags) unless merged_tags.empty?
111
+
112
+ datadog_statsd.send(method_name, *args, **opts)
113
+ end
114
+
71
115
  def connect(
72
116
  host: DEFAULT_HOST,
73
117
  port: DEFAULT_PORT,
@@ -79,7 +123,14 @@ module Datadog
79
123
  return @datadog_statsd if defined?(@datadog_statsd) && @datadog_statsd
80
124
 
81
125
  tags ||= {}
126
+
127
+ # Merge global tags from Schema configuration
128
+ schema_global_tags = ::Datadog::Statsd::Schema.configuration.tags || {}
129
+ tags = tags.merge(schema_global_tags)
130
+
131
+ # Merge global tags from Emitter configuration
82
132
  tags = tags.merge(global_tags.to_h)
133
+
83
134
  tags = tags.map { |k, v| "#{k}:#{v}" }
84
135
 
85
136
  opts ||= {}
@@ -107,10 +158,24 @@ module Datadog
107
158
  end
108
159
  end
109
160
 
110
- attr_reader :tags, :ab_test, :sample_rate, :metric, :schema, :validation_mode
111
-
161
+ attr_reader :tags, :ab_test, :sample_rate, :metric, :schema, :validation_mode, :stderr
162
+
163
+ # @description Initializes a new Statsd::Emitter instance.
164
+ # @param emitter [String, Symbol, Module, Class, Object] The emitter identifier.
165
+ # @param stderr [IO] The stderr output stream. Defaults to $stderr.
166
+ # @param metric [String] The metric name (or prefix) to use for all metrics.
167
+ # @param tags [Hash] The tags to add to the metric.
168
+ # @param ab_test [Hash] The AB test to add to the metric.
169
+ # @param sample_rate [Float] The sample rate to use for the metric.
170
+ # @param schema [Schema] The schema to use for validation.
171
+ # @param validation_mode [Symbol] The validation mode to use. Defaults to :strict.
172
+ # :strict - Raises an error if the metric is invalid.
173
+ # :warn - Logs a warning if the metric is invalid.
174
+ # :drop - Drops the metric if it is invalid.
175
+ # :off - Disables validation.
112
176
  def initialize(
113
177
  emitter = nil,
178
+ stderr: $stderr,
114
179
  metric: nil,
115
180
  tags: nil,
116
181
  ab_test: nil,
@@ -123,13 +188,23 @@ module Datadog
123
188
  "Datadog::Statsd::Emitter: use class methods if you are passing nothing to the constructor."
124
189
  end
125
190
  @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?
191
+
192
+ # Initialize tags with provided tags or empty hash
193
+ @tags = (tags || {}).dup
194
+
195
+ # Merge global tags from Schema configuration
196
+ schema_global_tags = ::Datadog::Statsd::Schema.configuration.tags || {}
197
+ @tags.merge!(schema_global_tags)
198
+
199
+ # Merge global tags from Emitter configuration
200
+ emitter_global_tags = self.class.global_tags.to_h
201
+ @tags.merge!(emitter_global_tags)
128
202
 
129
203
  @ab_test = ab_test || {}
130
204
  @metric = metric
131
205
  @schema = schema
132
206
  @validation_mode = validation_mode
207
+ @stderr = stderr
133
208
 
134
209
  emitter =
135
210
  case emitter
@@ -146,7 +221,6 @@ module Datadog
146
221
 
147
222
  return unless emitter
148
223
 
149
- @tags ||= {}
150
224
  @tags[:emitter] = emitter
151
225
  end
152
226
 
@@ -292,9 +366,16 @@ module Datadog
292
366
  end
293
367
 
294
368
  # Check for invalid tags (if metric has allowed_tags restrictions)
295
- # Exclude framework tags like 'emitter' from validation
369
+ # Exclude framework tags and global tags from validation
296
370
  framework_tags = %i[emitter ab_test_name ab_test_group]
297
- user_provided_tags = provided_tags.reject { |key, _| framework_tags.include?(key.to_sym) }
371
+
372
+ # Get global tags to exclude from validation
373
+ schema_global_tags = ::Datadog::Statsd::Schema.configuration.tags&.keys || []
374
+ emitter_global_tags = self.class.global_tags.to_h.keys
375
+ global_tag_keys = (schema_global_tags + emitter_global_tags).map(&:to_sym)
376
+
377
+ excluded_tags = framework_tags + global_tag_keys
378
+ user_provided_tags = provided_tags.reject { |key, _| excluded_tags.include?(key.to_sym) }
298
379
 
299
380
  invalid_tags = metric_definition.invalid_tags(user_provided_tags)
300
381
  if invalid_tags.any?
@@ -312,8 +393,8 @@ module Datadog
312
393
 
313
394
  # Validate tag values against schema definitions (including framework tags)
314
395
  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]
396
+ # Skip validation for framework tags and global tags that don't have schema definitions
397
+ next if excluded_tags.include?(tag_name.to_sym) && !effective_tags[tag_name.to_sym]
317
398
 
318
399
  tag_definition = effective_tags[tag_name.to_sym]
319
400
  next unless tag_definition
@@ -441,17 +522,17 @@ module Datadog
441
522
  when :strict
442
523
  # Only show colored output if not in test and colored2 is available
443
524
  if Datadog::Statsd::Schema.in_test
444
- warn "Schema Validation Error: #{error.message}"
525
+ stderr.puts "Schema Validation Error: #{error.message}"
445
526
  else
446
- warn "Schema Validation Error:\n • ".yellow + error.message.to_s.red
527
+ stderr.puts "Schema Validation Error:\n • ".yellow + error.message.to_s.red
447
528
  end
448
529
  raise error
449
530
  when :warn
450
531
  # Only show colored output if not in test and colored2 is available
451
532
  if Datadog::Statsd::Schema.in_test
452
- warn "Schema Validation Warning: #{error.message}"
533
+ stderr.puts "Schema Validation Warning: #{error.message}"
453
534
  else
454
- warn "Schema Validation Warning:\n • ".yellow + error.message.to_s.bold.yellow
535
+ stderr.puts "Schema Validation Warning:\n • ".yellow + error.message.to_s.bold.yellow
455
536
  end
456
537
  nil # Continue execution
457
538
  when :drop
@@ -3,12 +3,35 @@
3
3
  require "colored2"
4
4
  require "active_support/core_ext/string/inflections"
5
5
 
6
+ # @author Datadog Team
7
+ # @since 0.1.0
6
8
  module Datadog
7
9
  class Statsd
10
+ # Schema definition and validation module for StatsD metrics
8
11
  module Schema
12
+ # Base error class for all schema validation errors
13
+ # Provides context about where the error occurred including namespace, metric, and tag information
14
+ # @abstract Base class for schema validation errors
15
+ # @author Datadog Team
16
+ # @since 0.1.0
9
17
  class SchemaError < StandardError
10
- attr_reader :namespace, :metric, :tag
18
+ # The namespace where the error occurred
19
+ # @return [String] Namespace path or placeholder if not available
20
+ attr_reader :namespace
11
21
 
22
+ # The metric name where the error occurred
23
+ # @return [String] Metric name or placeholder if not available
24
+ attr_reader :metric
25
+
26
+ # The tag name where the error occurred
27
+ # @return [String] Tag name or placeholder if not available
28
+ attr_reader :tag
29
+
30
+ # Initialize a new schema error with context information
31
+ # @param message [String, nil] Custom error message, will be auto-generated if nil
32
+ # @param namespace [String] Namespace context for the error
33
+ # @param metric [String] Metric context for the error
34
+ # @param tag [String] Tag context for the error
12
35
  def initialize(message = nil, namespace: "<-no-namespace->", metric: "<-no-metric->", tag: "<-no-tag->")
13
36
  @namespace = namespace
14
37
  @metric = metric
@@ -19,16 +42,46 @@ module Datadog
19
42
  end
20
43
  end
21
44
 
45
+ # Raised when a metric is used that doesn't exist in the schema
46
+ # @example
47
+ # # This would raise UnknownMetricError if 'unknown_metric' is not defined in the schema
48
+ # emitter.increment('unknown_metric')
22
49
  class UnknownMetricError < SchemaError; end
23
50
 
51
+ # Raised when a tag is used that doesn't exist in the schema or is not allowed for the metric
52
+ # @example
53
+ # # This would raise InvalidTagError if 'invalid_tag' is not allowed for the metric
54
+ # emitter.increment('valid_metric', tags: { invalid_tag: 'value' })
24
55
  class InvalidTagError < SchemaError; end
25
56
 
57
+ # Raised when a required tag is missing from a metric call
58
+ # @example
59
+ # # This would raise MissingRequiredTagError if 'environment' tag is required
60
+ # emitter.increment('metric_requiring_env_tag', tags: { service: 'web' })
26
61
  class MissingRequiredTagError < SchemaError; end
27
62
 
63
+ # Raised when a metric is called with the wrong type
64
+ # @example
65
+ # # This would raise InvalidMetricTypeError if 'response_time' is defined as a histogram
66
+ # emitter.increment('response_time') # Should be emitter.histogram('response_time')
28
67
  class InvalidMetricTypeError < SchemaError; end
29
68
 
69
+ # Raised when attempting to define a metric that already exists in the schema
70
+ # @example Schema definition error
71
+ # namespace :web do
72
+ # metrics do
73
+ # counter :requests
74
+ # counter :requests # This would raise DuplicateMetricError
75
+ # end
76
+ # end
30
77
  class DuplicateMetricError < SchemaError; end
31
78
 
79
+ # Raised when a namespace definition is invalid
80
+ # @example
81
+ # # This might be raised for namespace naming conflicts or invalid structure
82
+ # namespace :invalid_namespace do
83
+ # # ... invalid configuration
84
+ # end
32
85
  class InvalidNamespaceError < SchemaError; end
33
86
  end
34
87
  end
@@ -3,28 +3,78 @@
3
3
  require "dry-struct"
4
4
  require "dry-types"
5
5
 
6
+ # @author Datadog Team
7
+ # @since 0.1.0
6
8
  module Datadog
7
9
  class Statsd
10
+ # Schema definition and validation module for StatsD metrics
8
11
  module Schema
12
+ # Represents a metric definition within a schema namespace
13
+ # Defines the metric type, allowed/required tags, validation rules, and metadata
14
+ # @example Basic metric definition
15
+ # metric_def = MetricDefinition.new(
16
+ # name: :page_views,
17
+ # type: :counter,
18
+ # allowed_tags: [:controller, :action],
19
+ # required_tags: [:controller]
20
+ # )
21
+ # @example Metric with description and units
22
+ # metric_def = MetricDefinition.new(
23
+ # name: :request_duration,
24
+ # type: :distribution,
25
+ # description: "HTTP request processing time",
26
+ # units: "milliseconds",
27
+ # allowed_tags: [:controller, :action, :status_code],
28
+ # required_tags: [:controller, :action]
29
+ # )
30
+ # @author Datadog Team
31
+ # @since 0.1.0
9
32
  class MetricDefinition < Dry::Struct
10
- # Include the types module for easier access
33
+ # Include the types module for easier access to Dry::Types
11
34
  module Types
12
35
  include Dry.Types()
13
36
  end
14
37
 
15
- # Valid metric types in StatsD
38
+ # Valid metric types supported by StatsD
16
39
  VALID_METRIC_TYPES = %i[counter gauge histogram distribution timing set].freeze
17
40
 
41
+ # The metric name as a symbol
42
+ # @return [Symbol] Metric name
18
43
  attribute :name, Types::Strict::Symbol
44
+
45
+ # The metric type (counter, gauge, histogram, distribution, timing, set)
46
+ # @return [Symbol] One of the valid metric types
19
47
  attribute :type, Types::Strict::Symbol.constrained(included_in: VALID_METRIC_TYPES)
48
+
49
+ # Human-readable description of what this metric measures
50
+ # @return [String, nil] Description text
20
51
  attribute :description, Types::String.optional.default(nil)
52
+
53
+ # Array of tag names that are allowed for this metric
54
+ # @return [Array<Symbol>] Allowed tag names (empty array means all tags allowed)
21
55
  attribute :allowed_tags, Types::Array.of(Types::Symbol).default([].freeze)
56
+
57
+ # Array of tag names that are required for this metric
58
+ # @return [Array<Symbol>] Required tag names
22
59
  attribute :required_tags, Types::Array.of(Types::Symbol).default([].freeze)
60
+
61
+ # Path to another metric to inherit tags from
62
+ # @return [String, nil] Dot-separated path to parent metric
23
63
  attribute :inherit_tags, Types::String.optional.default(nil)
64
+
65
+ # Units of measurement for this metric (e.g., "milliseconds", "bytes")
66
+ # @return [String, nil] Unit description
24
67
  attribute :units, Types::String.optional.default(nil)
68
+
69
+ # The namespace this metric belongs to
70
+ # @return [Symbol, nil] Namespace name
25
71
  attribute :namespace, Types::Strict::Symbol.optional.default(nil)
26
72
 
27
73
  # Get the full metric name including namespace path
74
+ # @param namespace_path [Array<Symbol>] Array of namespace names leading to this metric
75
+ # @return [String] Fully qualified metric name
76
+ # @example
77
+ # metric_def.full_name([:web, :api]) # => "web.api.page_views"
28
78
  def full_name(namespace_path = [])
29
79
  return name.to_s if namespace_path.empty?
30
80
 
@@ -32,24 +82,44 @@ module Datadog
32
82
  end
33
83
 
34
84
  # Check if a tag is allowed for this metric
85
+ # @param tag_name [String, Symbol] Tag name to check
86
+ # @return [Boolean] true if tag is allowed (or no restrictions exist)
87
+ # @example
88
+ # metric_def.allows_tag?(:controller) # => true
89
+ # metric_def.allows_tag?(:invalid_tag) # => false (if restrictions exist)
35
90
  def allows_tag?(tag_name)
36
91
  tag_symbol = tag_name.to_sym
37
92
  allowed_tags.empty? || allowed_tags.include?(tag_symbol)
38
93
  end
39
94
 
40
95
  # Check if a tag is required for this metric
96
+ # @param tag_name [String, Symbol] Tag name to check
97
+ # @return [Boolean] true if tag is required
98
+ # @example
99
+ # metric_def.requires_tag?(:controller) # => true
100
+ # metric_def.requires_tag?(:optional_tag) # => false
41
101
  def requires_tag?(tag_name)
42
102
  tag_symbol = tag_name.to_sym
43
103
  required_tags.include?(tag_symbol)
44
104
  end
45
105
 
46
106
  # Get all missing required tags from a provided tag set
107
+ # @param provided_tags [Hash] Hash of tag names to values
108
+ # @return [Array<Symbol>] Array of missing required tag names
109
+ # @example
110
+ # metric_def.missing_required_tags(controller: "users")
111
+ # # => [:action] (if action is also required)
47
112
  def missing_required_tags(provided_tags)
48
113
  provided_tag_symbols = provided_tags.keys.map(&:to_sym)
49
114
  required_tags - provided_tag_symbols
50
115
  end
51
116
 
52
117
  # Get all invalid tags from a provided tag set
118
+ # @param provided_tags [Hash] Hash of tag names to values
119
+ # @return [Array<Symbol>] Array of invalid tag names
120
+ # @example
121
+ # metric_def.invalid_tags(controller: "users", invalid: "value")
122
+ # # => [:invalid] (if only controller is allowed)
53
123
  def invalid_tags(provided_tags)
54
124
  return [] if allowed_tags.empty? # No restrictions
55
125
 
@@ -58,31 +128,41 @@ module Datadog
58
128
  end
59
129
 
60
130
  # Validate a complete tag set for this metric
131
+ # @param provided_tags [Hash] Hash of tag names to values
132
+ # @return [Boolean] true if all tags are valid
133
+ # @example
134
+ # metric_def.valid_tags?(controller: "users", action: "show") # => true
61
135
  def valid_tags?(provided_tags)
62
136
  missing_required_tags(provided_tags).empty? && invalid_tags(provided_tags).empty?
63
137
  end
64
138
 
65
139
  # Check if this is a timing-based metric
140
+ # @return [Boolean] true for timing, distribution, or histogram metrics
66
141
  def timing_metric?
67
142
  %i[timing distribution histogram].include?(type)
68
143
  end
69
144
 
70
145
  # Check if this is a counting metric
146
+ # @return [Boolean] true for counter metrics
71
147
  def counting_metric?
72
148
  %i[counter].include?(type)
73
149
  end
74
150
 
75
151
  # Check if this is a gauge metric
152
+ # @return [Boolean] true for gauge metrics
76
153
  def gauge_metric?
77
154
  type == :gauge
78
155
  end
79
156
 
80
157
  # Check if this is a set metric
158
+ # @return [Boolean] true for set metrics
81
159
  def set_metric?
82
160
  type == :set
83
161
  end
84
162
 
85
- # Get effective tags by merging with inherited tags if present
163
+ # Get effective allowed tags by merging with inherited tags if present
164
+ # @param schema_registry [Object] Registry to look up inherited metrics
165
+ # @return [Array<Symbol>] Combined allowed tags including inherited ones
86
166
  def effective_allowed_tags(schema_registry = nil)
87
167
  return allowed_tags unless inherit_tags && schema_registry
88
168
 
@@ -92,6 +172,9 @@ module Datadog
92
172
  (inherited_metric.effective_allowed_tags(schema_registry) + allowed_tags).uniq
93
173
  end
94
174
 
175
+ # Get effective required tags by merging with inherited tags if present
176
+ # @param schema_registry [Object] Registry to look up inherited metrics
177
+ # @return [Array<Symbol>] Combined required tags including inherited ones
95
178
  def effective_required_tags(schema_registry = nil)
96
179
  return required_tags unless inherit_tags && schema_registry
97
180