datadog-statsd-schema 0.1.1 → 0.2.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,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+
5
+ module Datadog
6
+ class Statsd
7
+ module Schema
8
+ module Commands
9
+ # @description Analyze a schema file for metrics and validation
10
+ class Analyze < Dry::CLI::Command
11
+ class << self
12
+ attr_accessor :stdout, :stderr
13
+ end
14
+
15
+ self.stdout = $stdout
16
+ self.stderr = $stderr
17
+
18
+ desc "Analyze a schema file for metrics and validation"
19
+
20
+ option :file, aliases: %w[-f], type: :string, required: true, desc: "Path to the schema file to analyze"
21
+ option :color, aliases: %w[-c], type: :boolean, required: false, desc: "Enable/Disable color output"
22
+
23
+ # @description Analyze a schema file for metrics and validation
24
+ # @param options [Hash] The options for the command
25
+ # @option options [String] :file The path to the schema file to analyze
26
+ # @option options [Boolean] :color Enable/Disable color output
27
+ # @return [void]
28
+ def call(**options)
29
+ file = options[:file]
30
+
31
+ unless file
32
+ warn "Error: --file option is required"
33
+ warn "Usage: dss analyze --file <schema.rb>"
34
+ exit 1
35
+ end
36
+
37
+ warn "Analyzing schema file: #{file}, color: #{options[:color]}"
38
+ @schema = ::Datadog::Statsd::Schema.load_file(file)
39
+ ::Datadog::Statsd::Schema::Analyzer.new([@schema],
40
+ stdout: self.class.stdout,
41
+ stderr: self.class.stderr,
42
+ color: options[:color]).analyze
43
+ end
44
+
45
+ def warn(message)
46
+ self.class.stderr.puts message
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Datadog
4
+ class Statsd
5
+ module Schema
6
+ module Commands
7
+ # CLI commands will be defined here
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ # Require all command files
14
+ require_relative "commands/analyze"
@@ -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
 
@@ -5,22 +5,58 @@ require "dry-types"
5
5
  require_relative "tag_definition"
6
6
  require_relative "metric_definition"
7
7
 
8
+ # @author Datadog Team
9
+ # @since 0.1.0
8
10
  module Datadog
9
11
  class Statsd
12
+ # Schema definition and validation module for StatsD metrics
10
13
  module Schema
14
+ # Represents a namespace in the metric schema hierarchy
15
+ # Namespaces contain tags, metrics, and nested namespaces, providing organization and scoping
16
+ # @example Basic namespace
17
+ # namespace = Namespace.new(
18
+ # name: :web,
19
+ # description: "Web application metrics"
20
+ # )
21
+ # @example Namespace with tags and metrics
22
+ # namespace = Namespace.new(
23
+ # name: :api,
24
+ # tags: { controller: tag_def, action: tag_def2 },
25
+ # metrics: { requests: metric_def }
26
+ # )
27
+ # @author Datadog Team
28
+ # @since 0.1.0
11
29
  class Namespace < Dry::Struct
12
- # Include the types module for easier access
30
+ # Include the types module for easier access to Dry::Typesa
13
31
  module Types
14
32
  include Dry.Types()
15
33
  end
16
34
 
35
+ # The namespace name as a symbol
36
+ # @return [Symbol] Namespace name
17
37
  attribute :name, Types::Strict::Symbol
38
+
39
+ # Hash of tag definitions within this namespace
40
+ # @return [Hash<Symbol, TagDefinition>] Tag name to TagDefinition mapping
18
41
  attribute :tags, Types::Hash.map(Types::Symbol, TagDefinition).default({}.freeze)
42
+
43
+ # Hash of metric definitions within this namespace
44
+ # @return [Hash<Symbol, MetricDefinition>] Metric name to MetricDefinition mapping
19
45
  attribute :metrics, Types::Hash.map(Types::Symbol, MetricDefinition).default({}.freeze)
46
+
47
+ # Hash of nested namespaces within this namespace
48
+ # @return [Hash<Symbol, Namespace>] Namespace name to Namespace mapping
20
49
  attribute :namespaces, Types::Hash.map(Types::Symbol, Namespace).default({}.freeze)
50
+
51
+ # Human-readable description of this namespace
52
+ # @return [String, nil] Description text
21
53
  attribute :description, Types::String.optional.default(nil)
22
54
 
23
- # Get the full path of this namespace
55
+ # Get the full path of this namespace including parent namespaces
56
+ # @param parent_path [Array<Symbol>] Array of parent namespace names
57
+ # @return [Array<Symbol>] Full namespace path
58
+ # @example
59
+ # namespace.full_path([:web, :api]) # => [:web, :api, :request]
24
60
  def full_path(parent_path = [])
25
61
  return [name] if parent_path.empty?
26
62
 
@@ -28,72 +64,109 @@ module Datadog
28
64
  end
29
65
 
30
66
  # Find a metric by name within this namespace
67
+ # @param metric_name [String, Symbol] Name of the metric to find
68
+ # @return [MetricDefinition, nil] The metric definition or nil if not found
69
+ # @example
70
+ # namespace.find_metric(:page_views) # => MetricDefinition instance
31
71
  def find_metric(metric_name)
32
72
  metric_symbol = metric_name.to_sym
33
73
  metrics[metric_symbol]
34
74
  end
35
75
 
36
76
  # Find a tag definition by name within this namespace
77
+ # @param tag_name [String, Symbol] Name of the tag to find
78
+ # @return [TagDefinition, nil] The tag definition or nil if not found
79
+ # @example
80
+ # namespace.find_tag(:controller) # => TagDefinition instance
37
81
  def find_tag(tag_name)
38
82
  tag_symbol = tag_name.to_sym
39
83
  tags[tag_symbol]
40
84
  end
41
85
 
42
86
  # Find a nested namespace by name
87
+ # @param namespace_name [String, Symbol] Name of the namespace to find
88
+ # @return [Namespace, nil] The nested namespace or nil if not found
89
+ # @example
90
+ # namespace.find_namespace(:api) # => Namespace instance
43
91
  def find_namespace(namespace_name)
44
92
  namespace_symbol = namespace_name.to_sym
45
93
  namespaces[namespace_symbol]
46
94
  end
47
95
 
48
- # Add a new metric to this namespace
96
+ # Add a new metric to this namespace (returns new namespace instance)
97
+ # @param metric_definition [MetricDefinition] The metric definition to add
98
+ # @return [Namespace] New namespace instance with the added metric
49
99
  def add_metric(metric_definition)
50
100
  new(metrics: metrics.merge(metric_definition.name => metric_definition))
51
101
  end
52
102
 
53
- # Add a new tag definition to this namespace
103
+ # Add a new tag definition to this namespace (returns new namespace instance)
104
+ # @param tag_definition [TagDefinition] The tag definition to add
105
+ # @return [Namespace] New namespace instance with the added tag
54
106
  def add_tag(tag_definition)
55
107
  new(tags: tags.merge(tag_definition.name => tag_definition))
56
108
  end
57
109
 
58
- # Add a nested namespace
110
+ # Add a nested namespace (returns new namespace instance)
111
+ # @param namespace [Namespace] The namespace to add
112
+ # @return [Namespace] New namespace instance with the added namespace
59
113
  def add_namespace(namespace)
60
114
  new(namespaces: namespaces.merge(namespace.name => namespace))
61
115
  end
62
116
 
63
117
  # Get all metric names in this namespace (not including nested)
118
+ # @return [Array<Symbol>] Array of metric names
64
119
  def metric_names
65
120
  metrics.keys
66
121
  end
67
122
 
68
123
  # Get all tag names in this namespace
124
+ # @return [Array<Symbol>] Array of tag names
69
125
  def tag_names
70
126
  tags.keys
71
127
  end
72
128
 
73
129
  # Get all nested namespace names
130
+ # @return [Array<Symbol>] Array of namespace names
74
131
  def namespace_names
75
132
  namespaces.keys
76
133
  end
77
134
 
78
135
  # Check if this namespace contains a metric
136
+ # @param metric_name [String, Symbol] Name of the metric to check
137
+ # @return [Boolean] true if metric exists
79
138
  def has_metric?(metric_name)
80
139
  metric_symbol = metric_name.to_sym
81
140
  metrics.key?(metric_symbol)
82
141
  end
83
142
 
84
143
  # Check if this namespace contains a tag definition
144
+ # @param tag_name [String, Symbol] Name of the tag to check
145
+ # @return [Boolean] true if tag exists
85
146
  def has_tag?(tag_name)
86
147
  tag_symbol = tag_name.to_sym
87
148
  tags.key?(tag_symbol)
88
149
  end
89
150
 
90
151
  # Check if this namespace contains a nested namespace
152
+ # @param namespace_name [String, Symbol] Name of the namespace to check
153
+ # @return [Boolean] true if namespace exists
91
154
  def has_namespace?(namespace_name)
92
155
  namespace_symbol = namespace_name.to_sym
93
156
  namespaces.key?(namespace_symbol)
94
157
  end
95
158
 
96
159
  # Get all metrics recursively including from nested namespaces
160
+ # @param path [Array<Symbol>] Current namespace path (used for recursion)
161
+ # @return [Hash] Hash mapping full metric names to metric info
162
+ # @example
163
+ # {
164
+ # "web.page_views" => {
165
+ # definition: MetricDefinition,
166
+ # namespace_path: [:web],
167
+ # namespace: Namespace
168
+ # }
169
+ # }
97
170
  def all_metrics(path = [])
98
171
  # Filter out :root from the path to avoid it appearing in metric names
99
172
  current_path = path + [name]
@@ -119,11 +192,14 @@ module Datadog
119
192
  end
120
193
 
121
194
  # Get all tag definitions including inherited from parent namespaces
195
+ # @param parent_tags [Hash] Tag definitions from parent namespaces
196
+ # @return [Hash<Symbol, TagDefinition>] Combined tag definitions
122
197
  def effective_tags(parent_tags = {})
123
198
  parent_tags.merge(tags)
124
199
  end
125
200
 
126
201
  # Validate that all tag references in metrics exist
202
+ # @return [Array<String>] Array of validation error messages
127
203
  def validate_tag_references
128
204
  errors = []
129
205
 
@@ -148,6 +224,10 @@ module Datadog
148
224
  end
149
225
 
150
226
  # Find metric by path (e.g., "request.duration" within web namespace)
227
+ # @param path [String] Dot-separated path to the metric
228
+ # @return [MetricDefinition, nil] The metric definition or nil if not found
229
+ # @example
230
+ # namespace.find_metric_by_path("api.requests") # => MetricDefinition
151
231
  def find_metric_by_path(path)
152
232
  parts = path.split(".")
153
233
 
@@ -167,6 +247,10 @@ module Datadog
167
247
  end
168
248
 
169
249
  # Get namespace by path (e.g., "web.request")
250
+ # @param path [String] Dot-separated path to the namespace
251
+ # @return [Namespace, nil] The namespace or nil if not found
252
+ # @example
253
+ # root_namespace.find_namespace_by_path("web.api") # => Namespace
170
254
  def find_namespace_by_path(path)
171
255
  return self if path.empty?
172
256
 
@@ -186,11 +270,13 @@ module Datadog
186
270
  end
187
271
 
188
272
  # Count total metrics including nested namespaces
273
+ # @return [Integer] Total number of metrics in this namespace tree
189
274
  def total_metrics_count
190
275
  metrics.count + namespaces.values.sum(&:total_metrics_count)
191
276
  end
192
277
 
193
278
  # Count total namespaces including nested
279
+ # @return [Integer] Total number of namespaces in this namespace tree
194
280
  def total_namespaces_count
195
281
  namespaces.count + namespaces.values.sum(&:total_namespaces_count)
196
282
  end