VictoriaMetrics/lib/protoparser/prometheus/parser_test.go
Aliaksandr Valialkin bb00bae353
Revert "Exemplar support (#5982)"
This reverts commit 5a3abfa041.

Reason for revert: exemplars aren't in wide use because they have numerous issues which prevent their adoption (see below).
Adding support for examplars into VictoriaMetrics introduces non-trivial code changes. These code changes need to be supported forever
once the release of VictoriaMetrics with exemplar support is published. That's why I don't think this is a good feature despite
that the source code of the reverted commit has an excellent quality. See https://docs.victoriametrics.com/goals/ .

Issues with Prometheus exemplars:

- Prometheus still has only experimental support for exemplars after more than three years since they were introduced.
  It stores exemplars in memory, so they are lost after Prometheus restart. This doesn't look like production-ready feature.
  See 0a2f3b3794/content/docs/instrumenting/exposition_formats.md (L153-L159)
  and https://prometheus.io/docs/prometheus/latest/feature_flags/#exemplars-storage

- It is very non-trivial to expose exemplars alongside metrics in your application, since the official Prometheus SDKs
  for metrics' exposition ( https://prometheus.io/docs/instrumenting/clientlibs/ ) either have very hard-to-use API
  for exposing histograms or do not have this API at all. For example, try figuring out how to expose exemplars
  via https://pkg.go.dev/github.com/prometheus/client_golang@v1.19.1/prometheus .

- It looks like exemplars are supported for Histogram metric types only -
  see https://pkg.go.dev/github.com/prometheus/client_golang@v1.19.1/prometheus#Timer.ObserveDurationWithExemplar .
  Exemplars aren't supported for Counter, Gauge and Summary metric types.

- Grafana has very poor support for Prometheus exemplars. It looks like it supports exemplars only when the query
  contains histogram_quantile() function. It queries exemplars via special Prometheus API -
  https://prometheus.io/docs/prometheus/latest/querying/api/#querying-exemplars - (which is still marked as experimental, btw.)
  and then displays all the returned exemplars on the graph as special dots. The issue is that this doesn't work
  in production in most cases when the histogram_quantile() is calculated over thousands of histogram buckets
  exposed by big number of application instances. Every histogram bucket may expose an exemplar on every timestamp shown on the graph.
  This makes the graph unusable, since it is litterally filled with thousands of exemplar dots.
  Neither Prometheus API nor Grafana doesn't provide the ability to filter out unneeded exemplars.

- Exemplars are usually connected to traces. While traces are good for some

I doubt exemplars will become production-ready in the near future because of the issues outlined above.

Alternative to exemplars:

Exemplars are marketed as a silver bullet for the correlation between metrics, traces and logs -
just click the exemplar dot on some graph in Grafana and instantly see the corresponding trace or log entry!
This doesn't work as expected in production as shown above. Are there better solutions, which work in production?
Yes - just use time-based and label-based correlation between metrics, traces and logs. Assign the same `job`
and `instance` labels to metrics, logs and traces, so you can quickly find the needed trace or log entry
by these labes on the time range with the anomaly on metrics' graph.
2024-07-03 15:30:21 +02:00

576 lines
14 KiB
Go

package prometheus
import (
"math"
"reflect"
"testing"
)
func TestGetRowsDiff(t *testing.T) {
f := func(s1, s2, resultExpected string) {
t.Helper()
result := GetRowsDiff(s1, s2)
if result != resultExpected {
t.Fatalf("unexpected result for GetRowsDiff(%q, %q); got %q; want %q", s1, s2, result, resultExpected)
}
}
f("", "", "")
f("", "foo 1", "")
f(" ", "foo 1", "")
f("foo 123", "", "foo 0\n")
f("foo 123", "bar 3", "foo 0\n")
f("foo 123", "bar 3\nfoo 344", "")
f("foo{x=\"y\", z=\"a a a\"} 123", "bar 3\nfoo{x=\"y\", z=\"b b b\"} 344", "foo{x=\"y\",z=\"a a a\"} 0\n")
f("foo{bar=\"baz\"} 123\nx 3.4 5\ny 5 6", "x 34 342", "foo{bar=\"baz\"} 0\ny 0\n")
}
func TestAreIdenticalSeriesFast(t *testing.T) {
f := func(s1, s2 string, resultExpected bool) {
t.Helper()
result := AreIdenticalSeriesFast(s1, s2)
if result != resultExpected {
t.Fatalf("unexpected result for AreIdenticalSeries(%q, %q); got %v; want %v", s1, s2, result, resultExpected)
}
}
f("", "", true)
f("", "a 1", false) // different number of metrics
f(" ", " a 1", false) // different number of metrics
f("a 1", "", false) // different number of metrics
f(" a 1", " ", false) // different number of metrics
f("foo", "foo", true) // consider series identical if they miss value
f("foo 1", "foo 1", true)
f("foo 1", "foo 2", true)
f("foo 1 ", "foo 2 ", true)
f("foo 1 ", "foo 2 ", false) // different number of spaces
f("foo 1 ", "foo 2 ", false) // different number of spaces
f("foo nan", "foo -inf", true)
f("foo 1 # coment x", "foo 2 #comment y", true)
f(" foo 1", " foo 1", true)
f(" foo 1", " foo 1", false) // different number of spaces in front of metric
f(" foo 1", " foo 1", false) // different number of spaces in front of metric
f("foo 1", "bar 1", false) // different metric name
f("foo 1", "fooo 1", false) // different metric name
f("foo 123", "foo 32.32", true)
f(`foo{bar="x"} -3.3e-6`, `foo{bar="x"} 23343`, true)
f(`foo{} 1`, `foo{} 234`, true)
f(`foo {x="y x" } 234`, `foo {x="y x" } 43.342`, true)
f(`foo {x="y x"} 234`, `foo{x="y x"} 43.342`, false) // different spaces
f("foo 2\nbar 3", "foo 34.43\nbar -34.3", true)
f("foo 2\nbar 3", "foo 34.43\nbarz -34.3", false) // different metric names
f("\nfoo 13\n", "\nfoo 3.4\n", true)
f("\nfoo 13", "\nfoo 3.4\n", false) // different number of blank lines
f("\nfoo 13\n", "\nfoo 3.4", false) // different number of blank lines
f("\n\nfoo 1", "\n\nfoo 34.43", true)
f("\n\nfoo 3434\n", "\n\nfoo 43\n", true)
f("\nfoo 1", "\n\nfoo 34.43", false) // different number of blank lines
f("#foo{bar}", "#baz", true)
f("", "#baz", false) // different number of comments
f("#foo{bar}", "", false) // different number of comments
f("#foo{bar}", "bar 3", false) // different number of comments
f("foo{bar} 2", "#bar 3", false) // different number of comments
f("#foo\n", "#bar", false) // different number of blank lines
f("#foo{bar}\n#baz", "#baz\n#xdsfds dsf", true)
f("# foo\nbar 234\nbaz{x=\"y\", z=\"\"} 3", "# foo\nbar 3.3\nbaz{x=\"y\", z=\"\"} 4323", true)
f("# foo\nbar 234\nbaz{x=\"z\", z=\"\"} 3", "# foo\nbar 3.3\nbaz{x=\"y\", z=\"\"} 4323", false) // different label value
f("foo {bar=\"xfdsdsffdsa\"} 1", "foo {x=\"y\"} 2", false) // different labels
f("foo {x=\"z\"} 1", "foo {x=\"y\"} 2", false) // different label value
// Lines with timestamps
f("foo 1 2", "foo 234 4334", true)
f("foo 2", "foo 3 4", false) // missing timestamp
f("foo 2 1", "foo 3", false) // missing timestamp
f("foo{bar=\"b az\"} 2 5", "foo{bar=\"b az\"} +6.3 7.43", true)
f("foo{bar=\"b az\"} 2 5 # comment ss ", "foo{bar=\"b az\"} +6.3 7.43 # comment as ", true)
f("foo{bar=\"b az\"} 2 5 #comment", "foo{bar=\"b az\"} +6.3 7.43 #comment {foo=\"bar\"} 21.44", true)
f("foo{bar=\"b az\"} +Inf 5", "foo{bar=\"b az\"} NaN 7.43", true)
f("foo{bar=\"b az\"} +Inf 5", "foo{bar=\"b az\"} nan 7.43", true)
f("foo{bar=\"b az\"} +Inf 5", "foo{bar=\"b az\"} nansf 7.43", false) // invalid value
// False positive - whitespace after the numeric char in the label.
f(`foo{bar=" 12.3 "} 1`, `foo{bar=" 13 "} 23`, true)
f(`foo{bar=" 12.3 "} 1 3443`, `foo{bar=" 13 "} 23 4345`, true)
f(`foo{bar=" 12.3 "} 1 3443 # {} 34`, `foo{bar=" 13 "} 23 4345 # {foo=" bar "} 34`, true)
// Metrics and labels with '#' chars
f(`foo{bar="#1"} 1`, `foo{bar="#1"} 1`, true)
f(`foo{bar="a#1"} 1`, `foo{bar="b#1"} 1.4`, false)
f(`foo{bar=" #1"} 1`, `foo{bar=" #1"} 3`, true)
f(`foo{bar=" #1 "} 1`, `foo{bar=" #1 "} -2.34 343.34 # {foo="#bar"} `, true)
f(`foo{bar=" #1"} 1`, `foo{bar="#1"} 1`, false)
f(`foo{b#ar=" #1"} 1`, `foo{b#ar=" #1"} 1.23`, true)
f(`foo{z#ar=" #1"} 1`, `foo{b#ar=" #1"} 1.23`, false)
f(`fo#o{b#ar="#1"} 1`, `fo#o{b#ar="#1"} 1.23`, true)
f(`fo#o{b#ar="#1"} 1`, `fa#o{b#ar="#1"} 1.23`, false)
// False positive - the value after '#' char can be arbitrary
f(`fo#o{b#ar="#1"} 1`, `fo#osdf 1.23`, true)
}
func TestPrevBackslashesCount(t *testing.T) {
f := func(s string, nExpected int) {
t.Helper()
n := prevBackslashesCount(s)
if n != nExpected {
t.Fatalf("unexpected value returned from prevBackslashesCount(%q); got %d; want %d", s, n, nExpected)
}
}
f(``, 0)
f(`foo`, 0)
f(`\`, 1)
f(`\\`, 2)
f(`\\\`, 3)
f(`\\\a`, 0)
f(`foo\bar`, 0)
f(`foo\\`, 2)
f(`\\foo\`, 1)
f(`\\foo\\\\`, 4)
}
func TestFindClosingQuote(t *testing.T) {
f := func(s string, nExpected int) {
t.Helper()
n := findClosingQuote(s)
if n != nExpected {
t.Fatalf("unexpected value returned from findClosingQuote(%q); got %d; want %d", s, n, nExpected)
}
}
f(``, -1)
f(`x`, -1)
f(`"`, -1)
f(`""`, 1)
f(`foobar"`, -1)
f(`"foo"`, 4)
f(`"\""`, 3)
f(`"\\"`, 3)
f(`"\"`, -1)
f(`"foo\"bar\"baz"`, 14)
}
func TestUnescapeValue(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := unescapeValue(s)
if result != resultExpected {
t.Fatalf("unexpected result; got %q; want %q", result, resultExpected)
}
}
f(``, "")
f(`f`, "f")
f(`foobar`, "foobar")
f(`\"\n\t`, "\"\n\\t")
// Edge cases
f(`foo\bar`, "foo\\bar")
f(`foo\`, "foo\\")
}
func TestAppendEscapedValue(t *testing.T) {
f := func(s, resultExpected string) {
t.Helper()
result := appendEscapedValue(nil, s)
if string(result) != resultExpected {
t.Fatalf("unexpected result; got %q; want %q", result, resultExpected)
}
}
f(``, ``)
f(`f`, `f`)
f(`foobar`, `foobar`)
f("\"\n\t\\xyz", "\\\"\\n\t\\\\xyz")
}
func TestRowsUnmarshalFailure(t *testing.T) {
f := func(s string) {
t.Helper()
var rows Rows
rows.Unmarshal(s)
if len(rows.Rows) != 0 {
t.Fatalf("unexpected number of rows parsed; got %d; want 0;\nrows:%#v", len(rows.Rows), rows.Rows)
}
// Try again
rows.Unmarshal(s)
if len(rows.Rows) != 0 {
t.Fatalf("unexpected number of rows parsed; got %d; want 0;\nrows:%#v", len(rows.Rows), rows.Rows)
}
}
// Empty lines and comments
f("")
f(" ")
f("\t")
f("\t \r")
f("\t\t \n\n # foobar")
f("#foobar")
f("#foobar\n")
// invalid tags
f("a{")
f("a { ")
f("a {foo")
f("a {foo} 3")
f("a {foo =")
f(`a {foo ="bar`)
f(`a {foo ="b\ar`)
f(`a {foo = "bar"`)
f(`a {foo ="bar",`)
f(`a {foo ="bar" , `)
f(`a {foo ="bar" , baz } 2`)
// invalid tags - see https://github.com/VictoriaMetrics/VictoriaMetrics/issues/4284
f(`a{"__name__":"upsd_time_left_ns","host":"myhost", status_OB="true"} 12`)
f(`a{host:"myhost"} 12`)
f(`a{host:"myhost",foo="bar"} 12`)
// empty metric name
f(`{foo="bar"}`)
// Invalid quotes for label value
f(`{foo='bar'} 23`)
f("{foo=`bar`} 23")
// Missing value
f("aaa")
f(" aaa")
f(" aaa ")
f(" aaa \n")
f(` aa{foo="bar"} ` + "\n")
// Invalid value
f("foo bar")
f("foo bar 124")
// Invalid timestamp
f("foo 123 bar")
}
func TestRowsUnmarshalSuccess(t *testing.T) {
f := func(s string, rowsExpected *Rows) {
t.Helper()
var rows Rows
rows.Unmarshal(s)
if !reflect.DeepEqual(rows.Rows, rowsExpected.Rows) {
t.Fatalf("unexpected rows;\ngot\n%+v;\nwant\n%+v", rows.Rows, rowsExpected.Rows)
}
// Try unmarshaling again
rows.Unmarshal(s)
if !reflect.DeepEqual(rows.Rows, rowsExpected.Rows) {
t.Fatalf("unexpected rows;\ngot\n%+v;\nwant\n%+v", rows.Rows, rowsExpected.Rows)
}
rows.Reset()
if len(rows.Rows) != 0 {
t.Fatalf("non-empty rows after reset: %+v", rows.Rows)
}
}
// Empty line or comment
f("", &Rows{})
f("\r", &Rows{})
f("\n\n", &Rows{})
f("\n\r\n", &Rows{})
f("\t \t\n\r\n#foobar\n # baz", &Rows{})
// Single line
f("foobar 78.9", &Rows{
Rows: []Row{{
Metric: "foobar",
Value: 78.9,
}},
})
f("foobar 123.456 789\n", &Rows{
Rows: []Row{{
Metric: "foobar",
Value: 123.456,
Timestamp: 789000,
}},
})
f("foobar{} 123.456 789.4354\n", &Rows{
Rows: []Row{{
Metric: "foobar",
Value: 123.456,
Timestamp: 789435,
}},
})
f(`# _ _
# ___ __ _ ___ ___ __ _ _ __ __| |_ __ __ _ _____ ___ __ ___ _ __| |_ ___ _ __
`+"# / __/ _` / __/ __|/ _` | '_ \\ / _` | '__/ _` |_____ / _ \\ \\/ / '_ \\ / _ \\| '__| __/ _ \\ '__|\n"+`
# | (_| (_| \__ \__ \ (_| | | | | (_| | | | (_| |_____| __/> <| |_) | (_) | | | || __/ |
# \___\__,_|___/___/\__,_|_| |_|\__,_|_| \__,_| \___/_/\_\ .__/ \___/|_| \__\___|_|
# |_|
#
# TYPE cassandra_token_ownership_ratio gauge
cassandra_token_ownership_ratio 78.9`, &Rows{
Rows: []Row{{
Metric: "cassandra_token_ownership_ratio",
Value: 78.9,
}},
})
// `#` char in label value
f(`foo{bar="#1 az"} 24`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{{
Key: "bar",
Value: "#1 az",
}},
Value: 24,
}},
})
// `#` char in label name and label value
f(`foo{bar#2="#1 az"} 24 456`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{{
Key: "bar#2",
Value: "#1 az",
}},
Value: 24,
Timestamp: 456000,
}},
})
// `#` char in metric name, label name and label value
f(`foo#qw{bar#2="#1 az"} 24 456 # foobar {baz="x"}`, &Rows{
Rows: []Row{{
Metric: "foo#qw",
Tags: []Tag{{
Key: "bar#2",
Value: "#1 az",
}},
Value: 24,
Timestamp: 456000,
}},
})
// Incorrectly escaped backlash. This is real-world case, which must be supported.
f(`mssql_sql_server_active_transactions_sec{loginname="domain\somelogin",env="develop"} 56`, &Rows{
Rows: []Row{{
Metric: "mssql_sql_server_active_transactions_sec",
Tags: []Tag{
{
Key: "loginname",
Value: "domain\\somelogin",
},
{
Key: "env",
Value: "develop",
},
},
Value: 56,
}},
})
// Exemplars - see https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md#exemplars-1
f(`foo_bucket{le="10",a="#b"} 17 # {trace_id="oHg5SJ#YRHA0"} 9.8 1520879607.789
abc 123 456 # foobar
foo 344#bar`, &Rows{
Rows: []Row{
{
Metric: "foo_bucket",
Tags: []Tag{
{
Key: "le",
Value: "10",
},
{
Key: "a",
Value: "#b",
},
},
Value: 17,
},
{
Metric: "abc",
Value: 123,
Timestamp: 456000,
},
{
Metric: "foo",
Value: 344,
},
},
})
// "Infinity" word - this has been added in OpenMetrics.
// See https://github.com/OpenObservability/OpenMetrics/blob/master/OpenMetrics.md
// Checks for https://github.com/VictoriaMetrics/VictoriaMetrics/issues/924
inf := math.Inf(1)
f(`
foo Infinity
bar +Infinity
baz -infinity
aaa +inf
bbb -INF
ccc INF
`, &Rows{
Rows: []Row{
{
Metric: "foo",
Value: inf,
},
{
Metric: "bar",
Value: inf,
},
{
Metric: "baz",
Value: -inf,
},
{
Metric: "aaa",
Value: inf,
},
{
Metric: "bbb",
Value: -inf,
},
{
Metric: "ccc",
Value: inf,
},
},
})
// Timestamp bigger than 1<<31.
// It should be parsed in milliseconds.
f("aaa 1123 429496729600", &Rows{
Rows: []Row{{
Metric: "aaa",
Value: 1123,
Timestamp: 429496729600,
}},
})
// Floating-point timestamps in OpenMetric format.
f("aaa 1123 42949.567", &Rows{
Rows: []Row{{
Metric: "aaa",
Value: 1123,
Timestamp: 42949567,
}},
})
// Tags
f(`foo{bar="baz"} 1 2`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{{
Key: "bar",
Value: "baz",
}},
Value: 1,
Timestamp: 2000,
}},
})
f(`foo{bar="b\"a\\z"} -1.2`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{{
Key: "bar",
Value: "b\"a\\z",
}},
Value: -1.2,
}},
})
// Empty tags
f(`foo {bar="baz",aa="",x="y",="z"} 1 2`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{
{
Key: "bar",
Value: "baz",
},
{
Key: "aa",
Value: "",
},
{
Key: "x",
Value: "y",
},
},
Value: 1,
Timestamp: 2000,
}},
})
// Trailing comma after tag
// See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/350
f(`foo{bar="baz",} 1 2`, &Rows{
Rows: []Row{{
Metric: "foo",
Tags: []Tag{{
Key: "bar",
Value: "baz",
}},
Value: 1,
Timestamp: 2000,
}},
})
// Multi lines
f("# foo\n # bar ba zzz\nfoo 0.3 2\naaa 3\nbar.baz 0.34 43\n", &Rows{
Rows: []Row{
{
Metric: "foo",
Value: 0.3,
Timestamp: 2000,
},
{
Metric: "aaa",
Value: 3,
},
{
Metric: "bar.baz",
Value: 0.34,
Timestamp: 43000,
},
},
})
// Multi lines with invalid line
f("\t foo\t { } 0.3\t 2\naaa\n bar.baz 0.34 43\n", &Rows{
Rows: []Row{
{
Metric: "foo",
Value: 0.3,
Timestamp: 2000,
},
{
Metric: "bar.baz",
Value: 0.34,
Timestamp: 43000,
},
},
})
// Spaces around tags
f(`vm_accounting { name="vminsertRows", accountID = "1" , projectID= "1" } 277779100`, &Rows{
Rows: []Row{
{
Metric: "vm_accounting",
Tags: []Tag{
{
Key: "name",
Value: "vminsertRows",
},
{
Key: "accountID",
Value: "1",
},
{
Key: "projectID",
Value: "1",
},
},
Value: 277779100,
Timestamp: 0,
},
},
})
}