diff --git a/examples/introspection/main.go b/examples/introspection/main.go new file mode 100644 index 000000000..1d01a18c1 --- /dev/null +++ b/examples/introspection/main.go @@ -0,0 +1,229 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// This example demonstrates how to use Registry.Descriptors() to introspect +// all registered metrics programmatically. This is useful for generating +// documentation, validating metric configurations, or implementing custom +// metric discovery systems. +package main + +import ( + "flag" + "fmt" + "sort" + "strings" + + "github.com/prometheus/client_golang/prometheus" +) + +var mode = flag.String("mode", "list", "Mode: list, docs, or filter") +var filter = flag.String("filter", "http", "Prefix to filter metrics by (only used in 'filter' mode)") + +func main() { + flag.Parse() + + // Create a custom registry + reg := prometheus.NewRegistry() + + // Register various metrics to demonstrate the feature + registerExampleMetrics(reg) + + // Get all registered descriptors + descs := reg.Descriptors() + + // Sort by name for consistent output + sort.Slice(descs, func(i, j int) bool { + return descs[i].Name() < descs[j].Name() + }) + + switch *mode { + case "list": + listMetrics(descs) + case "docs": + generateDocumentation(descs) + case "filter": + filterMetrics(descs, *filter) + default: + fmt.Printf("Unknown mode: %s\n", *mode) + fmt.Println("Available modes: list, docs, filter") + } +} + +func registerExampleMetrics(reg *prometheus.Registry) { + // HTTP metrics + httpRequests := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total number of HTTP requests", + }, + []string{"method", "status"}, + ) + reg.MustRegister(httpRequests) + + httpDuration := prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "http_request_duration_seconds", + Help: "HTTP request duration in seconds", + }, + []string{"method", "path"}, + ) + reg.MustRegister(httpDuration) + + httpErrors := prometheus.NewCounter( + prometheus.CounterOpts{ + Name: "http_errors_total", + Help: "Total number of HTTP errors", + }, + ) + reg.MustRegister(httpErrors) + + // Database metrics + dbConnections := prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "database_connections", + Help: "Number of active database connections", + ConstLabels: prometheus.Labels{"service": "api", "version": "1.0"}, + }, + []string{"state"}, + ) + reg.MustRegister(dbConnections) + + dbQueryDuration := prometheus.NewHistogram( + prometheus.HistogramOpts{ + Name: "database_query_duration_seconds", + Help: "Database query duration in seconds", + }, + ) + reg.MustRegister(dbQueryDuration) + + // System metrics + cpuUsage := prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "cpu_usage_percent", + Help: "Current CPU usage percentage", + }, + ) + reg.MustRegister(cpuUsage) + + memoryUsage := prometheus.NewGauge( + prometheus.GaugeOpts{ + Name: "memory_usage_bytes", + Help: "Current memory usage in bytes", + }, + ) + reg.MustRegister(memoryUsage) +} + +func listMetrics(descs []*prometheus.Desc) { + fmt.Println("=== Registered Metrics ===") + fmt.Println() + + for i, desc := range descs { + fmt.Printf("%d. %s\n", i+1, desc.Name()) + fmt.Printf(" Help: %s\n", desc.Help()) + + if labels := desc.VariableLabels(); len(labels) > 0 { + fmt.Printf(" Variable Labels: %v\n", labels) + } + + if constLabels := desc.ConstLabels(); len(constLabels) > 0 { + fmt.Printf(" Constant Labels: %v\n", constLabels) + } + + fmt.Println() + } + + fmt.Printf("Total metrics: %d\n", len(descs)) +} + +func generateDocumentation(descs []*prometheus.Desc) { + fmt.Println("# Metrics Documentation") + fmt.Println() + fmt.Println("This documentation was automatically generated from the registry.") + fmt.Println() + + // Group metrics by prefix + groups := make(map[string][]*prometheus.Desc) + for _, desc := range descs { + parts := strings.SplitN(desc.Name(), "_", 2) + prefix := parts[0] + groups[prefix] = append(groups[prefix], desc) + } + + // Get sorted group names + var groupNames []string + for name := range groups { + groupNames = append(groupNames, name) + } + sort.Strings(groupNames) + + // Print each group + for _, groupName := range groupNames { + fmt.Printf("## %s Metrics\n\n", strings.Title(groupName)) + + for _, desc := range groups[groupName] { + fmt.Printf("### `%s`\n\n", desc.Name()) + fmt.Printf("%s\n\n", desc.Help()) + + if labels := desc.VariableLabels(); len(labels) > 0 { + fmt.Printf("**Labels:**\n") + for _, label := range labels { + fmt.Printf("- `%s`\n", label) + } + fmt.Println() + } + + if constLabels := desc.ConstLabels(); len(constLabels) > 0 { + fmt.Printf("**Constant Labels:**\n") + var keys []string + for k := range constLabels { + keys = append(keys, k) + } + sort.Strings(keys) + for _, k := range keys { + fmt.Printf("- `%s`: `%s`\n", k, constLabels[k]) + } + fmt.Println() + } + } + } +} + +func filterMetrics(descs []*prometheus.Desc, prefix string) { + fmt.Printf("=== Metrics matching prefix '%s' ===\n\n", prefix) + + var matches []*prometheus.Desc + for _, desc := range descs { + if strings.HasPrefix(desc.Name(), prefix) { + matches = append(matches, desc) + } + } + + if len(matches) == 0 { + fmt.Printf("No metrics found with prefix '%s'\n", prefix) + return + } + + for _, desc := range matches { + fmt.Printf("• %s\n", desc.Name()) + fmt.Printf(" %s\n", desc.Help()) + + if labels := desc.VariableLabels(); len(labels) > 0 { + fmt.Printf(" Labels: %v\n", labels) + } + + fmt.Println() + } + + fmt.Printf("Found %d matching metric(s)\n", len(matches)) +} diff --git a/prometheus/desc.go b/prometheus/desc.go index 46dd59ac5..7a7b48c78 100644 --- a/prometheus/desc.go +++ b/prometheus/desc.go @@ -218,3 +218,49 @@ func (d *Desc) String() string { strings.Join(vlStrings, ","), ) } + +// Name returns the fully-qualified name of the metric descriptor. +// +// The fully-qualified name is constructed from the namespace, subsystem, and +// name components provided when the Desc was created. +func (d *Desc) Name() string { + return d.fqName +} + +// Help returns the help string for the metric descriptor. +// +// This is the human-readable description of what the metric measures. +func (d *Desc) Help() string { + return d.help +} + +// ConstLabels returns the constant labels as a Labels map. +// +// Constant labels are key-value pairs that are always attached to the metric +// and have the same value for all time series of this metric family. +// +// The returned map is a copy; modifying it will not affect the descriptor. +func (d *Desc) ConstLabels() Labels { + labels := Labels{} + for _, lp := range d.constLabelPairs { + labels[lp.GetName()] = lp.GetValue() + } + return labels +} + +// VariableLabels returns the names of the variable labels. +// +// Variable labels are label names for which the metric can have different +// values across different time series in the same metric family. +// +// The returned slice is a copy of the internal slice; modifying it will not +// affect the descriptor. Returns nil if the descriptor has no variable labels. +func (d *Desc) VariableLabels() []string { + if d.variableLabels == nil || len(d.variableLabels.names) == 0 { + return nil + } + // Return a copy to prevent external modification + labels := make([]string, len(d.variableLabels.names)) + copy(labels, d.variableLabels.names) + return labels +} diff --git a/prometheus/desc_test.go b/prometheus/desc_test.go index 0570d5bd1..ea5f26082 100644 --- a/prometheus/desc_test.go +++ b/prometheus/desc_test.go @@ -75,3 +75,248 @@ func TestNewInvalidDesc_String(t *testing.T) { t.Errorf("String: unexpected output: %s", desc.String()) } } + +func TestDesc_Name(t *testing.T) { + tests := []struct { + name string + fqName string + expected string + }{ + { + name: "simple name", + fqName: "my_metric", + expected: "my_metric", + }, + { + name: "namespaced metric", + fqName: "namespace_subsystem_metric_name", + expected: "namespace_subsystem_metric_name", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + desc := NewDesc(tt.fqName, "help text", nil, nil) + if got := desc.Name(); got != tt.expected { + t.Errorf("Name() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestDesc_Help(t *testing.T) { + tests := []struct { + name string + help string + expected string + }{ + { + name: "simple help", + help: "This is a help string", + expected: "This is a help string", + }, + { + name: "empty help", + help: "", + expected: "", + }, + { + name: "multiline help", + help: "Line 1\nLine 2", + expected: "Line 1\nLine 2", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + desc := NewDesc("my_metric", tt.help, nil, nil) + if got := desc.Help(); got != tt.expected { + t.Errorf("Help() = %q, want %q", got, tt.expected) + } + }) + } +} + +func TestDesc_ConstLabels(t *testing.T) { + tests := []struct { + name string + labels Labels + expected Labels + }{ + { + name: "no labels", + labels: nil, + expected: Labels{}, + }, + { + name: "empty labels", + labels: Labels{}, + expected: Labels{}, + }, + { + name: "single label", + labels: Labels{"env": "prod"}, + expected: Labels{"env": "prod"}, + }, + { + name: "multiple labels", + labels: Labels{ + "env": "prod", + "region": "us-west", + "version": "1.0", + }, + expected: Labels{ + "env": "prod", + "region": "us-west", + "version": "1.0", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + desc := NewDesc("my_metric", "help", nil, tt.labels) + got := desc.ConstLabels() + + // Check length + if len(got) != len(tt.expected) { + t.Errorf("ConstLabels() length = %d, want %d", len(got), len(tt.expected)) + } + + // Check all expected labels are present with correct values + for k, v := range tt.expected { + if gotVal, ok := got[k]; !ok { + t.Errorf("ConstLabels() missing key %q", k) + } else if gotVal != v { + t.Errorf("ConstLabels()[%q] = %q, want %q", k, gotVal, v) + } + } + + // Ensure returned map is a copy (modifying it shouldn't affect the descriptor) + if len(got) > 0 { + got["test_key"] = "test_value" + got2 := desc.ConstLabels() + if _, exists := got2["test_key"]; exists { + t.Error("ConstLabels() should return a copy, not the internal map") + } + } + }) + } +} + +func TestDesc_VariableLabels(t *testing.T) { + tests := []struct { + name string + labels []string + expected []string + }{ + { + name: "no labels", + labels: nil, + expected: nil, + }, + { + name: "empty labels", + labels: []string{}, + expected: nil, + }, + { + name: "single label", + labels: []string{"method"}, + expected: []string{"method"}, + }, + { + name: "multiple labels", + labels: []string{"method", "status", "path"}, + expected: []string{"method", "status", "path"}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + desc := NewDesc("my_metric", "help", tt.labels, nil) + got := desc.VariableLabels() + + // Check nil vs non-nil + if (got == nil) != (tt.expected == nil) { + t.Errorf("VariableLabels() = %v (nil=%t), want %v (nil=%t)", + got, got == nil, tt.expected, tt.expected == nil) + return + } + + // If both are nil, test passes + if got == nil && tt.expected == nil { + return + } + + // Check length + if len(got) != len(tt.expected) { + t.Errorf("VariableLabels() length = %d, want %d", len(got), len(tt.expected)) + } + + // Check all labels are present in order + for i, label := range tt.expected { + if i >= len(got) || got[i] != label { + t.Errorf("VariableLabels()[%d] = %q, want %q", i, got[i], label) + } + } + + // Ensure returned slice is a copy (modifying it shouldn't affect the descriptor) + if len(got) > 0 { + got[0] = "modified" + got2 := desc.VariableLabels() + if got2[0] == "modified" { + t.Error("VariableLabels() should return a copy, not the internal slice") + } + } + }) + } +} + +func TestDesc_GettersComprehensive(t *testing.T) { + // Create a descriptor with all fields populated + desc := NewDesc( + "my_namespace_my_subsystem_my_metric", + "This is a comprehensive help text", + []string{"label1", "label2", "label3"}, + Labels{"const1": "value1", "const2": "value2"}, + ) + + if desc.Err() != nil { + t.Fatalf("Unexpected error creating desc: %v", desc.Err()) + } + + // Test Name() + if got := desc.Name(); got != "my_namespace_my_subsystem_my_metric" { + t.Errorf("Name() = %q, want %q", got, "my_namespace_my_subsystem_my_metric") + } + + // Test Help() + if got := desc.Help(); got != "This is a comprehensive help text" { + t.Errorf("Help() = %q, want %q", got, "This is a comprehensive help text") + } + + // Test ConstLabels() + constLabels := desc.ConstLabels() + if len(constLabels) != 2 { + t.Errorf("ConstLabels() len = %d, want 2", len(constLabels)) + } + if constLabels["const1"] != "value1" { + t.Errorf("ConstLabels()[const1] = %q, want %q", constLabels["const1"], "value1") + } + if constLabels["const2"] != "value2" { + t.Errorf("ConstLabels()[const2] = %q, want %q", constLabels["const2"], "value2") + } + + // Test VariableLabels() + varLabels := desc.VariableLabels() + if len(varLabels) != 3 { + t.Errorf("VariableLabels() len = %d, want 3", len(varLabels)) + } + expectedVarLabels := []string{"label1", "label2", "label3"} + for i, expected := range expectedVarLabels { + if varLabels[i] != expected { + t.Errorf("VariableLabels()[%d] = %q, want %q", i, varLabels[i], expected) + } + } +} diff --git a/prometheus/example_desc_getters_test.go b/prometheus/example_desc_getters_test.go new file mode 100644 index 000000000..39f9fc27d --- /dev/null +++ b/prometheus/example_desc_getters_test.go @@ -0,0 +1,94 @@ +// Copyright 2025 The Prometheus Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package prometheus_test + +import ( + "fmt" + + "github.com/prometheus/client_golang/prometheus" +) + +// ExampleDesc_getters demonstrates how to use the Desc getter methods +// to introspect metric metadata programmatically. +func ExampleDesc_getters() { + // Create a descriptor directly + desc := prometheus.NewDesc( + "myapp_http_requests_total", + "Total number of HTTP requests", + []string{"method", "status"}, + prometheus.Labels{ + "service": "api", + "version": "1.0", + }, + ) + + // Use getter methods to access metadata + fmt.Println("Metric Name:", desc.Name()) + fmt.Println("Help Text:", desc.Help()) + fmt.Println("Constant Labels:", desc.ConstLabels()) + fmt.Println("Variable Labels:", desc.VariableLabels()) + + // Output: + // Metric Name: myapp_http_requests_total + // Help Text: Total number of HTTP requests + // Constant Labels: map[service:api version:1.0] + // Variable Labels: [method status] +} + +// ExampleDesc_getters_documentation shows how to generate documentation +// from metric descriptors programmatically. +func ExampleDesc_getters_documentation() { + // Create descriptors + descriptors := []*prometheus.Desc{ + prometheus.NewDesc( + "http_requests_total", + "Total number of HTTP requests", + nil, + nil, + ), + prometheus.NewDesc( + "http_request_duration_seconds", + "HTTP request duration in seconds", + []string{"method", "path"}, + nil, + ), + } + + // Generate documentation + fmt.Println("# Available Metrics") + fmt.Println() + for _, desc := range descriptors { + fmt.Printf("## %s\n", desc.Name()) + fmt.Printf("**Help:** %s\n", desc.Help()) + + if labels := desc.VariableLabels(); len(labels) > 0 { + fmt.Printf("**Labels:** %v\n", labels) + } + + if constLabels := desc.ConstLabels(); len(constLabels) > 0 { + fmt.Printf("**Constant Labels:** %v\n", constLabels) + } + fmt.Println() + } + + // Output: + // # Available Metrics + // + // ## http_requests_total + // **Help:** Total number of HTTP requests + // + // ## http_request_duration_seconds + // **Help:** HTTP request duration in seconds + // **Labels:** [method path] +} diff --git a/prometheus/registry.go b/prometheus/registry.go index c6fd2f58b..6bc68c559 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -584,6 +584,47 @@ func (r *Registry) Collect(ch chan<- Metric) { } } +// Descriptors returns all metric descriptors currently registered with this registry. +// +// This method is useful for introspection, documentation generation, and testing. +// Duplicate descriptors are automatically deduplicated. Unchecked collectors are +// excluded from the results. +func (r *Registry) Descriptors() []*Desc { + r.mtx.RLock() + defer r.mtx.RUnlock() + + // Use a map to deduplicate descriptors by their ID + descMap := make(map[uint64]*Desc) + + // Collect descriptors from all registered collectors + for _, c := range r.collectorsByID { + descChan := make(chan *Desc, capDescChan) + + // Start a goroutine to collect descriptors + go func(collector Collector) { + collector.Describe(descChan) + close(descChan) + }(c) + + // Collect all descriptors from the channel + for desc := range descChan { + // Deduplicate by descriptor ID + // This handles both: + // 1. Same descriptor returned multiple times by one collector + // 2. Same descriptor returned by different collectors + descMap[desc.id] = desc + } + } + + // Convert map to slice + descs := make([]*Desc, 0, len(descMap)) + for _, desc := range descMap { + descs = append(descs, desc) + } + + return descs +} + // WriteToTextfile calls Gather on the provided Gatherer, encodes the result in the // Prometheus text format, and writes it to a temporary file. Upon success, the // temporary file is renamed to the provided filename. diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 12b09d623..80aed01d0 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -1365,3 +1365,255 @@ func TestGatherDoesNotLeakGoroutines(t *testing.T) { } } } + +func TestRegistryDescriptors(t *testing.T) { + reg := prometheus.NewRegistry() + + // Test 1: Empty registry + descs := reg.Descriptors() + if len(descs) != 0 { + t.Errorf("Empty registry should have 0 descriptors, got %d", len(descs)) + } + + // Test 2: Single metric + counter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "test_counter", + Help: "A test counter", + }) + reg.MustRegister(counter) + + descs = reg.Descriptors() + if len(descs) != 1 { + t.Errorf("Expected 1 descriptor, got %d", len(descs)) + } + if descs[0].Name() != "test_counter" { + t.Errorf("Expected descriptor name 'test_counter', got %q", descs[0].Name()) + } + if descs[0].Help() != "A test counter" { + t.Errorf("Expected help 'A test counter', got %q", descs[0].Help()) + } + + // Test 3: Multiple metrics + gauge := prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "test_gauge", + Help: "A test gauge", + }) + histogram := prometheus.NewHistogram(prometheus.HistogramOpts{ + Name: "test_histogram", + Help: "A test histogram", + }) + reg.MustRegister(gauge, histogram) + + descs = reg.Descriptors() + if len(descs) != 3 { + t.Errorf("Expected 3 descriptors, got %d", len(descs)) + } + + // Verify all metrics are present + names := make(map[string]bool) + for _, desc := range descs { + names[desc.Name()] = true + } + expectedNames := []string{"test_counter", "test_gauge", "test_histogram"} + for _, name := range expectedNames { + if !names[name] { + t.Errorf("Expected to find descriptor with name %q", name) + } + } +} + +func TestRegistryDescriptors_WithLabels(t *testing.T) { + reg := prometheus.NewRegistry() + + // Register metric with variable and constant labels + counterVec := prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total HTTP requests", + ConstLabels: prometheus.Labels{"service": "api", "version": "1.0"}, + }, + []string{"method", "status"}, + ) + reg.MustRegister(counterVec) + + descs := reg.Descriptors() + if len(descs) != 1 { + t.Fatalf("Expected 1 descriptor, got %d", len(descs)) + } + + desc := descs[0] + + // Check name and help + if desc.Name() != "http_requests_total" { + t.Errorf("Expected name 'http_requests_total', got %q", desc.Name()) + } + if desc.Help() != "Total HTTP requests" { + t.Errorf("Expected help 'Total HTTP requests', got %q", desc.Help()) + } + + // Check constant labels + constLabels := desc.ConstLabels() + if len(constLabels) != 2 { + t.Errorf("Expected 2 constant labels, got %d", len(constLabels)) + } + if constLabels["service"] != "api" { + t.Errorf("Expected service=api, got service=%q", constLabels["service"]) + } + if constLabels["version"] != "1.0" { + t.Errorf("Expected version=1.0, got version=%q", constLabels["version"]) + } + + // Check variable labels + varLabels := desc.VariableLabels() + if len(varLabels) != 2 { + t.Errorf("Expected 2 variable labels, got %d", len(varLabels)) + } + expectedVarLabels := map[string]bool{"method": true, "status": true} + for _, label := range varLabels { + if !expectedVarLabels[label] { + t.Errorf("Unexpected variable label: %q", label) + } + } +} + +// duplicateDescCollector is a test collector that returns the same descriptor twice +type duplicateDescCollector struct { + desc *prometheus.Desc +} + +func (d *duplicateDescCollector) Describe(ch chan<- *prometheus.Desc) { + // Send the same descriptor twice + ch <- d.desc + ch <- d.desc +} + +func (d *duplicateDescCollector) Collect(ch chan<- prometheus.Metric) { + // No-op for this test +} + +func TestRegistryDescriptors_Deduplication(t *testing.T) { + reg := prometheus.NewRegistry() + + // Create a custom collector that returns the same descriptor twice + desc := prometheus.NewDesc("duplicate_metric", "A metric returned twice", nil, nil) + collector := &duplicateDescCollector{desc: desc} + + reg.MustRegister(collector) + + descs := reg.Descriptors() + + // Should be deduplicated to 1 descriptor + if len(descs) != 1 { + t.Errorf("Expected deduplication to result in 1 descriptor, got %d", len(descs)) + } + + if descs[0].Name() != "duplicate_metric" { + t.Errorf("Expected descriptor name 'duplicate_metric', got %q", descs[0].Name()) + } +} + +func TestRegistryDescriptors_UncheckedCollector(t *testing.T) { + reg := prometheus.NewRegistry() + + // Register a regular metric + counter := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "checked_metric", + Help: "A checked metric", + }) + reg.MustRegister(counter) + + // Register an unchecked collector (Describe yields nothing) + unchecked := uncheckedCollector{ + c: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "unchecked_metric", + Help: "An unchecked metric", + }), + } + reg.MustRegister(unchecked) + + descs := reg.Descriptors() + + // Should only include the checked metric + if len(descs) != 1 { + t.Errorf("Expected 1 descriptor (unchecked should be excluded), got %d", len(descs)) + } + + if descs[0].Name() != "checked_metric" { + t.Errorf("Expected descriptor name 'checked_metric', got %q", descs[0].Name()) + } +} + +func TestRegistryDescriptors_Concurrent(t *testing.T) { + reg := prometheus.NewRegistry() + + // Register some metrics + for i := 0; i < 10; i++ { + reg.MustRegister(prometheus.NewCounter(prometheus.CounterOpts{ + Name: fmt.Sprintf("metric_%d", i), + Help: "Test metric", + })) + } + + // Call Descriptors concurrently from multiple goroutines + const goroutines = 10 + const iterations = 100 + + var wg sync.WaitGroup + wg.Add(goroutines) + + errors := make(chan error, goroutines*iterations) + + for g := 0; g < goroutines; g++ { + go func() { + defer wg.Done() + for i := 0; i < iterations; i++ { + descs := reg.Descriptors() + if len(descs) != 10 { + errors <- fmt.Errorf("expected 10 descriptors, got %d", len(descs)) + } + } + }() + } + + wg.Wait() + close(errors) + + // Check if any errors occurred + for err := range errors { + t.Error(err) + } +} + +func TestRegistryDescriptors_AfterUnregister(t *testing.T) { + reg := prometheus.NewRegistry() + + counter1 := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "metric_1", + Help: "First metric", + }) + counter2 := prometheus.NewCounter(prometheus.CounterOpts{ + Name: "metric_2", + Help: "Second metric", + }) + + reg.MustRegister(counter1, counter2) + + // Should have 2 descriptors + descs := reg.Descriptors() + if len(descs) != 2 { + t.Errorf("Expected 2 descriptors after registration, got %d", len(descs)) + } + + // Unregister one metric + reg.Unregister(counter1) + + // Should now have 1 descriptor + descs = reg.Descriptors() + if len(descs) != 1 { + t.Errorf("Expected 1 descriptor after unregistration, got %d", len(descs)) + } + + if descs[0].Name() != "metric_2" { + t.Errorf("Expected remaining descriptor to be 'metric_2', got %q", descs[0].Name()) + } +}