Skip to content

Commit

Permalink
perf: reduce allocations when serving Prometheus metrics (#3172)
Browse files Browse the repository at this point in the history
  • Loading branch information
cgrinds authored Sep 25, 2024
1 parent cfa10ec commit 9d896f4
Show file tree
Hide file tree
Showing 2 changed files with 39 additions and 41 deletions.
62 changes: 34 additions & 28 deletions cmd/exporters/prometheus/httpd.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,21 +129,25 @@ func (p *Prometheus) ServeMetrics(w http.ResponseWriter, r *http.Request) {
}

p.cache.Lock()
// Count the number of metrics so we can pre-allocate the slice to avoid reallocations
for _, metrics := range p.cache.Get() {
data = append(data, metrics...)
count += len(metrics)
}

data = make([][]byte, 0, count)
tagsSeen := make(map[string]bool)

for _, metrics := range p.cache.Get() {
data = addMetricsToSlice(data, metrics, tagsSeen, p.addMetaTags)
}
p.cache.Unlock()

// serve our own metadata
// notice that some values are always taken from previous session
md, _ := p.render(p.Metadata)
data = append(data, md...)
count += len(md)
data = addMetricsToSlice(data, md, tagsSeen, p.addMetaTags)

if p.addMetaTags {
data = filterMetaTags(data)
}
count += len(md)

w.WriteHeader(http.StatusOK)
w.Header().Set("Content-Type", "text/plain")
Expand All @@ -169,35 +173,37 @@ func (p *Prometheus) ServeMetrics(w http.ResponseWriter, r *http.Request) {
}
}

// filterMetaTags removes duplicate TYPE/HELP tags in the metrics
// Note: this is a workaround, normally Render() will only add
// one TYPE/HELP for each metric type, however since some metric
// types (e.g. metadata_collector_metrics) are submitted from multiple
// collectors, we end up with duplicates in the final batch delivered
// over HTTP.
func filterMetaTags(metrics [][]byte) [][]byte {

filtered := make([][]byte, 0)

metricsWithTags := make(map[string]bool)

for i, m := range metrics {
if bytes.HasPrefix(m, []byte("# ")) {
if fields := strings.Fields(string(m)); len(fields) > 3 {
name := fields[2]
if !metricsWithTags[name] {
metricsWithTags[name] = true
filtered = append(filtered, m)
// addMetricsToSlice adds metrics to a slice, skipping duplicates. Normally
// Render() only adds one TYPE/HELP for each metric type. Some metric types
// (e.g., metadata_collector_metrics) are submitted from multiple collectors.
// That causes duplicates that are removed in this function. The seen map is
// used to keep track of which metrics have been added. The metrics slice is
// expected to be in the format: # HELP metric_name help text # TYPE metric_name
// type metric_name{tag="value"} value
func addMetricsToSlice(data [][]byte, metrics [][]byte, seen map[string]bool, addMetaTags bool) [][]byte {

if !addMetaTags {
return append(data, metrics...)
}

for i, metric := range metrics {
if bytes.HasPrefix(metric, []byte("# ")) {
if fields := bytes.Fields(metric); len(fields) > 3 {
name := string(fields[2])
if !seen[name] {
seen[name] = true
data = append(data, metric)
if i+1 < len(metrics) {
filtered = append(filtered, metrics[i+1])
data = append(data, metrics[i+1])
}
}
}
} else {
filtered = append(filtered, m)
data = append(data, metric)
}
}
return filtered

return data
}

// ServeInfo provides a human-friendly overview of metric types and source collectors
Expand Down
18 changes: 5 additions & 13 deletions cmd/exporters/prometheus/prometheus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
package prometheus

import (
"bytes"
"github.com/google/go-cmp/cmp"
"github.com/netapp/harvest/v2/cmd/poller/exporter"
"github.com/netapp/harvest/v2/cmd/poller/options"
Expand Down Expand Up @@ -40,20 +39,13 @@ func TestFilterMetaTags(t *testing.T) {
[]byte(`some_other_metric{node="node_3"} 0.0`),
}

output := filterMetaTags(example)
seen := make(map[string]bool)
data := addMetricsToSlice(nil, example, seen, true)

if len(output) != len(expected) {
t.Fatalf("filtered data should have %d, but got %d lines", len(expected), len(output))
diff := cmp.Diff(data, expected)
if diff != "" {
t.Errorf("Mismatch (-got +want):\n%s", diff)
}

// output should have exact same lines in the same order
for i := range output {
if !bytes.Equal(output[i], expected[i]) {
t.Fatalf("line:%d - output = [%s], expected = [%s]", i, string(output[i]), string(expected[i]))
}
}

t.Log("OK - output is exactly what is expected")
}

func TestEscape(t *testing.T) {
Expand Down

0 comments on commit 9d896f4

Please sign in to comment.