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.
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
@@ -0,0 +1,29 @@
1
+ namespace "marathon" do
2
+ tags do
3
+ tag :course, values: %w[sf-marathon new-york austin]
4
+ tag :length, values: %w[full half]
5
+ end
6
+
7
+ namespace "started" do
8
+ metrics do
9
+ counter "total" do
10
+ description "Number of people who started the Marathon"
11
+ tags required: %i[course length]
12
+ end
13
+ end
14
+ end
15
+
16
+ namespace "finished" do
17
+ metrics do
18
+ counter "total" do
19
+ description "Number of people who finished the Marathon"
20
+ inherit_tags "marathon.started.total"
21
+ end
22
+
23
+ distribution "duration" do
24
+ description "Marathon duration"
25
+ inherit_tags "marathon.started.total"
26
+ end
27
+ end
28
+ end
29
+ end
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)
data/exe/dss ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.expand_path("../lib", __dir__))
5
+
6
+ require "datadog/statsd/schema"
7
+
8
+ Dry::CLI.new(Datadog::Statsd::Schema::CLI).call
@@ -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
@@ -0,0 +1,397 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+ require "colored2"
5
+ module Datadog
6
+ class Statsd
7
+ module Schema
8
+ # Result structure for schema analysis
9
+ # @!attribute [r] total_unique_metrics
10
+ # @return [Integer] Total number of unique metric names (including expansions)
11
+ # @!attribute [r] metrics_analysis
12
+ # @return [Array<MetricAnalysis>] Analysis for each metric
13
+ # @!attribute [r] total_possible_custom_metrics
14
+ # @return [Integer] Total number of possible custom metric combinations
15
+ AnalysisResult = Data.define(
16
+ :total_unique_metrics,
17
+ :metrics_analysis,
18
+ :total_possible_custom_metrics
19
+ )
20
+
21
+ # Analysis data for individual metrics
22
+ # @!attribute [r] metric_name
23
+ # @return [String] Full metric name
24
+ # @!attribute [r] metric_type
25
+ # @return [Symbol] Type of metric (:counter, :gauge, etc.)
26
+ # @!attribute [r] expanded_names
27
+ # @return [Array<String>] All expanded metric names (for gauge/distribution/histogram)
28
+ # @!attribute [r] unique_tags
29
+ # @return [Integer] Number of unique tags for this metric
30
+ # @!attribute [r] unique_tag_values
31
+ # @return [Integer] Total number of unique tag values across all tags
32
+ # @!attribute [r] total_combinations
33
+ # @return [Integer] Total possible tag value combinations for this metric
34
+ MetricAnalysis = Data.define(
35
+ :metric_name,
36
+ :metric_type,
37
+ :expanded_names,
38
+ :unique_tags,
39
+ :unique_tag_values,
40
+ :total_combinations
41
+ )
42
+
43
+ # Analyzes schema instances to provide comprehensive metrics statistics
44
+ class Analyzer
45
+ # Metric suffixes for different metric types that create multiple metrics
46
+ METRIC_EXPANSIONS = {
47
+ gauge: %w[count min max sum avg],
48
+ distribution: %w[count min max sum avg p50 p75 p90 p95 p99],
49
+ histogram: %w[count min max sum avg]
50
+ }.freeze
51
+
52
+ attr_reader :schemas, :stdout, :stderr, :color
53
+
54
+ # Initialize analyzer with schema(s)
55
+ # @param schemas [Datadog::Statsd::Schema::Namespace, Array<Datadog::Statsd::Schema::Namespace>]
56
+ # Single schema or array of schemas to analyze
57
+ def initialize(schemas, stdout: $stdout, stderr: $stderr, color: true)
58
+ @schemas = Array(schemas)
59
+ @stdout = stdout
60
+ @stderr = stderr
61
+ @color = color
62
+ if color
63
+ Colored2.enable!
64
+ else
65
+ Colored2.disable!
66
+ end
67
+ end
68
+
69
+ # Perform comprehensive analysis of the schemas
70
+ # @return [AnalysisResult] Complete analysis results
71
+ def analyze
72
+ all_metrics = collect_all_metrics
73
+ metrics_analysis = analyze_metrics(all_metrics)
74
+
75
+ total_unique_metrics = metrics_analysis.sum { |analysis| analysis.expanded_names.size }
76
+ total_possible_custom_metrics = metrics_analysis.sum(&:total_combinations)
77
+
78
+ stdout.puts format_analysis_output(metrics_analysis, total_unique_metrics, total_possible_custom_metrics)
79
+
80
+ AnalysisResult.new(
81
+ total_unique_metrics: total_unique_metrics,
82
+ metrics_analysis: metrics_analysis,
83
+ total_possible_custom_metrics: total_possible_custom_metrics
84
+ )
85
+ end
86
+
87
+ private
88
+
89
+ # Collect all metrics from all schemas with their context
90
+ # @return [Array<Hash>] Array of metric info hashes
91
+ def collect_all_metrics
92
+ all_metrics = []
93
+
94
+ @schemas.each do |schema|
95
+ schema_metrics = schema.all_metrics
96
+ schema_metrics.each do |metric_full_name, metric_info|
97
+ all_metrics << {
98
+ full_name: metric_full_name,
99
+ definition: metric_info[:definition],
100
+ namespace: metric_info[:namespace],
101
+ namespace_path: metric_info[:namespace_path]
102
+ }
103
+ end
104
+ end
105
+
106
+ all_metrics
107
+ end
108
+
109
+ # Analyze each metric for tags and combinations
110
+ # @param all_metrics [Array<Hash>] Collected metrics
111
+ # @return [Array<MetricAnalysis>] Analysis for each metric
112
+ def analyze_metrics(all_metrics)
113
+ all_metrics.map do |metric_info|
114
+ analyze_single_metric(metric_info)
115
+ end
116
+ end
117
+
118
+ # Analyze a single metric
119
+ # @param metric_info [Hash] Metric information
120
+ # @return [MetricAnalysis] Analysis for this metric
121
+ def analyze_single_metric(metric_info)
122
+ definition = metric_info[:definition]
123
+ namespace = metric_info[:namespace]
124
+ namespace_path = metric_info[:namespace_path]
125
+ full_name = metric_info[:full_name]
126
+
127
+ # Get expanded metric names based on type
128
+ expanded_names = get_expanded_metric_names(full_name, definition.type)
129
+
130
+ # Build effective tags including parent namespace tags
131
+ effective_tags = build_effective_tags_for_metric(namespace, namespace_path)
132
+ available_tag_definitions = collect_available_tags(definition, effective_tags)
133
+
134
+ # Calculate tag statistics
135
+ unique_tags = available_tag_definitions.size
136
+ unique_tag_values = available_tag_definitions.values.sum { |tag_def| count_tag_values(tag_def) }
137
+
138
+ # Calculate total combinations (cartesian product of all tag values)
139
+ total_combinations = calculate_tag_combinations(available_tag_definitions) * expanded_names.size
140
+
141
+ MetricAnalysis.new(
142
+ metric_name: full_name,
143
+ metric_type: definition.type,
144
+ expanded_names: expanded_names,
145
+ unique_tags: unique_tags,
146
+ unique_tag_values: unique_tag_values,
147
+ total_combinations: total_combinations
148
+ )
149
+ end
150
+
151
+ # Build effective tags for a metric including parent namespace tags
152
+ # @param namespace [Namespace] The immediate namespace containing the metric
153
+ # @param namespace_path [Array<Symbol>] Full path to the namespace
154
+ # @return [Hash] Hash of effective tag definitions
155
+ def build_effective_tags_for_metric(namespace, namespace_path)
156
+ effective_tags = {}
157
+
158
+ # Start from the root and build up tags through the hierarchy
159
+ current_path = []
160
+
161
+ # Find and traverse parent namespaces to collect their tags
162
+ @schemas.each do |schema|
163
+ # Traverse the namespace path to collect parent tags
164
+ namespace_path.each do |path_segment|
165
+ # Skip :root as it's just the schema root
166
+ next if path_segment == :root
167
+
168
+ current_path << path_segment
169
+
170
+ # Find the namespace at this path
171
+ path_str = current_path.join(".")
172
+ found_namespace = schema.find_namespace_by_path(path_str)
173
+
174
+ next unless found_namespace
175
+
176
+ # Add tags from this namespace level
177
+ found_namespace.tags.each do |tag_name, tag_def|
178
+ effective_tags[tag_name] = tag_def
179
+ end
180
+ end
181
+
182
+ break if effective_tags.any? # Found the schema with our namespaces
183
+ end
184
+
185
+ # Add the immediate namespace's tags (these take precedence)
186
+ namespace.tags.each do |tag_name, tag_def|
187
+ effective_tags[tag_name] = tag_def
188
+ end
189
+
190
+ effective_tags
191
+ end
192
+
193
+ # Get expanded metric names for types that create multiple metrics
194
+ # @param base_name [String] Base metric name
195
+ # @param metric_type [Symbol] Type of the metric
196
+ # @return [Array<String>] All metric names this creates
197
+ def get_expanded_metric_names(base_name, metric_type)
198
+ expansions = METRIC_EXPANSIONS[metric_type]
199
+
200
+ if expansions
201
+ # For metrics that expand, create name.suffix for each expansion
202
+ expansions.map { |suffix| "#{base_name}.#{suffix}" }
203
+ else
204
+ # For simple metrics, just return the base name
205
+ [base_name]
206
+ end
207
+ end
208
+
209
+ # Collect all tags available to a metric
210
+ # @param definition [MetricDefinition] The metric definition
211
+ # @param effective_tags [Hash] Tags available in the namespace
212
+ # @return [Hash] Hash of tag name to tag definition
213
+ def collect_available_tags(definition, effective_tags)
214
+ available_tags = {}
215
+
216
+ # Handle tag inheritance from other metrics first
217
+ if definition.inherit_tags
218
+ inherited_tags = resolve_inherited_tags(definition.inherit_tags)
219
+ inherited_tags.keys
220
+ inherited_tags.each do |tag_name, tag_def|
221
+ available_tags[tag_name] = tag_def
222
+ end
223
+ end
224
+
225
+ # Determine which additional tags to include based on metric's tag specification
226
+ if definition.allowed_tags.any? || definition.required_tags.any?
227
+ # If metric specifies allowed or required tags, only include those + inherited tags
228
+ additional_tag_names = (definition.allowed_tags + definition.required_tags).map(&:to_sym).uniq
229
+
230
+ additional_tag_names.each do |tag_name|
231
+ available_tags[tag_name] = effective_tags[tag_name] if effective_tags[tag_name]
232
+ end
233
+ else
234
+ # If no allowed or required tags specified, include all effective namespace tags
235
+ # (This is the case when a metric doesn't restrict its tags)
236
+ effective_tags.each do |tag_name, tag_def|
237
+ available_tags[tag_name] = tag_def unless available_tags[tag_name]
238
+ end
239
+ end
240
+
241
+ available_tags
242
+ end
243
+
244
+ # Resolve inherited tags from a parent metric path
245
+ # @param inherit_path [String] Dot-separated path to parent metric
246
+ # @return [Hash] Hash of inherited tag definitions
247
+ def resolve_inherited_tags(inherit_path)
248
+ inherited_tags = {}
249
+
250
+ @schemas.each do |schema|
251
+ # Find the parent metric in the schema
252
+ all_metrics = schema.all_metrics
253
+ parent_metric_info = all_metrics[inherit_path]
254
+
255
+ next unless parent_metric_info
256
+
257
+ parent_definition = parent_metric_info[:definition]
258
+ parent_namespace = parent_metric_info[:namespace]
259
+ parent_namespace_path = parent_metric_info[:namespace_path]
260
+
261
+ # Build effective tags for the parent metric (including its own parent namespace tags)
262
+ parent_effective_tags = build_effective_tags_for_metric(parent_namespace, parent_namespace_path)
263
+
264
+ # Recursively resolve parent's inherited tags first
265
+ if parent_definition.inherit_tags
266
+ parent_inherited = resolve_inherited_tags(parent_definition.inherit_tags)
267
+ inherited_tags.merge!(parent_inherited)
268
+ end
269
+
270
+ # Get the tags that are actually available to the parent metric
271
+ parent_available_tags = collect_parent_available_tags(parent_definition, parent_effective_tags)
272
+ inherited_tags.merge!(parent_available_tags)
273
+
274
+ break # Found the parent metric, stop searching
275
+ end
276
+
277
+ inherited_tags
278
+ end
279
+
280
+ # Collect available tags for a parent metric (without recursion to avoid infinite loops)
281
+ # @param definition [MetricDefinition] The parent metric definition
282
+ # @param effective_tags [Hash] Tags available in the parent's namespace
283
+ # @return [Hash] Hash of tag name to tag definition
284
+ def collect_parent_available_tags(definition, effective_tags)
285
+ available_tags = {}
286
+
287
+ # Start with all effective tags from namespace
288
+ effective_tags.each do |tag_name, tag_def|
289
+ available_tags[tag_name] = tag_def
290
+ end
291
+
292
+ # Apply parent metric's tag restrictions
293
+ if definition.allowed_tags.any?
294
+ allowed_and_required_tags = (definition.allowed_tags + definition.required_tags).map(&:to_sym).uniq
295
+ available_tags.select! { |tag_name, _| allowed_and_required_tags.include?(tag_name) }
296
+ end
297
+
298
+ available_tags
299
+ end
300
+
301
+ # Count the number of possible values for a tag
302
+ # @param tag_definition [TagDefinition] The tag definition
303
+ # @return [Integer] Number of possible values
304
+ def count_tag_values(tag_definition)
305
+ if tag_definition.values.nil?
306
+ # If no values specified, assume it can have any value (estimate)
307
+ 100 # Conservative estimate for open-ended tags
308
+ elsif tag_definition.values.is_a?(Array)
309
+ tag_definition.values.size
310
+ elsif tag_definition.values.is_a?(Regexp)
311
+ # For regex, we can't know exact count, use estimate
312
+ 50 # Conservative estimate for regex patterns
313
+ else
314
+ 1 # Single value
315
+ end
316
+ end
317
+
318
+ # Calculate total possible combinations of tag values
319
+ # @param tag_definitions [Hash] Hash of tag name to definition
320
+ # @return [Integer] Total combinations possible
321
+ def calculate_tag_combinations(tag_definitions)
322
+ return 1 if tag_definitions.empty?
323
+
324
+ # Multiply the number of possible values for each tag
325
+ tag_definitions.values.reduce(1) do |total, tag_def|
326
+ total * count_tag_values(tag_def)
327
+ end
328
+ end
329
+
330
+ # Format the analysis output for display
331
+ # @param metrics_analysis [Array<MetricAnalysis>] All metric analyses
332
+ # @param total_unique_metrics [Integer] Total unique metrics
333
+ # @param total_possible_custom_metrics [Integer] Total possible combinations
334
+ # @return [String] Formatted output
335
+ def format_analysis_output(
336
+ metrics_analysis,
337
+ total_unique_metrics,
338
+ total_possible_custom_metrics
339
+ )
340
+ output = StringIO.new
341
+
342
+ format_metric_analysis_header(output)
343
+ metrics_analysis.each do |analysis|
344
+ output.puts
345
+ format_metric_analysis(output, analysis)
346
+ line(output, placement: :flat)
347
+ end
348
+ summary(output, total_unique_metrics, total_possible_custom_metrics)
349
+ output.string
350
+ end
351
+
352
+ def line(output, placement: :top)
353
+ if placement == :top
354
+ output.puts "┌──────────────────────────────────────────────────────────────────────────────────────────────┐".white.on.blue
355
+ elsif placement == :bottom
356
+ output.puts "└──────────────────────────────────────────────────────────────────────────────────────────────┘".white.on.blue
357
+ elsif placement == :middle
358
+ output.puts "├──────────────────────────────────────────────────────────────────────────────────────────────┤".white.on.blue
359
+ elsif placement == :flat
360
+ output.puts " ──────────────────────────────────────────────────────────────────────────────────────────────".white.bold
361
+ end
362
+ end
363
+
364
+ def summary(output, total_unique_metrics, total_possible_custom_metrics)
365
+ line(output)
366
+ output.puts "│ Schema Analysis Results: │".yellow.bold.on.blue
367
+ output.puts "│ SUMMARY │".white.on.blue
368
+ line(output, placement: :bottom)
369
+ output.puts
370
+ output.puts " Total unique metrics: #{("%3d" % total_unique_metrics).bold.green}"
371
+ output.puts "Total possible custom metric combinations: #{("%3d" % total_possible_custom_metrics).bold.green}"
372
+ output.puts
373
+ end
374
+
375
+ def format_metric_analysis_header(output)
376
+ line(output)
377
+ output.puts "│ Detailed Metric Analysis: │".white.on.blue
378
+ line(output, placement: :bottom)
379
+ end
380
+
381
+ def format_metric_analysis(output, analysis)
382
+ output.puts " • #{analysis.metric_type.to_s.cyan}('#{analysis.metric_name.yellow.bold}')"
383
+ if analysis.expanded_names.size > 1
384
+ output.puts " Expanded names:"
385
+ output.print " • ".yellow
386
+ output.puts analysis.expanded_names.join("\n • ").yellow
387
+ end
388
+ output.puts
389
+ output.puts " Unique tags: #{("%3d" % analysis.unique_tags).bold.green}"
390
+ output.puts " Total tag values: #{("%3d" % analysis.unique_tag_values).bold.green}"
391
+ output.puts " Possible combinations: #{("%3d" % analysis.total_combinations).bold.green}"
392
+ output.puts
393
+ end
394
+ end
395
+ end
396
+ end
397
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/cli"
4
+ require_relative "commands"
5
+
6
+ module Datadog
7
+ class Statsd
8
+ module Schema
9
+ module CLI
10
+ extend Dry::CLI::Registry
11
+
12
+ register "analyze", Commands::Analyze
13
+ end
14
+ end
15
+ end
16
+ end