Repository: prometheus/statsd_exporter
Pull request #521 (2023-11-02 13:14:45)

Add configuration for keeping original labels in mappings
When a label is assigned in mappings that already exists, it will be replaced. This PR introduces a `honor_labels` parameter. When this parameter is set to true, it will preserve the original value of the label.


Patch:
diff --git a/pkg/exporter/exporter.go b/pkg/exporter/exporter.go
index 4bbed4d..f35cde5 100644
--- a/pkg/exporter/exporter.go
+++ b/pkg/exporter/exporter.go
@@ -105,6 +105,10 @@ func (b *Exporter) handleEvent(thisEvent event.Event) {
 		}
 		metricName = mapper.EscapeMetricName(mapping.Name)
 		for label, value := range labels {
+			if _, ok := prometheusLabels[label]; mapping.HonorLabels && ok {
+				continue
+			}
+
 			prometheusLabels[label] = value
 		}
 		b.EventsActions.WithLabelValues(string(mapping.Action)).Inc()
diff --git a/pkg/mapper/mapping.go b/pkg/mapper/mapping.go
index dda6423..80fb2a8 100644
--- a/pkg/mapper/mapping.go
+++ b/pkg/mapper/mapping.go
@@ -28,6 +28,7 @@ type MetricMapping struct {
 	nameFormatter    *fsm.TemplateFormatter
 	regex            *regexp.Regexp
 	Labels           prometheus.Labels `yaml:"labels"`
+	HonorLabels      bool              `yaml:"honor_labels"`
 	labelKeys        []string
 	labelFormatters  []*fsm.TemplateFormatter
 	ObserverType     ObserverType      `yaml:"observer_type"`
@@ -57,6 +58,7 @@ func (m *MetricMapping) UnmarshalYAML(unmarshal func(interface{}) error) error {
 	m.Match = tmp.Match
 	m.Name = tmp.Name
 	m.Labels = tmp.Labels
+	m.HonorLabels = tmp.HonorLabels
 	m.ObserverType = tmp.ObserverType
 	m.LegacyBuckets = tmp.LegacyBuckets
 	m.LegacyQuantiles = tmp.LegacyQuantiles


Test patch:
diff --git a/pkg/exporter/exporter_test.go b/pkg/exporter/exporter_test.go
index a2bb948..2ac0874 100644
--- a/pkg/exporter/exporter_test.go
+++ b/pkg/exporter/exporter_test.go
@@ -277,7 +277,7 @@ mappings:
 // TestLabelParsing verifies that labels getting parsed out of metric
 // names are being properly created.
 func TestLabelParsing(t *testing.T) {
-	codes := [2]string{"200", "300"}
+	codes := [3]string{"200", "300", "400"}
 
 	events := make(chan event.Events)
 	go func() {
@@ -292,6 +292,11 @@ func TestLabelParsing(t *testing.T) {
 				CValue:      1,
 				CLabels:     make(map[string]string),
 			},
+			&event.CounterEvent{
+				CMetricName: "counter.test.400",
+				CValue:      1,
+				CLabels:     map[string]string{"code": "should be overwritten"},
+			},
 		}
 		events <- c
 		close(events)
@@ -329,6 +334,51 @@ mappings:
 	}
 }
 
+func TestHonorLabels(t *testing.T) {
+	metricName := "some_counter"
+	events := make(chan event.Events)
+	go func() {
+		c := event.Events{
+			&event.CounterEvent{
+				CMetricName: metricName,
+				CValue:      1,
+				CLabels:     map[string]string{"some_label": "bar"},
+			},
+		}
+		events <- c
+		close(events)
+	}()
+
+	config := `
+mappings:
+  - match: .*
+    match_type: regex
+    name: $0
+    labels:
+      some_label: foo
+    honor_labels: true
+`
+	testMapper := &mapper.MetricMapper{
+		Logger: log.NewNopLogger(),
+	}
+	err := testMapper.InitFromYAMLString(config)
+	if err != nil {
+		t.Fatalf("Config load error: %s %s", config, err)
+	}
+
+	ex := NewExporter(prometheus.DefaultRegisterer, testMapper, log.NewNopLogger(), eventsActions, eventsUnmapped, errorEventStats, eventStats, conflictingEventStats, metricsCount)
+	ex.Listen(events)
+
+	metrics, err := prometheus.DefaultGatherer.Gather()
+	if err != nil {
+		t.Fatalf("Cannot gather from DefaultGatherer: %v", err)
+	}
+
+	if getFloat64(metrics, metricName, map[string]string{"some_label": "bar"}) == nil {
+		t.Fatalf("Could not find metrics for %s with label set", metricName)
+	}
+}
+
 // TestConflictingMetrics validates that the exporter will not register metrics
 // of different types that have overlapping names.
 func TestConflictingMetrics(t *testing.T) {