diff --git a/prometheus/registry.go b/prometheus/registry.go index ed0681c8b..9f4887d4d 100644 --- a/prometheus/registry.go +++ b/prometheus/registry.go @@ -679,7 +679,7 @@ func processMetric( } metricFamily, ok := metricFamiliesByName[desc.fqName] if ok { // Existing name. - if metricFamily.GetHelp() != desc.help { + if !helpTextMatches(metricFamily.GetHelp(), desc.help) { return fmt.Errorf( "collected metric %s %s has help %q but should have %q", desc.fqName, dtoMetric, desc.help, metricFamily.GetHelp(), @@ -812,7 +812,7 @@ func (gs Gatherers) Gather() ([]*dto.MetricFamily, error) { for _, mf := range mfs { existingMF, exists := metricFamiliesByName[mf.GetName()] if exists { - if existingMF.GetHelp() != mf.GetHelp() { + if !helpTextMatches(existingMF.GetHelp(), mf.GetHelp()) { errs = append(errs, fmt.Errorf( "gathered metric family %s has help %q but should have %q", mf.GetName(), mf.GetHelp(), existingMF.GetHelp(), @@ -1001,7 +1001,7 @@ func checkDescConsistency( desc *Desc, ) error { // Desc help consistency with metric family help. - if metricFamily.GetHelp() != desc.help { + if !helpTextMatches(metricFamily.GetHelp(), desc.help) { return fmt.Errorf( "collected metric %s %s has help %q but should have %q", metricFamily.GetName(), dtoMetric, metricFamily.GetHelp(), desc.help, @@ -1036,6 +1036,20 @@ func checkDescConsistency( return nil } +// helpTextMatches preserves compatibility with collectors that changed only a +// final period in their help text. This is needed when applications using +// different client versions share a registry, but other help-text changes +// remain strict consistency errors. +func helpTextMatches(a, b string) bool { + if a == b { + return true + } + if len(a) > 1 && strings.HasSuffix(a, ".") && strings.TrimSuffix(a, ".") == b { + return true + } + return len(b) > 1 && strings.HasSuffix(b, ".") && strings.TrimSuffix(b, ".") == a +} + var _ TransactionalGatherer = &MultiTRegistry{} // MultiTRegistry is a TransactionalGatherer that joins gathered metrics from multiple diff --git a/prometheus/registry_test.go b/prometheus/registry_test.go index 379984fef..e1360e55f 100644 --- a/prometheus/registry_test.go +++ b/prometheus/registry_test.go @@ -748,6 +748,42 @@ func TestHandler(t *testing.T) { testHandler(t) } +func TestGatherAllowsHelpTextFinalPeriodDifference(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + registry.MustRegister(prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "period_compatibility", + Help: "help text", + ConstLabels: prometheus.Labels{"source": "old"}, + })) + registry.MustRegister(uncheckedCollector{c: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "period_compatibility", + Help: "help text.", + ConstLabels: prometheus.Labels{"source": "new"}, + })}) + + if _, err := registry.Gather(); err != nil { + t.Fatalf("Gather returned an error for a final-period-only difference: %v", err) + } +} + +func TestGatherRejectsOtherHelpTextDifference(t *testing.T) { + registry := prometheus.NewPedanticRegistry() + registry.MustRegister(prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "period_compatibility_strict", + Help: "help text", + ConstLabels: prometheus.Labels{"source": "old"}, + })) + registry.MustRegister(uncheckedCollector{c: prometheus.NewGauge(prometheus.GaugeOpts{ + Name: "period_compatibility_strict", + Help: "different help text", + ConstLabels: prometheus.Labels{"source": "new"}, + })}) + + if _, err := registry.Gather(); err == nil { + t.Fatal("Gather accepted a non-period help-text difference") + } +} + func BenchmarkHandler(b *testing.B) { for i := 0; i < b.N; i++ { testHandler(b)