Skip to content

Commit

Permalink
[Prometheus] Add unit tests for parsing metric families (elastic#36669)
Browse files Browse the repository at this point in the history
* Add tests for parsing metrics.

* Remove _info suffix.

* Add unit tests

* Add unit tests

* Add unit tests

* Add license header

* Fix goimports.

* Fix openmetrics tests.
  • Loading branch information
constanca-m authored and Scholar-Li committed Feb 5, 2024
1 parent 2659c5e commit 8d586af
Show file tree
Hide file tree
Showing 8 changed files with 832 additions and 210 deletions.
20 changes: 8 additions & 12 deletions metricbeat/helper/openmetrics/openmetrics_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,8 @@ metrics_one_count_total{name="john",surname="williams"} 2
metrics_one_count_total{name="jahn",surname="baldwin",age="30"} 3
`

openMetricsCounterKeyLabelWithNaNInf = `# TYPE metrics_one_count_errors counter
metrics_one_count_errors{name="jane",surname="foster"} 1
openMetricsCounterKeyLabelWithNaNInf = `# TYPE metrics_one_count_errors_total counter
metrics_one_count_errors_total{name="jane",surname="foster"} 1
# TYPE metrics_one_count_total counter
metrics_one_count_total{name="jane",surname="foster"} NaN
metrics_one_count_total{name="john",surname="williams"} +Inf
Expand Down Expand Up @@ -537,22 +537,20 @@ func TestOpenMetrics(t *testing.T) {
msg: "Info metric",
mapping: &MetricsMapping{
Metrics: map[string]MetricMap{
"target_info": Metric("target_info.metric"),
"target": Metric("target"),
},
},
expected: []mapstr.M{
mapstr.M{
"target_info": mapstr.M{
"metric": int64(1),
},
"target": int64(1),
},
},
},
{
msg: "Info metric with labels",
mapping: &MetricsMapping{
Metrics: map[string]MetricMap{
"target_with_labels_info": Metric("target_with_labels_info.metric"),
"target_with_labels": Metric("target_with_labels"),
},
Labels: map[string]LabelMap{
"env": Label("labels.env"),
Expand All @@ -561,9 +559,7 @@ func TestOpenMetrics(t *testing.T) {
},
expected: []mapstr.M{
mapstr.M{
"target_with_labels_info": mapstr.M{
"metric": int64(1),
},
"target_with_labels": int64(1),
"labels": mapstr.M{
"env": "prod",
"hostname": "myhost",
Expand Down Expand Up @@ -747,8 +743,8 @@ func TestOpenMetricsKeyLabels(t *testing.T) {
openmetricsResponse: openMetricsCounterKeyLabelWithNaNInf,
mapping: &MetricsMapping{
Metrics: map[string]MetricMap{
"metrics_one_count_errors": Metric("metrics.one.count"),
"metrics_one_count_total": Metric("metrics.one.count"),
"metrics_one_count_errors_total": Metric("metrics.one.count"),
"metrics_one_count_total": Metric("metrics.one.count"),
},
Labels: map[string]LabelMap{
"name": KeyLabel("metrics.one.labels.name"),
Expand Down
88 changes: 0 additions & 88 deletions metricbeat/helper/prometheus/prometheus_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ import (
"net/http"
"sort"
"testing"
"time"

"github.com/prometheus/prometheus/pkg/labels"

"github.com/stretchr/testify/assert"

Expand Down Expand Up @@ -1005,88 +1002,3 @@ func TestPrometheusKeyLabels(t *testing.T) {
}
}
}

func TestPrometheusHistogramWithoutSuffix(t *testing.T) {
promHistogramWithAndWithoutBucket := `
# HELP http_server_requests_seconds Duration of HTTP server request handling
# TYPE http_server_requests_seconds histogram
http_server_requests_seconds{exception="None",uri="/actuator/prometheus",quantile="0.002796201",} 0.046137344
http_server_requests_seconds{exception="None",uri="/actuator/prometheus",quantile="0.003145726",} 0.046137344
http_server_requests_seconds_bucket{exception="None",uri="/actuator/prometheus",le="0.001",} 0.0
http_server_requests_seconds_bucket{exception="None",uri="/actuator/prometheus",le="0.001048576",} 0.0
http_server_requests_seconds_count{exception="None",uri="/actuator/prometheus",} 1.0
http_server_requests_seconds_sum{exception="None",uri="/actuator/prometheus",} 0.046745444
http_server_requests_seconds_impossible{exception="None",uri="/actuator/prometheus",} 0.046745444
http_server_requests_seconds_created{exception="None",uri="/actuator/prometheus",} 0.046745444
`

labelsList := []*labels.Label{
{
Name: "exception",
Value: "None",
},
{
Name: "uri",
Value: "/actuator/prometheus",
},
}

cumulativeCounts := []uint64{
uint64(0.0),
uint64(0.0),
}
upperBounds := []float64{
0.001,
0.001048576,
}
var buckets []*Bucket
for i := range cumulativeCounts {
buckets = append(buckets, &Bucket{
CumulativeCount: &cumulativeCounts[i],
UpperBound: &upperBounds[i],
})
}

name := "http_server_requests_seconds"
help := "Duration of HTTP server request handling"

var expectedMetric OpenMetric
expectedMetric.Name = &name
expectedMetric.Label = labelsList
sampleSum := 0.046745444
sampleCount := uint64(1.0)
expectedMetric.Histogram = &Histogram{
IsGaugeHistogram: false,
SampleSum: &sampleSum,
SampleCount: &sampleCount,
Bucket: buckets,
}

expectedMetrics := []*OpenMetric{
&expectedMetric,
}

type Histogram struct {
SampleCount *uint64
SampleSum *float64
Bucket []*Bucket
IsGaugeHistogram bool
}

expected := []*MetricFamily{
{
Name: &name,
Help: &help,
Type: "histogram",
Unit: nil,
Metric: expectedMetrics,
},
}

b := []byte(promHistogramWithAndWithoutBucket)
result, err := ParseMetricFamilies(b, ContentTypeTextFormat, time.Now())
if err != nil {
t.Fatal("ParseMetricFamilies returned an error.")
}
assert.Equal(t, expected, result)
}
100 changes: 65 additions & 35 deletions metricbeat/helper/prometheus/textparse.go
Original file line number Diff line number Diff line change
Expand Up @@ -318,19 +318,25 @@ func (m *MetricFamily) GetMetric() []*OpenMetric {
}

const (
suffixTotal = "_total"
suffixGCount = "_gcount"
suffixGSum = "_gsum"
suffixCount = "_count"
suffixSum = "_sum"
suffixBucket = "_bucket"
suffixTotal = "_total"
suffixGCount = "_gcount"
suffixGSum = "_gsum"
suffixCount = "_count"
suffixSum = "_sum"
suffixBucket = "_bucket"
suffixCreated = "_created"
suffixInfo = "_info"
)

// Counters have _total suffix
func isTotal(name string) bool {
return strings.HasSuffix(name, suffixTotal)
}

func isCreated(name string) bool {
return strings.HasSuffix(name, suffixCreated)
}

func isGCount(name string) bool {
return strings.HasSuffix(name, suffixGCount)
}
Expand All @@ -351,7 +357,11 @@ func isBucket(name string) bool {
return strings.HasSuffix(name, suffixBucket)
}

func summaryMetricName(name string, s float64, qv string, lbls string, t *int64, summariesByName map[string]map[string]*OpenMetric) (string, *OpenMetric) {
func isInfo(name string) bool {
return strings.HasSuffix(name, suffixInfo)
}

func summaryMetricName(name string, s float64, qv string, lbls string, summariesByName map[string]map[string]*OpenMetric) (string, *OpenMetric) {
var summary = &Summary{}
var quantile = []*Quantile{}
var quant = &Quantile{}
Expand All @@ -360,10 +370,10 @@ func summaryMetricName(name string, s float64, qv string, lbls string, t *int64,
case isCount(name):
u := uint64(s)
summary.SampleCount = &u
name = name[:len(name)-6]
name = strings.TrimSuffix(name, suffixCount)
case isSum(name):
summary.SampleSum = &s
name = name[:len(name)-4]
name = strings.TrimSuffix(name, suffixSum)
default:
f, err := strconv.ParseFloat(qv, 64)
if err != nil {
Expand All @@ -373,8 +383,8 @@ func summaryMetricName(name string, s float64, qv string, lbls string, t *int64,
quant.Value = &s
}

_, k := summariesByName[name]
if !k {
_, ok := summariesByName[name]
if !ok {
summariesByName[name] = make(map[string]*OpenMetric)
}
metric, ok := summariesByName[name][lbls]
Expand All @@ -392,7 +402,6 @@ func summaryMetricName(name string, s float64, qv string, lbls string, t *int64,
} else if quant.Quantile != nil {
metric.Summary.Quantile = append(metric.Summary.Quantile, quant)
}

return name, metric
}

Expand Down Expand Up @@ -499,8 +508,6 @@ func ParseMetricFamilies(b []byte, contentType string, ts time.Time) ([]*MetricF
fam = &MetricFamily{Name: &s, Type: t}
metricFamiliesByName[s] = fam
} else {
// In case the metric family already exists, we need to make sure the type is correctly defined
// instead of being `unknown` like it was initialized for the other entry types (help and unit).
fam.Type = t
}
mt = t
Expand All @@ -511,21 +518,23 @@ func ParseMetricFamilies(b []byte, contentType string, ts time.Time) ([]*MetricF
h := string(t)
_, ok = metricFamiliesByName[s]
if !ok {
fam = &MetricFamily{Name: &s, Help: &h, Type: textparse.MetricTypeUnknown}
fam = &MetricFamily{Name: &s, Help: &h}
metricFamiliesByName[s] = fam
} else {
fam.Help = &h
}
fam.Help = &h
continue
case textparse.EntryUnit:
buf, t := parser.Unit()
s := string(buf)
u := string(t)
_, ok = metricFamiliesByName[s]
if !ok {
fam = &MetricFamily{Name: &s, Unit: &u, Type: textparse.MetricTypeUnknown}
fam = &MetricFamily{Name: &s, Unit: &u}
metricFamiliesByName[string(buf)] = fam
} else {
fam.Unit = &u
}
fam.Unit = &u
continue
case textparse.EntryComment:
continue
Expand Down Expand Up @@ -577,49 +586,67 @@ func ParseMetricFamilies(b []byte, contentType string, ts time.Time) ([]*MetricF
var metric *OpenMetric

metricName := lset.Get(labels.MetricName)
var lookupMetricName string

// lookupMetricName will have the suffixes removed
lookupMetricName := metricName
var exm *exemplar.Exemplar

// Suffixes - https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#suffixes
switch mt {
case textparse.MetricTypeCounter:
if contentType == OpenMetricsType && !isTotal(metricName) && !isCreated(metricName) {
// Possible suffixes for counter in Open metrics are _created and _total.
// Otherwise, ignore.
// https://github.com/OpenObservability/OpenMetrics/blob/main/specification/OpenMetrics.md#counter-1
continue
}

var counter = &Counter{Value: &v}
mn := lset.Get(labels.MetricName)
metric = &OpenMetric{Name: &mn, Counter: counter, Label: labelPairs}
if isTotal(metricName) && contentType == OpenMetricsType { // Remove suffix _total, get lookup metricname
lookupMetricName = strings.TrimSuffix(metricName, suffixTotal)
if contentType == OpenMetricsType {
// Remove the two possible suffixes, _created and _total
if isTotal(metricName) {
lookupMetricName = strings.TrimSuffix(metricName, suffixTotal)
} else {
lookupMetricName = strings.TrimSuffix(metricName, suffixCreated)
}
} else {
lookupMetricName = metricName
}
case textparse.MetricTypeGauge:
var gauge = &Gauge{Value: &v}
metric = &OpenMetric{Name: &metricName, Gauge: gauge, Label: labelPairs}
lookupMetricName = metricName
//lookupMetricName = metricName
case textparse.MetricTypeInfo:
// Info only exists for Openmetrics. It must have the suffix _info
if !isInfo(metricName) {
continue
}
lookupMetricName = strings.TrimSuffix(metricName, suffixInfo)
value := int64(v)
var info = &Info{Value: &value}
metric = &OpenMetric{Name: &metricName, Info: info, Label: labelPairs}
lookupMetricName = metricName
case textparse.MetricTypeSummary:
lookupMetricName, metric = summaryMetricName(metricName, v, qv, lbls.String(), &t, summariesByName)
lookupMetricName, metric = summaryMetricName(metricName, v, qv, lbls.String(), summariesByName)
metric.Label = labelPairs
if !isSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
metricName = lookupMetricName
case textparse.MetricTypeHistogram:
if hasExemplar := parser.Exemplar(&e); hasExemplar {
exm = &e
}
lookupMetricName, metric = histogramMetricName(metricName, v, qv, lbls.String(), &t, false, exm, histogramsByName)
if metric == nil { // metric name does not have a suffix supported for the type histogram
if metric == nil {
continue
}
metric.Label = labelPairs
if !isSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
metricName = lookupMetricName
case textparse.MetricTypeGaugeHistogram:
if hasExemplar := parser.Exemplar(&e); hasExemplar {
exm = &e
Expand All @@ -631,29 +658,32 @@ func ParseMetricFamilies(b []byte, contentType string, ts time.Time) ([]*MetricF
metric.Label = labelPairs
metric.Histogram.IsGaugeHistogram = true
if !isGSum(metricName) {
// Avoid registering the metric multiple times.
continue
}
metricName = lookupMetricName
case textparse.MetricTypeStateset:
value := int64(v)
var stateset = &Stateset{Value: &value}
metric = &OpenMetric{Name: &metricName, Stateset: stateset, Label: labelPairs}
lookupMetricName = metricName
case textparse.MetricTypeUnknown:
var unknown = &Unknown{Value: &v}
metric = &OpenMetric{Name: &metricName, Unknown: unknown, Label: labelPairs}
lookupMetricName = metricName
default:
lookupMetricName = metricName
}

fam, ok = metricFamiliesByName[lookupMetricName]
if !ok {
fam = &MetricFamily{Type: mt}
metricFamiliesByName[lookupMetricName] = fam
}
// If the lookupMetricName is not in metricFamiliesByName, we check for metric name, in case
// the removed suffix is part of the name.
fam, ok = metricFamiliesByName[metricName]
if !ok {
// There is not any metadata for this metric. In this case, the name of the metric
// will remain metricName instead of the possible modified lookupMetricName
fam = &MetricFamily{Name: &metricName, Type: mt}
metricFamiliesByName[metricName] = fam

fam.Name = &metricName
}
}

if hasExemplar := parser.Exemplar(&e); hasExemplar && mt != textparse.MetricTypeHistogram && metric != nil {
if !e.HasTs {
Expand Down
Loading

0 comments on commit 8d586af

Please sign in to comment.