diff --git a/main.go b/main.go index 2f139e61..91c3ff98 100644 --- a/main.go +++ b/main.go @@ -178,7 +178,7 @@ func serveHTTP(mux http.Handler, listenAddress string, logger *slog.Logger) { os.Exit(1) } -func sighupConfigReloader(fileName string, mapper *mapper.MetricMapper, logger *slog.Logger) { +func sighupConfigReloader(fileName string, mapper *mapper.MetricMapper, registry exporter.Registry, logger *slog.Logger) { signals := make(chan os.Signal, 1) signal.Notify(signals, syscall.SIGHUP) @@ -190,16 +190,19 @@ func sighupConfigReloader(fileName string, mapper *mapper.MetricMapper, logger * logger.Info("Received signal, attempting reload", "signal", s) - reloadConfig(fileName, mapper, logger) + reloadConfig(fileName, mapper, registry, logger) } } -func reloadConfig(fileName string, mapper *mapper.MetricMapper, logger *slog.Logger) { +func reloadConfig(fileName string, mapper *mapper.MetricMapper, registry exporter.Registry, logger *slog.Logger) { err := mapper.InitFromFile(fileName) if err != nil { logger.Info("Error reloading config", "error", err) configLoads.WithLabelValues("failure").Inc() } else { + // Reset registry so that metrics with updated bucket/quantile settings + // are re-registered with the new configuration. + registry.Reset() logger.Info("Config reloaded successfully") configLoads.WithLabelValues("success").Inc() } @@ -327,6 +330,7 @@ func main() { } exporter := exporter.NewExporter(prometheus.DefaultRegisterer, thisMapper, logger, eventsActions, eventsUnmapped, errorEventStats, eventStats, conflictingEventStats, metricsCount) + exporterRegistry := exporter.Registry if *checkConfig { logger.Info("Configuration check successful, exiting") @@ -519,7 +523,7 @@ func main() { return } logger.Info("Received lifecycle api reload, attempting reload") - reloadConfig(*mappingConfig, thisMapper, logger) + reloadConfig(*mappingConfig, thisMapper, exporterRegistry, logger) } }) mux.HandleFunc("/-/quit", func(w http.ResponseWriter, r *http.Request) { @@ -548,7 +552,7 @@ func main() { go serveHTTP(mux, *listenAddress, logger) - go sighupConfigReloader(*mappingConfig, thisMapper, logger) + go sighupConfigReloader(*mappingConfig, thisMapper, exporterRegistry, logger) go exporter.Listen(events) signals := make(chan os.Signal, 1) diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go index 07d5a461..196aef30 100644 --- a/pkg/exporter/exporter.go +++ b/pkg/exporter/exporter.go @@ -37,6 +37,9 @@ type Registry interface { GetHistogram(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error) GetSummary(metricName string, labels prometheus.Labels, help string, mapping *mapper.MetricMapping, metricsCount *prometheus.GaugeVec) (prometheus.Observer, error) RemoveStaleMetrics() + // Reset clears all cached metrics, forcing re-registration on next use. + // This is needed after config reload to pick up new bucket/quantile settings. + Reset() } type Exporter struct { diff --git a/pkg/registry/registry.go b/pkg/registry/registry.go index 825cb14c..7e79a000 100644 --- a/pkg/registry/registry.go +++ b/pkg/registry/registry.go @@ -378,6 +378,10 @@ func (r *Registry) GetSummary(metricName string, labels prometheus.Labels, help return observer, nil } +func (r *Registry) Reset() { + r.Metrics = make(map[string]metrics.Metric) +} + func (r *Registry) RemoveStaleMetrics() { now := clock.Now() // delete timeseries with expired ttl diff --git a/pkg/registry/registry_test.go b/pkg/registry/registry_test.go new file mode 100644 index 00000000..b6a18b49 --- /dev/null +++ b/pkg/registry/registry_test.go @@ -0,0 +1,75 @@ +// Copyright 2013 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 registry + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/statsd_exporter/pkg/mapper" + "github.com/prometheus/statsd_exporter/pkg/metrics" +) + +func TestRegistryReset(t *testing.T) { + reg := prometheus.NewRegistry() + m := &mapper.MetricMapper{} + r := NewRegistry(reg, m) + + // Use Store directly with correct signature + metric := metrics.Metric{ + MetricType: metrics.CounterMetricType, + Vectors: make(map[metrics.NameHash]*metrics.Vector), + Metrics: make(map[metrics.ValueHash]*metrics.RegisteredMetric), + } + r.Metrics["test_metric"] = metric + + if len(r.Metrics) != 1 { + t.Fatalf("expected 1 metric, got %d", len(r.Metrics)) + } + + // Reset should clear all metrics + r.Reset() + + if len(r.Metrics) != 0 { + t.Fatalf("expected 0 metrics after reset, got %d", len(r.Metrics)) + } +} + +func TestRegistryResetAfterStore(t *testing.T) { + reg := prometheus.NewRegistry() + m := &mapper.MetricMapper{} + r := NewRegistry(reg, m) + + // Simulate storing metrics with different types + r.Metrics["metric1"] = metrics.Metric{ + MetricType: metrics.CounterMetricType, + Vectors: make(map[metrics.NameHash]*metrics.Vector), + Metrics: make(map[metrics.ValueHash]*metrics.RegisteredMetric), + } + r.Metrics["metric2"] = metrics.Metric{ + MetricType: metrics.GaugeMetricType, + Vectors: make(map[metrics.NameHash]*metrics.Vector), + Metrics: make(map[metrics.ValueHash]*metrics.RegisteredMetric), + } + + if len(r.Metrics) != 2 { + t.Fatalf("expected 2 metrics, got %d", len(r.Metrics)) + } + + r.Reset() + + if len(r.Metrics) != 0 { + t.Fatalf("expected 0 metrics after reset, got %d", len(r.Metrics)) + } +}