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.
- checksums.yaml +4 -4
- data/.envrc +2 -0
- data/.rspec +2 -1
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +32 -47
- data/FUTURE_DIRECTION.md +32 -0
- data/README.md +309 -258
- data/Rakefile +7 -7
- data/examples/schema/example_marathon.rb +29 -0
- data/examples/shared.rb +1 -1
- data/exe/dss +8 -0
- data/lib/datadog/statsd/emitter.rb +102 -21
- data/lib/datadog/statsd/schema/analyzer.rb +397 -0
- data/lib/datadog/statsd/schema/cli.rb +16 -0
- data/lib/datadog/statsd/schema/commands/analyze.rb +52 -0
- data/lib/datadog/statsd/schema/commands.rb +14 -0
- data/lib/datadog/statsd/schema/errors.rb +54 -1
- data/lib/datadog/statsd/schema/metric_definition.rb +86 -3
- data/lib/datadog/statsd/schema/namespace.rb +91 -5
- data/lib/datadog/statsd/schema/schema_builder.rb +162 -4
- data/lib/datadog/statsd/schema/tag_definition.rb +66 -6
- data/lib/datadog/statsd/schema/version.rb +6 -1
- data/lib/datadog/statsd/schema.rb +91 -13
- metadata +25 -4
- data/exe/datadog-statsd-schema +0 -3
data/Rakefile
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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(
|
13
|
+
shell("rm -rf pkg/ tmp/ coverage/ doc/ ")
|
14
14
|
end
|
15
15
|
|
16
16
|
task gem: [:build] do
|
17
|
-
shell(
|
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
data/exe/dss
ADDED
@@ -51,14 +51,32 @@ module Datadog
|
|
51
51
|
end
|
52
52
|
|
53
53
|
extend Forwardable
|
54
|
-
def_delegators :datadog_statsd,
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
127
|
-
|
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
|
369
|
+
# Exclude framework tags and global tags from validation
|
296
370
|
framework_tags = %i[emitter ab_test_name ab_test_group]
|
297
|
-
|
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
|
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
|
-
|
525
|
+
stderr.puts "Schema Validation Error: #{error.message}"
|
445
526
|
else
|
446
|
-
|
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
|
-
|
533
|
+
stderr.puts "Schema Validation Warning: #{error.message}"
|
453
534
|
else
|
454
|
-
|
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
|