prometheus/model/textparse/openmetricsparse_test.go
cui 54d4f527a0
textparse: fix NaN canonicalization check in OpenMetrics getFloatValue (#18399)
* textparse: fix NaN canonicalization check in OpenMetrics getFloatValue
* textparse: add tests for OpenMetrics summary NaN quantiles

getFloatValue was testing p.exemplarVal instead of the parsed float when normalizing NaN to the canonical representation, so metric values that were NaN were not normalized correctly.

Extend TestOpenMetricsParse with nansum summary lines and cmpopts.EquateNaNs in requireEntries so NaN float values compare equal after canonicalization.

Signed-off-by: Weixie Cui <cuiweixie@gmail.com>
Signed-off-by: cui <cuiweixie@gmail.com>
Co-authored-by: George Krajcsovits <krajorama@users.noreply.github.com>
2026-05-12 15:45:59 +02:00

1278 lines
40 KiB
Go

// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package textparse
import (
"fmt"
"io"
"math"
"testing"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
"github.com/prometheus/prometheus/model/exemplar"
"github.com/prometheus/prometheus/model/labels"
"github.com/prometheus/prometheus/model/value"
)
func int64p(x int64) *int64 { return &x }
func TestOpenMetricsParse(t *testing.T) {
input := `# HELP go_gc_duration_seconds A summary of the GC invocation durations.
# TYPE go_gc_duration_seconds summary
# UNIT go_gc_duration_seconds seconds
go_gc_duration_seconds{quantile="0"} 4.9351e-05
go_gc_duration_seconds{quantile="0.25"} 7.424100000000001e-05
go_gc_duration_seconds{quantile="0.5",a="b"} 8.3835e-05
# HELP nohelp1
# HELP help2 escape \ \n \\ \" \x chars
# UNIT nounit
go_gc_duration_seconds{quantile="1.0",a="b"} 8.3835e-05
go_gc_duration_seconds_count 99
some:aggregate:rate5m{a_b="c"} 1
# HELP go_goroutines Number of goroutines that currently exist.
# TYPE go_goroutines gauge
go_goroutines 33 123.123
# TYPE hh histogram
hh_bucket{le="+Inf"} 1
# TYPE gh gaugehistogram
gh_bucket{le="+Inf"} 1
# TYPE hhh histogram
hhh_bucket{le="+Inf"} 1 # {id="histogram-bucket-test"} 4
hhh_count 1 # {id="histogram-count-test"} 4
# TYPE ggh gaugehistogram
ggh_bucket{le="+Inf"} 1 # {id="gaugehistogram-bucket-test",xx="yy"} 4 123.123
ggh_count 1 # {id="gaugehistogram-count-test",xx="yy"} 4 123.123
# TYPE smr_seconds summary
smr_seconds_count 2.0 # {id="summary-count-test"} 1 123.321
smr_seconds_sum 42.0 # {id="summary-sum-test"} 1 123.321
# TYPE ii info
ii{foo="bar"} 1
# TYPE ss stateset
ss{ss="foo"} 1
ss{ss="bar"} 0
ss{A="a"} 0
# TYPE un unknown
_metric_starting_with_underscore 1
testmetric{_label_starting_with_underscore="foo"} 1
testmetric{label="\"bar\""} 1
# HELP foo Counter with and without labels to certify ST is parsed for both cases
# TYPE foo counter
foo_total 17.0 1520879607.789 # {id="counter-test"} 5
foo_created 1520872607.123
foo_total{a="b"} 17.0 1520879607.789 # {id="counter-test"} 5
foo_created{a="b"} 1520872607.123
foo_total{le="c"} 21.0
foo_created{le="c"} 1520872621.123
foo_total{le="1"} 10.0
# HELP bar Summary with ST at the end, making sure we find ST even if it's multiple lines a far
# TYPE bar summary
bar_count 17.0
bar_sum 324789.3
bar{quantile="0.95"} 123.7
bar{quantile="0.99"} 150.0
bar_created 1520872608.124
# HELP baz Histogram with the same objective as above's summary
# TYPE baz histogram
baz_bucket{le="0.0"} 0
baz_bucket{le="+Inf"} 17
baz_count 17
baz_sum 324789.3
baz_created 1520872609.125
# HELP fizz_created Gauge which shouldn't be parsed as ST
# TYPE fizz_created gauge
fizz_created 17.0
# HELP something Histogram with _created between buckets and summary
# TYPE something histogram
something_count 18
something_sum 324789.4
something_created 1520430001
something_bucket{le="0.0"} 1
something_bucket{le="1"} 2
something_bucket{le="+Inf"} 18
# HELP yum Summary with _created between sum and quantiles
# TYPE yum summary
yum_count 20
yum_sum 324789.5
yum_created 1520430003
yum{quantile="0.95"} 123.7
yum{quantile="0.99"} 150.0
# HELP foobar Summary with _created as the first line
# TYPE foobar summary
foobar_count 21
foobar_created 1520430004
foobar_sum 324789.6
foobar{quantile="0.95"} 123.8
foobar{quantile="0.99"} 150.1
# HELP nansum Summary with NaN value
# TYPE nansum summary
nansum_count 0
nansum_sum 0.0
nansum{quantile="0.95"} nan
nansum{quantile="0.99"} NaN`
input += "\n# HELP metric foo\x00bar"
input += "\nnull_byte_metric{a=\"abc\x00\"} 1"
input += "\n# EOF\n"
for _, typeAndUnitEnabled := range []bool{false, true} {
t.Run(fmt.Sprintf("type-and-unit=%v", typeAndUnitEnabled), func(t *testing.T) {
exp := []parsedEntry{
{
m: "go_gc_duration_seconds",
help: "A summary of the GC invocation durations.",
}, {
m: "go_gc_duration_seconds",
typ: model.MetricTypeSummary,
}, {
m: "go_gc_duration_seconds",
unit: "seconds",
}, {
m: `go_gc_duration_seconds{quantile="0"}`,
v: 4.9351e-05,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_gc_duration_seconds", "__type__", string(model.MetricTypeSummary), "__unit__", "seconds", "quantile", "0.0"),
labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.0"),
),
}, {
m: `go_gc_duration_seconds{quantile="0.25"}`,
v: 7.424100000000001e-05,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_gc_duration_seconds", "__type__", string(model.MetricTypeSummary), "__unit__", "seconds", "quantile", "0.25"),
labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.25"),
),
}, {
m: `go_gc_duration_seconds{quantile="0.5",a="b"}`,
v: 8.3835e-05,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_gc_duration_seconds", "__type__", string(model.MetricTypeSummary), "__unit__", "seconds", "quantile", "0.5", "a", "b"),
labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "0.5", "a", "b"),
),
}, {
m: "nohelp1",
help: "",
}, {
m: "help2",
help: "escape \\ \n \\ \" \\x chars",
}, {
m: "nounit",
unit: "",
}, {
m: `go_gc_duration_seconds{quantile="1.0",a="b"}`,
v: 8.3835e-05,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_gc_duration_seconds", "__type__", string(model.MetricTypeSummary), "quantile", "1.0", "a", "b"),
labels.FromStrings("__name__", "go_gc_duration_seconds", "quantile", "1.0", "a", "b"),
),
}, {
m: `go_gc_duration_seconds_count`,
v: 99,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_gc_duration_seconds_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "go_gc_duration_seconds_count"),
),
}, {
m: `some:aggregate:rate5m{a_b="c"}`,
v: 1,
lset: todoDetectFamilySwitch(typeAndUnitEnabled, labels.FromStrings("__name__", "some:aggregate:rate5m", "a_b", "c"), model.MetricTypeSummary),
}, {
m: "go_goroutines",
help: "Number of goroutines that currently exist.",
}, {
m: "go_goroutines",
typ: model.MetricTypeGauge,
}, {
m: `go_goroutines`,
v: 33,
t: int64p(123123),
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "go_goroutines", "__type__", string(model.MetricTypeGauge)),
labels.FromStrings("__name__", "go_goroutines"),
),
}, {
m: "hh",
typ: model.MetricTypeHistogram,
}, {
m: `hh_bucket{le="+Inf"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "hh_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "hh_bucket", "le", "+Inf"),
),
}, {
m: "gh",
typ: model.MetricTypeGaugeHistogram,
}, {
m: `gh_bucket{le="+Inf"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "gh_bucket", "__type__", string(model.MetricTypeGaugeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "gh_bucket", "le", "+Inf"),
),
}, {
m: "hhh",
typ: model.MetricTypeHistogram,
}, {
m: `hhh_bucket{le="+Inf"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "hhh_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "hhh_bucket", "le", "+Inf"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "histogram-bucket-test"), Value: 4},
},
}, {
m: `hhh_count`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "hhh_count", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "hhh_count"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "histogram-count-test"), Value: 4},
},
}, {
m: "ggh",
typ: model.MetricTypeGaugeHistogram,
}, {
m: `ggh_bucket{le="+Inf"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ggh_bucket", "__type__", string(model.MetricTypeGaugeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "ggh_bucket", "le", "+Inf"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "gaugehistogram-bucket-test", "xx", "yy"), Value: 4, HasTs: true, Ts: 123123},
},
}, {
m: `ggh_count`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ggh_count", "__type__", string(model.MetricTypeGaugeHistogram)),
labels.FromStrings("__name__", "ggh_count"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "gaugehistogram-count-test", "xx", "yy"), Value: 4, HasTs: true, Ts: 123123},
},
}, {
m: "smr_seconds",
typ: model.MetricTypeSummary,
}, {
m: `smr_seconds_count`,
v: 2,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "smr_seconds_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "smr_seconds_count"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "summary-count-test"), Value: 1, HasTs: true, Ts: 123321},
},
}, {
m: `smr_seconds_sum`,
v: 42,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "smr_seconds_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "smr_seconds_sum"),
),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "summary-sum-test"), Value: 1, HasTs: true, Ts: 123321},
},
}, {
m: "ii",
typ: model.MetricTypeInfo,
}, {
m: `ii{foo="bar"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ii", "__type__", string(model.MetricTypeInfo), "foo", "bar"),
labels.FromStrings("__name__", "ii", "foo", "bar"),
),
}, {
m: "ss",
typ: model.MetricTypeStateset,
}, {
m: `ss{ss="foo"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ss", "__type__", string(model.MetricTypeStateset), "ss", "foo"),
labels.FromStrings("__name__", "ss", "ss", "foo"),
),
}, {
m: `ss{ss="bar"}`,
v: 0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ss", "__type__", string(model.MetricTypeStateset), "ss", "bar"),
labels.FromStrings("__name__", "ss", "ss", "bar"),
),
}, {
m: `ss{A="a"}`,
v: 0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "ss", "__type__", string(model.MetricTypeStateset), "A", "a"),
labels.FromStrings("__name__", "ss", "A", "a"),
),
}, {
m: "un",
typ: model.MetricTypeUnknown,
}, {
m: "_metric_starting_with_underscore",
v: 1,
lset: labels.FromStrings("__name__", "_metric_starting_with_underscore"),
}, {
m: "testmetric{_label_starting_with_underscore=\"foo\"}",
v: 1,
lset: labels.FromStrings("__name__", "testmetric", "_label_starting_with_underscore", "foo"),
}, {
m: "testmetric{label=\"\\\"bar\\\"\"}",
v: 1,
lset: labels.FromStrings("__name__", "testmetric", "label", `"bar"`),
}, {
m: "foo",
help: "Counter with and without labels to certify ST is parsed for both cases",
}, {
m: "foo",
typ: model.MetricTypeCounter,
}, {
m: "foo_total",
v: 17,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foo_total", "__type__", string(model.MetricTypeCounter)),
labels.FromStrings("__name__", "foo_total"),
),
t: int64p(1520879607789),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "counter-test"), Value: 5},
},
st: 1520872607123,
}, {
m: `foo_total{a="b"}`,
v: 17.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foo_total", "__type__", string(model.MetricTypeCounter), "a", "b"),
labels.FromStrings("__name__", "foo_total", "a", "b"),
),
t: int64p(1520879607789),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id", "counter-test"), Value: 5},
},
st: 1520872607123,
}, {
m: `foo_total{le="c"}`,
v: 21.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foo_total", "__type__", string(model.MetricTypeCounter), "le", "c"),
labels.FromStrings("__name__", "foo_total", "le", "c"),
),
st: 1520872621123,
}, {
m: `foo_total{le="1"}`,
v: 10.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foo_total", "__type__", string(model.MetricTypeCounter), "le", "1"),
labels.FromStrings("__name__", "foo_total", "le", "1"),
),
}, {
m: "bar",
help: "Summary with ST at the end, making sure we find ST even if it's multiple lines a far",
}, {
m: "bar",
typ: model.MetricTypeSummary,
}, {
m: "bar_count",
v: 17.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "bar_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "bar_count"),
),
st: 1520872608124,
}, {
m: "bar_sum",
v: 324789.3,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "bar_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "bar_sum"),
),
st: 1520872608124,
}, {
m: `bar{quantile="0.95"}`,
v: 123.7,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "bar", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "bar", "quantile", "0.95"),
),
st: 1520872608124,
}, {
m: `bar{quantile="0.99"}`,
v: 150.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "bar", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "bar", "quantile", "0.99"),
),
st: 1520872608124,
}, {
m: "baz",
help: "Histogram with the same objective as above's summary",
}, {
m: "baz",
typ: model.MetricTypeHistogram,
}, {
m: `baz_bucket{le="0.0"}`,
v: 0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "baz_bucket", "__type__", string(model.MetricTypeHistogram), "le", "0.0"),
labels.FromStrings("__name__", "baz_bucket", "le", "0.0"),
),
st: 1520872609125,
}, {
m: `baz_bucket{le="+Inf"}`,
v: 17,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "baz_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "baz_bucket", "le", "+Inf"),
),
st: 1520872609125,
}, {
m: `baz_count`,
v: 17,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "baz_count", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "baz_count"),
),
st: 1520872609125,
}, {
m: `baz_sum`,
v: 324789.3,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "baz_sum", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "baz_sum"),
),
st: 1520872609125,
}, {
m: "fizz_created",
help: "Gauge which shouldn't be parsed as ST",
}, {
m: "fizz_created",
typ: model.MetricTypeGauge,
}, {
m: `fizz_created`,
v: 17,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "fizz_created", "__type__", string(model.MetricTypeGauge)),
labels.FromStrings("__name__", "fizz_created"),
),
}, {
m: "something",
help: "Histogram with _created between buckets and summary",
}, {
m: "something",
typ: model.MetricTypeHistogram,
}, {
m: `something_count`,
v: 18,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "something_count", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "something_count"),
),
st: 1520430001000,
}, {
m: `something_sum`,
v: 324789.4,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "something_sum", "__type__", string(model.MetricTypeHistogram)),
labels.FromStrings("__name__", "something_sum"),
),
st: 1520430001000,
}, {
m: `something_bucket{le="0.0"}`,
v: 1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "0.0"),
labels.FromStrings("__name__", "something_bucket", "le", "0.0"),
),
st: 1520430001000,
}, {
m: `something_bucket{le="1"}`,
v: 2,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "1.0"),
labels.FromStrings("__name__", "something_bucket", "le", "1.0"),
),
st: 1520430001000,
}, {
m: `something_bucket{le="+Inf"}`,
v: 18,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "something_bucket", "__type__", string(model.MetricTypeHistogram), "le", "+Inf"),
labels.FromStrings("__name__", "something_bucket", "le", "+Inf"),
),
st: 1520430001000,
}, {
m: "yum",
help: "Summary with _created between sum and quantiles",
}, {
m: "yum",
typ: model.MetricTypeSummary,
}, {
m: `yum_count`,
v: 20,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "yum_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "yum_count"),
),
st: 1520430003000,
}, {
m: `yum_sum`,
v: 324789.5,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "yum_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "yum_sum"),
),
st: 1520430003000,
}, {
m: `yum{quantile="0.95"}`,
v: 123.7,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "yum", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "yum", "quantile", "0.95"),
),
st: 1520430003000,
}, {
m: `yum{quantile="0.99"}`,
v: 150.0,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "yum", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "yum", "quantile", "0.99"),
),
st: 1520430003000,
}, {
m: "foobar",
help: "Summary with _created as the first line",
}, {
m: "foobar",
typ: model.MetricTypeSummary,
}, {
m: `foobar_count`,
v: 21,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foobar_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "foobar_count"),
),
st: 1520430004000,
}, {
m: `foobar_sum`,
v: 324789.6,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foobar_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "foobar_sum"),
),
st: 1520430004000,
}, {
m: `foobar{quantile="0.95"}`,
v: 123.8,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foobar", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "foobar", "quantile", "0.95"),
),
st: 1520430004000,
}, {
m: `foobar{quantile="0.99"}`,
v: 150.1,
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "foobar", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "foobar", "quantile", "0.99"),
),
st: 1520430004000,
}, {
m: "nansum",
help: "Summary with NaN value",
}, {
m: "nansum",
typ: model.MetricTypeSummary,
}, {
m: "nansum_count",
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "nansum_count", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "nansum_count"),
),
}, {
m: "nansum_sum",
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "nansum_sum", "__type__", string(model.MetricTypeSummary)),
labels.FromStrings("__name__", "nansum_sum"),
),
}, {
m: `nansum{quantile="0.95"}`,
v: math.Float64frombits(value.NormalNaN),
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "nansum", "__type__", string(model.MetricTypeSummary), "quantile", "0.95"),
labels.FromStrings("__name__", "nansum", "quantile", "0.95"),
),
}, {
m: `nansum{quantile="0.99"}`,
v: math.Float64frombits(value.NormalNaN),
lset: typeAndUnitLabels(
typeAndUnitEnabled,
labels.FromStrings("__name__", "nansum", "__type__", string(model.MetricTypeSummary), "quantile", "0.99"),
labels.FromStrings("__name__", "nansum", "quantile", "0.99"),
),
}, {
m: "metric",
help: "foo\x00bar",
}, {
m: "null_byte_metric{a=\"abc\x00\"}",
v: 1,
lset: todoDetectFamilySwitch(typeAndUnitEnabled, labels.FromStrings("__name__", "null_byte_metric", "a", "abc\x00"), model.MetricTypeSummary),
},
}
opts := []OpenMetricsOption{WithOMParserSTSeriesSkipped()}
if typeAndUnitEnabled {
opts = append(opts, WithOMParserTypeAndUnitLabels())
}
p := NewOpenMetricsParser([]byte(input), labels.NewSymbolTable(), opts...)
got := testParse(t, p)
requireEntries(t, exp, got)
})
}
}
func TestOpenMetricsParse_UTF8(t *testing.T) {
input := `# HELP "go.gc_duration_seconds" A summary of the GC invocation durations.
# TYPE "go.gc_duration_seconds" summary
# UNIT "go.gc_duration_seconds" seconds
{"go.gc_duration_seconds",quantile="0"} 4.9351e-05
{"go.gc_duration_seconds",quantile="0.25"} 7.424100000000001e-05
{"go.gc_duration_seconds_created"} 1520872607.123
{"go.gc_duration_seconds",quantile="0.5",a="b"} 8.3835e-05
{"http.status",q="0.9",a="b"} 8.3835e-05
{"http.status",q="0.9",a="b"} 8.3835e-05
{q="0.9","http.status",a="b"} 8.3835e-05
{"go.gc_duration_seconds_sum"} 0.004304266
{"Heizölrückstoßabdämpfung 10€ metric with \"interesting\" {character\nchoices}","strange©™\n'quoted' \"name\""="6"} 10.0
quotedexemplar_count 1 # {"id.thing"="histogram-count-test"} 4
quotedexemplar2_count 1 # {"id.thing"="histogram-count-test",other="hello"} 4
`
input += "# EOF\n"
exp := []parsedEntry{
{
m: "go.gc_duration_seconds",
help: "A summary of the GC invocation durations.",
}, {
m: "go.gc_duration_seconds",
typ: model.MetricTypeSummary,
}, {
m: "go.gc_duration_seconds",
unit: "seconds",
}, {
m: `{"go.gc_duration_seconds",quantile="0"}`,
v: 4.9351e-05,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds", "quantile", "0.0"),
st: 1520872607123,
}, {
m: `{"go.gc_duration_seconds",quantile="0.25"}`,
v: 7.424100000000001e-05,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds", "quantile", "0.25"),
st: 1520872607123,
}, {
m: `{"go.gc_duration_seconds",quantile="0.5",a="b"}`,
v: 8.3835e-05,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds", "quantile", "0.5", "a", "b"),
}, {
m: `{"http.status",q="0.9",a="b"}`,
v: 8.3835e-05,
lset: labels.FromStrings("__name__", "http.status", "q", "0.9", "a", "b"),
}, {
m: `{"http.status",q="0.9",a="b"}`,
v: 8.3835e-05,
lset: labels.FromStrings("__name__", "http.status", "q", "0.9", "a", "b"),
}, {
m: `{q="0.9","http.status",a="b"}`,
v: 8.3835e-05,
lset: labels.FromStrings("__name__", "http.status", "q", "0.9", "a", "b"),
}, {
m: `{"go.gc_duration_seconds_sum"}`,
v: 0.004304266,
lset: labels.FromStrings("__name__", "go.gc_duration_seconds_sum"),
}, {
m: `{"Heizölrückstoßabdämpfung 10€ metric with \"interesting\" {character\nchoices}","strange©™\n'quoted' \"name\""="6"}`,
v: 10.0,
lset: labels.FromStrings("__name__", `Heizölrückstoßabdämpfung 10€ metric with "interesting" {character
choices}`, "strange©™\n'quoted' \"name\"", "6"),
}, {
m: `quotedexemplar_count`,
v: 1,
lset: labels.FromStrings("__name__", "quotedexemplar_count"),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id.thing", "histogram-count-test"), Value: 4},
},
}, {
m: `quotedexemplar2_count`,
v: 1,
lset: labels.FromStrings("__name__", "quotedexemplar2_count"),
es: []exemplar.Exemplar{
{Labels: labels.FromStrings("id.thing", "histogram-count-test", "other", "hello"), Value: 4},
},
},
}
p := NewOpenMetricsParser([]byte(input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
got := testParse(t, p)
requireEntries(t, exp, got)
}
func TestOpenMetricsParseErrors(t *testing.T) {
cases := []struct {
input string
err string
}{
// Happy cases. EOF is returned by the parser at the end of valid
// data.
{
input: "# EOF",
err: "EOF",
},
{
input: "# EOF\n",
err: "EOF",
},
// Unhappy cases.
{
input: "",
err: "data does not end with # EOF",
},
{
input: "\n",
err: "expected a valid start token, got \"\\n\" (\"INVALID\") while parsing: \"\\n\"",
},
{
input: "metric",
err: "expected value after metric, got \"metric\" (\"EOF\") while parsing: \"metric\"",
},
{
input: "metric 1",
err: "data does not end with # EOF",
},
{
input: "metric 1\n",
err: "data does not end with # EOF",
},
{
input: "metric_total 1 # {aa=\"bb\"} 4",
err: "data does not end with # EOF",
},
{
input: "a\n#EOF\n",
err: "expected value after metric, got \"\\n\" (\"INVALID\") while parsing: \"a\\n\"",
},
{
input: "\n\n#EOF\n",
err: "expected a valid start token, got \"\\n\" (\"INVALID\") while parsing: \"\\n\"",
},
{
input: " a 1\n#EOF\n",
err: "expected a valid start token, got \" \" (\"INVALID\") while parsing: \" \"",
},
{
input: "9\n#EOF\n",
err: "expected a valid start token, got \"9\" (\"INVALID\") while parsing: \"9\"",
},
{
input: "# TYPE u untyped\n#EOF\n",
err: "invalid metric type \"untyped\"",
},
{
input: "# TYPE c counter \n#EOF\n",
err: "invalid metric type \"counter \"",
},
{
input: "# TYPE c counter\n#EOF\n",
err: "expected a valid start token, got \"# \" (\"INVALID\") while parsing: \"# \"",
},
{
input: "# TYPE \n#EOF\n",
err: "expected metric name after TYPE, got \"\\n\" (\"INVALID\") while parsing: \"# TYPE \\n\"",
},
{
input: "# TYPE m\n#EOF\n",
err: "expected text in TYPE",
},
{
input: "# UNIT metric suffix\n#EOF\n",
err: "unit \"suffix\" not a suffix of metric \"metric\"",
},
{
input: "# UNIT metricsuffix suffix\n#EOF\n",
err: "unit \"suffix\" not a suffix of metric \"metricsuffix\"",
},
{
input: "# UNIT m suffix\n#EOF\n",
err: "unit \"suffix\" not a suffix of metric \"m\"",
},
{
input: "# UNIT \n#EOF\n",
err: "expected metric name after UNIT, got \"\\n\" (\"INVALID\") while parsing: \"# UNIT \\n\"",
},
{
input: "# UNIT m\n#EOF\n",
err: "expected text in UNIT",
},
{
input: "# HELP \n#EOF\n",
err: "expected metric name after HELP, got \"\\n\" (\"INVALID\") while parsing: \"# HELP \\n\"",
},
{
input: "# HELP m\n#EOF\n",
err: "expected text in HELP",
},
{
input: "a\t1\n#EOF\n",
err: "expected value after metric, got \"\\t\" (\"INVALID\") while parsing: \"a\\t\"",
},
{
input: "a 1\t2\n#EOF\n",
err: "strconv.ParseFloat: parsing \"1\\t2\": invalid syntax while parsing: \"a 1\\t2\"",
},
{
input: "a 1 2 \n#EOF\n",
err: "expected next entry after timestamp, got \" \\n\" (\"INVALID\") while parsing: \"a 1 2 \\n\"",
},
{
input: "a 1 2 #\n#EOF\n",
err: "expected next entry after timestamp, got \" #\\n\" (\"TIMESTAMP\") while parsing: \"a 1 2 #\\n\"",
},
{
input: "a 1 1z\n#EOF\n",
err: "strconv.ParseFloat: parsing \"1z\": invalid syntax while parsing: \"a 1 1z\"",
},
{
input: " # EOF\n",
err: "expected a valid start token, got \" \" (\"INVALID\") while parsing: \" \"",
},
{
input: "# EOF\na 1",
err: "unexpected data after # EOF",
},
{
input: "# EOF\n\n",
err: "unexpected data after # EOF",
},
{
input: "# EOFa 1",
err: "unexpected data after # EOF",
},
{
input: "#\tTYPE c counter\n",
err: "expected a valid start token, got \"#\\t\" (\"INVALID\") while parsing: \"#\\t\"",
},
{
input: "# TYPE c counter\n",
err: "invalid metric type \" counter\"",
},
{
input: "a 1 1 1\n# EOF\n",
err: "expected next entry after timestamp, got \" 1\\n\" (\"TIMESTAMP\") while parsing: \"a 1 1 1\\n\"",
},
{
input: "a{b='c'} 1\n# EOF\n",
err: "expected label value, got \"'\" (\"INVALID\") while parsing: \"a{b='\"",
},
{
input: "a{,b=\"c\"} 1\n# EOF\n",
err: "expected label name, got \",b\" (\"COMMA\") while parsing: \"a{,b\"",
},
{
input: "a{b=\"c\"d=\"e\"} 1\n# EOF\n",
err: "expected comma or brace close, got \"d=\" (\"LNAME\") while parsing: \"a{b=\\\"c\\\"d=\"",
},
{
input: "a{b=\"c\",,d=\"e\"} 1\n# EOF\n",
err: "expected label name, got \",d\" (\"COMMA\") while parsing: \"a{b=\\\"c\\\",,d\"",
},
{
input: "a{b=\n# EOF\n",
err: "expected label value, got \"\\n\" (\"INVALID\") while parsing: \"a{b=\\n\"",
},
{
input: "a{\xff=\"foo\"} 1\n# EOF\n",
err: "expected label name, got \"\\xff\" (\"INVALID\") while parsing: \"a{\\xff\"",
},
{
input: "a{b=\"\xff\"} 1\n# EOF\n",
err: "invalid UTF-8 label value: \"\\\"\\xff\\\"\"",
},
{
input: `{"a","b = "c"}
# EOF
`,
err: "expected equal, got \"c\\\"\" (\"LNAME\") while parsing: \"{\\\"a\\\",\\\"b = \\\"c\\\"\"",
},
{
input: `{"a",b\nc="d"} 1
# EOF
`,
err: "expected equal, got \"\\\\\" (\"INVALID\") while parsing: \"{\\\"a\\\",b\\\\\"",
},
{
input: "a true\n",
err: "strconv.ParseFloat: parsing \"true\": invalid syntax while parsing: \"a true\"",
},
{
input: "something_weird{problem=\"\n# EOF\n",
err: "expected label value, got \"\\\"\\n\" (\"INVALID\") while parsing: \"something_weird{problem=\\\"\\n\"",
},
{
input: "empty_label_name{=\"\"} 0\n# EOF\n",
err: "expected label name, got \"=\\\"\" (\"EQUAL\") while parsing: \"empty_label_name{=\\\"\"",
},
{
input: "{A}0\n# EOF\n",
err: "expected equal, got \"}0\" (\"BCLOSE\") while parsing: \"{A}0\"",
},
{
input: "foo 1_2\n\n# EOF\n",
err: "unsupported character in float while parsing: \"foo 1_2\"",
},
{
input: "foo 0x1p-3\n\n# EOF\n",
err: "unsupported character in float while parsing: \"foo 0x1p-3\"",
},
{
input: "foo 0x1P-3\n\n# EOF\n",
err: "unsupported character in float while parsing: \"foo 0x1P-3\"",
},
{
input: "foo 0 1_2\n\n# EOF\n",
err: "unsupported character in float while parsing: \"foo 0 1_2\"",
},
{
input: "custom_metric_total 1 # {aa=bb}\n# EOF\n",
err: "expected label value, got \"b\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {aa=b\"",
},
{
input: "custom_metric_total 1 # {aa=\"bb\"}\n# EOF\n",
err: "expected value after exemplar labels, got \"\\n\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\"}\\n\"",
},
{
input: `custom_metric_total 1 # {aa="bb"}`,
err: "expected value after exemplar labels, got \"}\" (\"EOF\") while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\"}\"",
},
{
input: `custom_metric_total 1 # {bb}`,
err: "expected equal, got \"}\" (\"BCLOSE\") while parsing: \"custom_metric_total 1 # {bb}\"",
},
{
input: `custom_metric_total 1 # {bb, a="dd"}`,
err: "expected equal, got \", \" (\"COMMA\") while parsing: \"custom_metric_total 1 # {bb, \"",
},
{
input: `custom_metric_total 1 # {aa="bb",,cc="dd"} 1`,
err: "expected label name, got \",c\" (\"COMMA\") while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\",,c\"",
},
{
input: `custom_metric_total 1 # {aa="bb"} 1_2`,
err: "unsupported character in float while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\"} 1_2\"",
},
{
input: `custom_metric_total 1 # {aa="bb"} 0x1p-3`,
err: "unsupported character in float while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\"} 0x1p-3\"",
},
{
input: `custom_metric_total 1 # {aa="bb"} true`,
err: "strconv.ParseFloat: parsing \"true\": invalid syntax while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\"} true\"",
},
{
input: `custom_metric_total 1 # {aa="bb",cc=}`,
err: "expected label value, got \"}\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {aa=\\\"bb\\\",cc=}\"",
},
{
input: `custom_metric_total 1 # {aa=\"\xff\"} 9.0`,
err: "expected label value, got \"\\\\\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {aa=\\\\\"",
},
{
input: `{b="c",} 1`,
err: "metric name not set while parsing: \"{b=\\\"c\\\",} 1\"",
},
{
input: `a 1 NaN`,
err: `invalid timestamp NaN`,
},
{
input: `a 1 -Inf`,
err: `invalid timestamp -Inf`,
},
{
input: `a 1 Inf`,
err: `invalid timestamp +Inf`,
},
{
input: "# TYPE hhh histogram\nhhh_bucket{le=\"+Inf\"} 1 # {aa=\"bb\"} 4 NaN",
err: `invalid exemplar timestamp NaN`,
},
{
input: "# TYPE hhh histogram\nhhh_bucket{le=\"+Inf\"} 1 # {aa=\"bb\"} 4 -Inf",
err: `invalid exemplar timestamp -Inf`,
},
}
for i, c := range cases {
p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
var err error
for err == nil {
_, err = p.Next()
}
require.Equal(t, c.err, err.Error(), "test %d: %s", i, c.input)
}
}
func TestOpenMetricsParseBareIdentifierInBraces(t *testing.T) {
require.NotPanics(t, func() {
p := NewOpenMetricsParser([]byte("{A} 0\n# EOF\n"), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
for {
et, err := p.Next()
if err != nil {
break
}
if et == EntrySeries {
var lset labels.Labels
p.Labels(&lset)
}
}
})
}
func TestOMNullByteHandling(t *testing.T) {
cases := []struct {
input string
err string
}{
{
input: "null_byte_metric{a=\"abc\x00\"} 1\n# EOF\n",
err: "",
},
{
input: "a{b=\"\x00ss\"} 1\n# EOF\n",
err: "",
},
{
input: "a{b=\"\x00\"} 1\n# EOF\n",
err: "",
},
{
input: "a{b=\"\x00\"} 1\n# EOF",
err: "",
},
{
input: "a{b=\x00\"ssss\"} 1\n# EOF\n",
err: "expected label value, got \"\\x00\" (\"INVALID\") while parsing: \"a{b=\\x00\"",
},
{
input: "a{b=\"\x00",
err: "expected label value, got \"\\\"\\x00\" (\"INVALID\") while parsing: \"a{b=\\\"\\x00\"",
},
{
input: "a{b\x00=\"hiih\"} 1",
err: "expected equal, got \"\\x00\" (\"INVALID\") while parsing: \"a{b\\x00\"",
},
{
input: "a\x00{b=\"ddd\"} 1",
err: "expected value after metric, got \"\\x00\" (\"INVALID\") while parsing: \"a\\x00\"",
},
{
input: "#",
err: "expected a valid start token, got \"#\" (\"INVALID\") while parsing: \"#\"",
},
{
input: "# H",
err: "expected a valid start token, got \"# H\" (\"INVALID\") while parsing: \"# H\"",
},
{
input: "custom_metric_total 1 # {b=\x00\"ssss\"} 1\n",
err: "expected label value, got \"\\x00\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {b=\\x00\"",
},
{
input: "custom_metric_total 1 # {b=\"\x00ss\"} 1\n",
err: "expected label value, got \"\\\"\\x00\" (\"INVALID\") while parsing: \"custom_metric_total 1 # {b=\\\"\\x00\"",
},
}
for i, c := range cases {
p := NewOpenMetricsParser([]byte(c.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
var err error
for err == nil {
_, err = p.Next()
}
if c.err == "" {
require.ErrorIs(t, err, io.EOF, "test %d", i)
continue
}
require.EqualError(t, err, c.err, "test %d", i)
}
}
// TestSTParseFailures tests known failure edge cases, we know does not work due
// current OM spec limitations or clients with broken OM format.
// TODO(maniktherana): Make sure OM 1.1/2.0 pass ST via metadata or exemplar-like to avoid this.
func TestSTParseFailures(t *testing.T) {
for _, tcase := range []struct {
name string
input string
expected []parsedEntry
}{
{
name: "_created line is a first one",
input: `# HELP thing histogram with _created as first line
# TYPE thing histogram
thing_created 1520872607.123
thing_count 17
thing_sum 324789.3
thing_bucket{le="0.0"} 0
thing_bucket{le="+Inf"} 17
# HELP thing_c counter with _created as first line
# TYPE thing_c counter
thing_c_created 1520872607.123
thing_c_total 14123.232
# EOF
`,
expected: []parsedEntry{
{
m: "thing",
help: "histogram with _created as first line",
},
{
m: "thing",
typ: model.MetricTypeHistogram,
},
{
m: `thing_count`,
st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_sum`,
st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_bucket{le="0.0"}`,
st: 0, // Should be int64p(1520872607123).
},
{
m: `thing_bucket{le="+Inf"}`,
st: 0, // Should be int64p(1520872607123),
},
{
m: "thing_c",
help: "counter with _created as first line",
},
{
m: "thing_c",
typ: model.MetricTypeCounter,
},
{
m: `thing_c_total`,
st: 0, // Should be int64p(1520872607123).
},
},
},
{
// TODO(bwplotka): Kind of correct bevaviour? If yes, let's move to the OK tests above.
name: "maybe counter with no meta",
input: `foo_total 17.0
foo_created 1520872607.123
foo_total{a="b"} 17.0
foo_created{a="b"} 1520872608.123
# EOF
`,
expected: []parsedEntry{
{
m: `foo_total`,
},
{
m: `foo_created`,
},
{
m: `foo_total{a="b"}`,
},
{
m: `foo_created{a="b"}`,
},
},
},
} {
t.Run(fmt.Sprintf("case=%v", tcase.name), func(t *testing.T) {
p := NewOpenMetricsParser([]byte(tcase.input), labels.NewSymbolTable(), WithOMParserSTSeriesSkipped())
got := testParse(t, p)
resetValAndLset(got) // Keep this test focused on metric, basic entries and ST only.
requireEntries(t, tcase.expected, got)
})
}
}
func resetValAndLset(e []parsedEntry) {
for i := range e {
e[i].v = 0
e[i].lset = labels.EmptyLabels()
}
}