datadog-statsd-schema 0.1.2 → 0.2.1
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 +4 -4
- data/.rubocop.yml +1 -0
- data/.rubocop_todo.yml +27 -22
- data/README.md +366 -502
- data/Rakefile +5 -1
- data/docs/img/dss-analyze.png +0 -0
- data/examples/schema/example_marathon.rb +33 -0
- data/examples/schema/web_schema.rb +32 -0
- data/exe/dss +8 -0
- data/lib/datadog/statsd/schema/analyzer.rb +477 -0
- data/lib/datadog/statsd/schema/cli.rb +16 -0
- data/lib/datadog/statsd/schema/commands/analyze.rb +63 -0
- data/lib/datadog/statsd/schema/commands.rb +14 -0
- data/lib/datadog/statsd/schema/namespace.rb +1 -1
- data/lib/datadog/statsd/schema/version.rb +1 -1
- data/lib/datadog/statsd/schema.rb +2 -0
- metadata +25 -4
- data/exe/datadog-statsd-schema +0 -3
data/Rakefile
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require "bundler/gem_tasks"
|
4
4
|
require "rspec/core/rake_task"
|
5
|
+
require "rubocop/rake_task"
|
5
6
|
require "timeout"
|
6
7
|
|
7
8
|
def shell(*args)
|
@@ -26,4 +27,7 @@ task build: :permissions
|
|
26
27
|
|
27
28
|
RSpec::Core::RakeTask.new(:spec)
|
28
29
|
|
29
|
-
|
30
|
+
RuboCop::RakeTask.new(:rubocop)
|
31
|
+
|
32
|
+
# Define the default task to include both
|
33
|
+
task default: %i[spec rubocop]
|
Binary file
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# vim: ft=ruby
|
4
|
+
|
5
|
+
namespace "marathon" do
|
6
|
+
tags do
|
7
|
+
tag :course, values: %w[sf-marathon new-york austin]
|
8
|
+
tag :length, values: %w[full half]
|
9
|
+
end
|
10
|
+
|
11
|
+
namespace "started" do
|
12
|
+
metrics do
|
13
|
+
counter "total" do
|
14
|
+
description "Number of people who started the Marathon"
|
15
|
+
tags required: %i[course length]
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
namespace "finished" do
|
21
|
+
metrics do
|
22
|
+
counter "total" do
|
23
|
+
description "Number of people who finished the Marathon"
|
24
|
+
inherit_tags "marathon.started.total"
|
25
|
+
end
|
26
|
+
|
27
|
+
distribution "duration" do
|
28
|
+
description "Marathon duration"
|
29
|
+
inherit_tags "marathon.started.total"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# vim: ft=ruby
|
4
|
+
|
5
|
+
namespace :web do
|
6
|
+
tags do
|
7
|
+
tag :environment, values: %w[production staging development]
|
8
|
+
tag :service, values: %w[api web worker]
|
9
|
+
tag :region, values: %w[us-east-1 us-west-2 eu-west-1]
|
10
|
+
end
|
11
|
+
|
12
|
+
namespace :requests do
|
13
|
+
metrics do
|
14
|
+
counter :total do
|
15
|
+
description "Total HTTP requests"
|
16
|
+
tags required: %i[environment service], allowed: %i[region]
|
17
|
+
end
|
18
|
+
|
19
|
+
distribution :duration do
|
20
|
+
description "Request processing time in milliseconds"
|
21
|
+
inherit_tags "web.requests.total"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
metrics do
|
27
|
+
gauge :memory_usage do
|
28
|
+
description "Memory usage in bytes"
|
29
|
+
tags required: %i[environment], allowed: %i[service]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
data/exe/dss
ADDED
@@ -0,0 +1,477 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "stringio"
|
4
|
+
require "colored2"
|
5
|
+
require "forwardable"
|
6
|
+
require "json"
|
7
|
+
require "yaml"
|
8
|
+
|
9
|
+
module Datadog
|
10
|
+
class Statsd
|
11
|
+
module Schema
|
12
|
+
# Result structure for schema analysis
|
13
|
+
# @!attribute [r] total_unique_metrics
|
14
|
+
# @return [Integer] Total number of unique metric names (including expansions)
|
15
|
+
# @!attribute [r] metrics_analysis
|
16
|
+
# @return [Array<MetricAnalysis>] Analysis for each metric
|
17
|
+
# @!attribute [r] total_possible_custom_metrics
|
18
|
+
# @return [Integer] Total number of possible custom metric combinations
|
19
|
+
AnalysisResult = Data.define(
|
20
|
+
:total_unique_metrics,
|
21
|
+
:metrics_analysis,
|
22
|
+
:total_possible_custom_metrics
|
23
|
+
)
|
24
|
+
|
25
|
+
# Analysis data for individual metrics
|
26
|
+
# @!attribute [r] metric_name
|
27
|
+
# @return [String] Full metric name
|
28
|
+
# @!attribute [r] metric_type
|
29
|
+
# @return [Symbol] Type of metric (:counter, :gauge, etc.)
|
30
|
+
# @!attribute [r] expanded_names
|
31
|
+
# @return [Array<String>] All expanded metric names (for gauge/distribution/histogram)
|
32
|
+
# @!attribute [r] unique_tags
|
33
|
+
# @return [Integer] Number of unique tags for this metric
|
34
|
+
# @!attribute [r] unique_tag_values
|
35
|
+
# @return [Integer] Total number of unique tag values across all tags
|
36
|
+
# @!attribute [r] total_combinations
|
37
|
+
# @return [Integer] Total possible tag value combinations for this metric
|
38
|
+
MetricAnalysis = Data.define(
|
39
|
+
:metric_name,
|
40
|
+
:metric_type,
|
41
|
+
:expanded_names,
|
42
|
+
:unique_tags,
|
43
|
+
:unique_tag_values,
|
44
|
+
:total_combinations
|
45
|
+
)
|
46
|
+
|
47
|
+
# Analyzes schema instances to provide comprehensive metrics statistics
|
48
|
+
class Analyzer
|
49
|
+
# Metric suffixes for different metric types that create multiple metrics
|
50
|
+
METRIC_EXPANSIONS = {
|
51
|
+
gauge: %w[count min max sum avg],
|
52
|
+
distribution: %w[count min max sum avg p50 p75 p90 p95 p99],
|
53
|
+
histogram: %w[count min max sum avg]
|
54
|
+
}.freeze
|
55
|
+
|
56
|
+
attr_reader :schemas, :stdout, :stderr, :color, :format, :analysis_result
|
57
|
+
|
58
|
+
SUPPORTED_FORMATS = %i[text json yaml].freeze
|
59
|
+
|
60
|
+
# Initialize analyzer with schema(s)
|
61
|
+
# @param schemas [Datadog::Statsd::Schema::Namespace, Array<Datadog::Statsd::Schema::Namespace>]
|
62
|
+
# Single schema or array of schemas to analyze
|
63
|
+
def initialize(
|
64
|
+
schemas,
|
65
|
+
stdout: $stdout,
|
66
|
+
stderr: $stderr,
|
67
|
+
color: true,
|
68
|
+
format: SUPPORTED_FORMATS.first
|
69
|
+
)
|
70
|
+
@schemas = Array(schemas)
|
71
|
+
@stdout = stdout
|
72
|
+
@stderr = stderr
|
73
|
+
@color = color
|
74
|
+
@format = format.to_sym
|
75
|
+
|
76
|
+
raise ArgumentError, "Unsupported format: #{format}. Supported formats are: #{SUPPORTED_FORMATS.join(", ")}" unless SUPPORTED_FORMATS.include?(format)
|
77
|
+
|
78
|
+
if color
|
79
|
+
Colored2.enable!
|
80
|
+
else
|
81
|
+
Colored2.disable!
|
82
|
+
end
|
83
|
+
|
84
|
+
@analysis_result = analyze
|
85
|
+
end
|
86
|
+
|
87
|
+
# Perform comprehensive analysis of the schemas
|
88
|
+
# @return [AnalysisResult] Complete analysis results
|
89
|
+
def analyze
|
90
|
+
all_metrics = collect_all_metrics
|
91
|
+
metrics_analysis = analyze_metrics(all_metrics).map(&:to_h)
|
92
|
+
|
93
|
+
total_unique_metrics = metrics_analysis.sum { |analysis| analysis[:expanded_names].size }
|
94
|
+
total_possible_custom_metrics = metrics_analysis.sum { |e| e[:total_combinations] }
|
95
|
+
|
96
|
+
AnalysisResult.new(
|
97
|
+
total_unique_metrics:,
|
98
|
+
metrics_analysis:,
|
99
|
+
total_possible_custom_metrics:
|
100
|
+
)
|
101
|
+
end
|
102
|
+
|
103
|
+
def render
|
104
|
+
case format
|
105
|
+
when :text
|
106
|
+
TextFormatter.new(stdout:, stderr:, color:, analysis_result:).render
|
107
|
+
when :json
|
108
|
+
JSONFormatter.new(stdout:, stderr:, color:, analysis_result:).render
|
109
|
+
when :yaml
|
110
|
+
YAMLFormatter.new(stdout:, stderr:, color:, analysis_result:).render
|
111
|
+
else
|
112
|
+
raise ArgumentError, "Unsupported format: #{format}. Supported formats are: #{SUPPORTED_FORMATS.join(", ")}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
# Collect all metrics from all schemas with their context
|
119
|
+
# @return [Array<Hash>] Array of metric info hashes
|
120
|
+
def collect_all_metrics
|
121
|
+
all_metrics = []
|
122
|
+
|
123
|
+
@schemas.each do |schema|
|
124
|
+
schema_metrics = schema.all_metrics
|
125
|
+
schema_metrics.each do |metric_full_name, metric_info|
|
126
|
+
all_metrics << {
|
127
|
+
full_name: metric_full_name,
|
128
|
+
definition: metric_info[:definition],
|
129
|
+
namespace: metric_info[:namespace],
|
130
|
+
namespace_path: metric_info[:namespace_path]
|
131
|
+
}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
all_metrics
|
136
|
+
end
|
137
|
+
|
138
|
+
# Analyze each metric for tags and combinations
|
139
|
+
# @param all_metrics [Array<Hash>] Collected metrics
|
140
|
+
# @return [Array<MetricAnalysis>] Analysis for each metric
|
141
|
+
def analyze_metrics(all_metrics)
|
142
|
+
all_metrics.map do |metric_info|
|
143
|
+
analyze_single_metric(metric_info)
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Analyze a single metric
|
148
|
+
# @param metric_info [Hash] Metric information
|
149
|
+
# @return [MetricAnalysis] Analysis for this metric
|
150
|
+
def analyze_single_metric(metric_info)
|
151
|
+
definition = metric_info[:definition]
|
152
|
+
namespace = metric_info[:namespace]
|
153
|
+
namespace_path = metric_info[:namespace_path]
|
154
|
+
full_name = metric_info[:full_name]
|
155
|
+
|
156
|
+
# Get expanded metric names based on type
|
157
|
+
expanded_names = get_expanded_metric_names(full_name, definition.type)
|
158
|
+
|
159
|
+
# Build effective tags including parent namespace tags
|
160
|
+
effective_tags = build_effective_tags_for_metric(namespace, namespace_path)
|
161
|
+
available_tag_definitions = collect_available_tags(definition, effective_tags)
|
162
|
+
|
163
|
+
# Calculate tag statistics
|
164
|
+
unique_tags = available_tag_definitions.size
|
165
|
+
unique_tag_values = available_tag_definitions.values.sum { |tag_def| count_tag_values(tag_def) }
|
166
|
+
|
167
|
+
# Calculate total combinations (cartesian product of all tag values)
|
168
|
+
total_combinations = calculate_tag_combinations(available_tag_definitions) * expanded_names.size
|
169
|
+
|
170
|
+
MetricAnalysis.new(
|
171
|
+
metric_name: full_name,
|
172
|
+
metric_type: definition.type,
|
173
|
+
expanded_names: expanded_names,
|
174
|
+
unique_tags: unique_tags,
|
175
|
+
unique_tag_values: unique_tag_values,
|
176
|
+
total_combinations: total_combinations
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Build effective tags for a metric including parent namespace tags
|
181
|
+
# @param namespace [Namespace] The immediate namespace containing the metric
|
182
|
+
# @param namespace_path [Array<Symbol>] Full path to the namespace
|
183
|
+
# @return [Hash] Hash of effective tag definitions
|
184
|
+
def build_effective_tags_for_metric(namespace, namespace_path)
|
185
|
+
effective_tags = {}
|
186
|
+
|
187
|
+
# Start from the root and build up tags through the hierarchy
|
188
|
+
current_path = []
|
189
|
+
|
190
|
+
# Find and traverse parent namespaces to collect their tags
|
191
|
+
@schemas.each do |schema|
|
192
|
+
# Traverse the namespace path to collect parent tags
|
193
|
+
namespace_path.each do |path_segment|
|
194
|
+
# Skip :root as it's just the schema root
|
195
|
+
next if path_segment == :root
|
196
|
+
|
197
|
+
current_path << path_segment
|
198
|
+
|
199
|
+
# Find the namespace at this path
|
200
|
+
path_str = current_path.join(".")
|
201
|
+
found_namespace = schema.find_namespace_by_path(path_str)
|
202
|
+
|
203
|
+
next unless found_namespace
|
204
|
+
|
205
|
+
# Add tags from this namespace level
|
206
|
+
found_namespace.tags.each do |tag_name, tag_def|
|
207
|
+
effective_tags[tag_name] = tag_def
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
break if effective_tags.any? # Found the schema with our namespaces
|
212
|
+
end
|
213
|
+
|
214
|
+
# Add the immediate namespace's tags (these take precedence)
|
215
|
+
namespace.tags.each do |tag_name, tag_def|
|
216
|
+
effective_tags[tag_name] = tag_def
|
217
|
+
end
|
218
|
+
|
219
|
+
effective_tags
|
220
|
+
end
|
221
|
+
|
222
|
+
# Get expanded metric names for types that create multiple metrics
|
223
|
+
# @param base_name [String] Base metric name
|
224
|
+
# @param metric_type [Symbol] Type of the metric
|
225
|
+
# @return [Array<String>] All metric names this creates
|
226
|
+
def get_expanded_metric_names(base_name, metric_type)
|
227
|
+
expansions = METRIC_EXPANSIONS[metric_type]
|
228
|
+
|
229
|
+
if expansions
|
230
|
+
# For metrics that expand, create name.suffix for each expansion
|
231
|
+
expansions.map { |suffix| "#{base_name}.#{suffix}" }
|
232
|
+
else
|
233
|
+
# For simple metrics, just return the base name
|
234
|
+
[base_name]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Collect all tags available to a metric
|
239
|
+
# @param definition [MetricDefinition] The metric definition
|
240
|
+
# @param effective_tags [Hash] Tags available in the namespace
|
241
|
+
# @return [Hash] Hash of tag name to tag definition
|
242
|
+
def collect_available_tags(definition, effective_tags)
|
243
|
+
available_tags = {}
|
244
|
+
|
245
|
+
# Handle tag inheritance from other metrics first
|
246
|
+
if definition.inherit_tags
|
247
|
+
inherited_tags = resolve_inherited_tags(definition.inherit_tags)
|
248
|
+
inherited_tags.keys
|
249
|
+
inherited_tags.each do |tag_name, tag_def|
|
250
|
+
available_tags[tag_name] = tag_def
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
# Determine which additional tags to include based on metric's tag specification
|
255
|
+
if definition.allowed_tags.any? || definition.required_tags.any?
|
256
|
+
# If metric specifies allowed or required tags, only include those + inherited tags
|
257
|
+
additional_tag_names = (definition.allowed_tags + definition.required_tags).map(&:to_sym).uniq
|
258
|
+
|
259
|
+
additional_tag_names.each do |tag_name|
|
260
|
+
available_tags[tag_name] = effective_tags[tag_name] if effective_tags[tag_name]
|
261
|
+
end
|
262
|
+
else
|
263
|
+
# If no allowed or required tags specified, include all effective namespace tags
|
264
|
+
# (This is the case when a metric doesn't restrict its tags)
|
265
|
+
effective_tags.each do |tag_name, tag_def|
|
266
|
+
available_tags[tag_name] = tag_def unless available_tags[tag_name]
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
available_tags
|
271
|
+
end
|
272
|
+
|
273
|
+
# Resolve inherited tags from a parent metric path
|
274
|
+
# @param inherit_path [String] Dot-separated path to parent metric
|
275
|
+
# @return [Hash] Hash of inherited tag definitions
|
276
|
+
def resolve_inherited_tags(inherit_path)
|
277
|
+
inherited_tags = {}
|
278
|
+
|
279
|
+
@schemas.each do |schema|
|
280
|
+
# Find the parent metric in the schema
|
281
|
+
all_metrics = schema.all_metrics
|
282
|
+
parent_metric_info = all_metrics[inherit_path]
|
283
|
+
|
284
|
+
next unless parent_metric_info
|
285
|
+
|
286
|
+
parent_definition = parent_metric_info[:definition]
|
287
|
+
parent_namespace = parent_metric_info[:namespace]
|
288
|
+
parent_namespace_path = parent_metric_info[:namespace_path]
|
289
|
+
|
290
|
+
# Build effective tags for the parent metric (including its own parent namespace tags)
|
291
|
+
parent_effective_tags = build_effective_tags_for_metric(parent_namespace, parent_namespace_path)
|
292
|
+
|
293
|
+
# Recursively resolve parent's inherited tags first
|
294
|
+
if parent_definition.inherit_tags
|
295
|
+
parent_inherited = resolve_inherited_tags(parent_definition.inherit_tags)
|
296
|
+
inherited_tags.merge!(parent_inherited)
|
297
|
+
end
|
298
|
+
|
299
|
+
# Get the tags that are actually available to the parent metric
|
300
|
+
parent_available_tags = collect_parent_available_tags(parent_definition, parent_effective_tags)
|
301
|
+
inherited_tags.merge!(parent_available_tags)
|
302
|
+
|
303
|
+
break # Found the parent metric, stop searching
|
304
|
+
end
|
305
|
+
|
306
|
+
inherited_tags
|
307
|
+
end
|
308
|
+
|
309
|
+
# Collect available tags for a parent metric (without recursion to avoid infinite loops)
|
310
|
+
# @param definition [MetricDefinition] The parent metric definition
|
311
|
+
# @param effective_tags [Hash] Tags available in the parent's namespace
|
312
|
+
# @return [Hash] Hash of tag name to tag definition
|
313
|
+
def collect_parent_available_tags(definition, effective_tags)
|
314
|
+
available_tags = {}
|
315
|
+
|
316
|
+
# Start with all effective tags from namespace
|
317
|
+
effective_tags.each do |tag_name, tag_def|
|
318
|
+
available_tags[tag_name] = tag_def
|
319
|
+
end
|
320
|
+
|
321
|
+
# Apply parent metric's tag restrictions
|
322
|
+
if definition.allowed_tags.any?
|
323
|
+
allowed_and_required_tags = (definition.allowed_tags + definition.required_tags).map(&:to_sym).uniq
|
324
|
+
available_tags.select! { |tag_name, _| allowed_and_required_tags.include?(tag_name) }
|
325
|
+
end
|
326
|
+
|
327
|
+
available_tags
|
328
|
+
end
|
329
|
+
|
330
|
+
# Count the number of possible values for a tag
|
331
|
+
# @param tag_definition [TagDefinition] The tag definition
|
332
|
+
# @return [Integer] Number of possible values
|
333
|
+
def count_tag_values(tag_definition)
|
334
|
+
if tag_definition.values.nil?
|
335
|
+
# If no values specified, assume it can have any value (estimate)
|
336
|
+
100 # Conservative estimate for open-ended tags
|
337
|
+
elsif tag_definition.values.is_a?(Array)
|
338
|
+
tag_definition.values.size
|
339
|
+
elsif tag_definition.values.is_a?(Regexp)
|
340
|
+
# For regex, we can't know exact count, use estimate
|
341
|
+
50 # Conservative estimate for regex patterns
|
342
|
+
else
|
343
|
+
1 # Single value
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
# Calculate total possible combinations of tag values
|
348
|
+
# @param tag_definitions [Hash] Hash of tag name to definition
|
349
|
+
# @return [Integer] Total combinations possible
|
350
|
+
def calculate_tag_combinations(tag_definitions)
|
351
|
+
return 1 if tag_definitions.empty?
|
352
|
+
|
353
|
+
# Multiply the number of possible values for each tag
|
354
|
+
tag_definitions.values.reduce(1) do |total, tag_def|
|
355
|
+
total * count_tag_values(tag_def)
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
360
|
+
# Formatter classes
|
361
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
362
|
+
class BaseFormatter
|
363
|
+
attr_reader :stdout, :stderr, :color,
|
364
|
+
:analysis_result,
|
365
|
+
:total_unique_metrics,
|
366
|
+
:metrics_analysis,
|
367
|
+
:total_possible_custom_metrics
|
368
|
+
|
369
|
+
def initialize(stdout:, stderr:, color:, analysis_result:)
|
370
|
+
@stdout = stdout
|
371
|
+
@stderr = stderr
|
372
|
+
@color = color
|
373
|
+
@analysis_result = analysis_result.to_h.transform_values { |v| v.is_a?(Data) ? v.to_h : v }
|
374
|
+
@total_unique_metrics = @analysis_result[:total_unique_metrics]
|
375
|
+
@metrics_analysis = @analysis_result[:metrics_analysis]
|
376
|
+
@total_possible_custom_metrics = @analysis_result[:total_possible_custom_metrics]
|
377
|
+
end
|
378
|
+
|
379
|
+
def render
|
380
|
+
raise NotImplementedError, "Subclasses must implement this method"
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
class TextFormatter < BaseFormatter
|
385
|
+
attr_reader :output
|
386
|
+
|
387
|
+
def render
|
388
|
+
@output = StringIO.new
|
389
|
+
format_analysis_output
|
390
|
+
@output.string
|
391
|
+
end
|
392
|
+
|
393
|
+
private
|
394
|
+
|
395
|
+
# Format the analysis output for display
|
396
|
+
def format_analysis_output
|
397
|
+
format_metric_analysis_header(output)
|
398
|
+
|
399
|
+
analysis_result[:metrics_analysis].each do |analysis|
|
400
|
+
output.puts
|
401
|
+
format_metric_analysis(output, analysis)
|
402
|
+
line(output, placement: :flat)
|
403
|
+
end
|
404
|
+
|
405
|
+
summary(
|
406
|
+
output,
|
407
|
+
analysis_result[:total_unique_metrics],
|
408
|
+
analysis_result[:total_possible_custom_metrics]
|
409
|
+
)
|
410
|
+
output.string
|
411
|
+
end
|
412
|
+
|
413
|
+
def line(output, placement: :top)
|
414
|
+
if placement == :top
|
415
|
+
output.puts "┌──────────────────────────────────────────────────────────────────────────────────────────────┐".white.on.blue
|
416
|
+
elsif placement == :bottom
|
417
|
+
output.puts "└──────────────────────────────────────────────────────────────────────────────────────────────┘".white.on.blue
|
418
|
+
elsif placement == :middle
|
419
|
+
output.puts "├──────────────────────────────────────────────────────────────────────────────────────────────┤".white.on.blue
|
420
|
+
elsif placement == :flat
|
421
|
+
output.puts " ──────────────────────────────────────────────────────────────────────────────────────────────".white.bold
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def summary(output, total_unique_metrics, total_possible_custom_metrics)
|
426
|
+
line(output)
|
427
|
+
output.puts "│ Schema Analysis Results: │".yellow.bold.on.blue
|
428
|
+
output.puts "│ SUMMARY │".white.on.blue
|
429
|
+
line(output, placement: :bottom)
|
430
|
+
output.puts
|
431
|
+
output.puts " Total unique metrics: #{("%3d" % total_unique_metrics).bold.green}"
|
432
|
+
output.puts "Total possible custom metric combinations: #{("%3d" % total_possible_custom_metrics).bold.green}"
|
433
|
+
output.puts
|
434
|
+
end
|
435
|
+
|
436
|
+
def format_metric_analysis_header(output)
|
437
|
+
line(output)
|
438
|
+
output.puts "│ Detailed Metric Analysis: │".white.on.blue
|
439
|
+
line(output, placement: :bottom)
|
440
|
+
end
|
441
|
+
|
442
|
+
def format_metric_analysis(output, analysis)
|
443
|
+
output.puts " • #{analysis[:metric_type].to_s.cyan}('#{analysis[:metric_name].yellow.bold}')"
|
444
|
+
if analysis[:expanded_names].size > 1
|
445
|
+
output.puts " Expanded names:"
|
446
|
+
output.print " • ".yellow
|
447
|
+
output.puts analysis[:expanded_names].join("\n • ").yellow
|
448
|
+
end
|
449
|
+
output.puts
|
450
|
+
output.puts " Unique tags: #{("%3d" % analysis[:unique_tags]).bold.green}"
|
451
|
+
output.puts " Total tag values: #{("%3d" % analysis[:unique_tag_values]).bold.green}"
|
452
|
+
output.puts " Possible combinations: #{("%3d" % analysis[:total_combinations]).bold.green}"
|
453
|
+
output.puts
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
458
|
+
# JSON Formatter classes
|
459
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
460
|
+
class JSONFormatter < BaseFormatter
|
461
|
+
def render
|
462
|
+
JSON.pretty_generate(analysis_result.to_h)
|
463
|
+
end
|
464
|
+
end
|
465
|
+
|
466
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
467
|
+
# YAML Formatter classes
|
468
|
+
# ——————————————————————————————————————————————————————————————————————————————————————————————————————————————-
|
469
|
+
class YAMLFormatter < BaseFormatter
|
470
|
+
def render
|
471
|
+
YAML.dump(analysis_result.to_h)
|
472
|
+
end
|
473
|
+
end
|
474
|
+
end
|
475
|
+
end
|
476
|
+
end
|
477
|
+
end
|
@@ -0,0 +1,63 @@
|
|
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", default: true
|
22
|
+
option :format, aliases: %w[-o], type: :string, required: false, desc: "Output format, supports: json, yaml, text", default: :text
|
23
|
+
|
24
|
+
# @description Analyze a schema file for metrics and validation
|
25
|
+
# @param options [Hash] The options for the command
|
26
|
+
# @option options [String] :file The path to the schema file to analyze
|
27
|
+
# @option options [Boolean] :color Enable/Disable color output
|
28
|
+
# @return [void]
|
29
|
+
def call(**options)
|
30
|
+
file = options[:file]
|
31
|
+
|
32
|
+
unless file
|
33
|
+
warn "Error: --file option is required"
|
34
|
+
warn "Usage: dss analyze --file <schema.rb>"
|
35
|
+
exit 1
|
36
|
+
end
|
37
|
+
|
38
|
+
warn "Analyzing Schema File:"
|
39
|
+
warn " • file #{file.green}"
|
40
|
+
warn " • color: #{(options[:color] ? "enabled" : "disabled").yellow}"
|
41
|
+
warn " • formar #{options[:format].to_s.red}"
|
42
|
+
@schema = ::Datadog::Statsd::Schema.load_file(file)
|
43
|
+
::Datadog::Statsd::Schema::Analyzer.new([@schema],
|
44
|
+
format: (options[:format] || :text).to_sym,
|
45
|
+
stdout: self.class.stdout,
|
46
|
+
stderr: self.class.stderr,
|
47
|
+
color: options[:color]).tap do |analyzer|
|
48
|
+
puts analyzer.render
|
49
|
+
end.analyze
|
50
|
+
end
|
51
|
+
|
52
|
+
def warn(...)
|
53
|
+
self.class.stderr.puts(...)
|
54
|
+
end
|
55
|
+
|
56
|
+
def puts(...)
|
57
|
+
self.class.stdout.puts(...)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -27,7 +27,7 @@ module Datadog
|
|
27
27
|
# @author Datadog Team
|
28
28
|
# @since 0.1.0
|
29
29
|
class Namespace < Dry::Struct
|
30
|
-
# Include the types module for easier access to Dry::
|
30
|
+
# Include the types module for easier access to Dry::Typesa
|
31
31
|
module Types
|
32
32
|
include Dry.Types()
|
33
33
|
end
|