diff --git a/exporter.go b/exporter.go index 7a6adb4..15c01b7 100644 --- a/exporter.go +++ b/exporter.go @@ -280,7 +280,7 @@ type Exporter struct { func escapeMetricName(metricName string) string { // If a metric starts with a digit, prepend an underscore. - if metricName[0] >= '0' && metricName[0] <= '9' { + if len(metricName) > 0 && metricName[0] >= '0' && metricName[0] <= '9' { metricName = "_" + metricName } @@ -333,6 +333,11 @@ func (b *Exporter) handleEvent(event Event) { metricName := "" prometheusLabels := event.Labels() if present { + if mapping.Name == "" { + log.Debugf("The mapping of '%s' for match '%s' generates an empty metric name", event.MetricName(), mapping.Match) + errorEventStats.WithLabelValues("empty_metric_name").Inc() + return + } metricName = escapeMetricName(mapping.Name) for label, value := range labels { prometheusLabels[label] = value @@ -348,7 +353,7 @@ func (b *Exporter) handleEvent(event Event) { // will cause the exporter to panic. Instead we will warn and continue to the next event. if event.Value() < 0.0 { log.Debugf("Counter %q is: '%f' (counter must be non-negative value)", metricName, event.Value()) - eventStats.WithLabelValues("illegal_negative_counter").Inc() + errorEventStats.WithLabelValues("illegal_negative_counter").Inc() return } diff --git a/exporter_test.go b/exporter_test.go index f353c96..fc0d0b1 100644 --- a/exporter_test.go +++ b/exporter_test.go @@ -52,8 +52,56 @@ func TestNegativeCounter(t *testing.T) { close(events) }() + errorCounter := errorEventStats.WithLabelValues("illegal_negative_counter") + prev := getTelemetryCounterValue(errorCounter) + ex := NewExporter(&mapper.MetricMapper{}) ex.Listen(events) + + updated := getTelemetryCounterValue(errorCounter) + if updated-prev != 1 { + t.Fatal("Illegal negative counter error not counted") + } +} + +// TestEmptyStringMetric validates when a metric name ends up +// being the empty string after applying the match replacements +// tha we don't panic the Exporter Listener. +func TestEmptyStringMetric(t *testing.T) { + events := make(chan Events) + go func() { + c := Events{ + &CounterEvent{ + metricName: "foo_bar", + value: 1, + }, + } + events <- c + close(events) + }() + + config := ` +mappings: +- match: .*_bar + match_type: regex + name: "${1}" +` + testMapper := &mapper.MetricMapper{} + err := testMapper.InitFromYAMLString(config) + if err != nil { + t.Fatalf("Config load error: %s %s", config, err) + } + + errorCounter := errorEventStats.WithLabelValues("empty_metric_name") + prev := getTelemetryCounterValue(errorCounter) + + ex := NewExporter(testMapper) + ex.Listen(events) + + updated := getTelemetryCounterValue(errorCounter) + if updated-prev != 1 { + t.Fatal("Empty metric name error event not counted") + } } // TestInvalidUtf8InDatadogTagValue validates robustness of exporter listener @@ -167,6 +215,7 @@ func TestEscapeMetricName(t *testing.T) { "with😱emoji": "with_emoji", "with.*.multiple": "with___multiple", "test.web-server.foo.bar": "test_web_server_foo_bar", + "": "", } for in, want := range scenarios { @@ -355,3 +404,12 @@ func labelPairsAsLabels(pairs []*dto.LabelPair) (labels prometheus.Labels) { } return } + +func getTelemetryCounterValue(counter prometheus.Counter) float64 { + var metric dto.Metric + err := counter.Write(&metric) + if err != nil { + return 0.0 + } + return metric.Counter.GetValue() +} diff --git a/telemetry.go b/telemetry.go index 4e881aa..541140c 100644 --- a/telemetry.go +++ b/telemetry.go @@ -102,6 +102,13 @@ var ( }, []string{"type"}, ) + errorEventStats = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "statsd_exporter_events_error_total", + Help: "The total number of StatsD events discarded due to errors.", + }, + []string{"reason"}, + ) ) func init() { @@ -119,4 +126,5 @@ func init() { prometheus.MustRegister(configLoads) prometheus.MustRegister(mappingsCount) prometheus.MustRegister(conflictingEventStats) + prometheus.MustRegister(errorEventStats) }