mirror of
https://github.com/prometheus/prometheus.git
synced 2026-05-16 18:17:35 +02:00
This change is for demo purposes, exploring the benefits (and downsides) for the complex type format for OM2 captured in https://github.com/prometheus/docs/pull/2679. This assumes Prometheus stores NS and NHCB (and NH) going forward (for best case efficiency), but is expected to work for classic mode too with little overhead (benchmarks will tell us). Part of the PromCon talk we do with @krajorama Signed-off-by: bwplotka <bwplotka@gmail.com>
284 lines
8.4 KiB
Go
284 lines
8.4 KiB
Go
// Copyright 2022 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 (
|
|
"errors"
|
|
"io"
|
|
"testing"
|
|
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/prometheus/prometheus/config"
|
|
"github.com/prometheus/prometheus/model/exemplar"
|
|
"github.com/prometheus/prometheus/model/histogram"
|
|
"github.com/prometheus/prometheus/model/labels"
|
|
"github.com/prometheus/prometheus/util/testutil"
|
|
)
|
|
|
|
func TestNewParser(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
requireNilParser := func(t *testing.T, p Parser) {
|
|
require.Nil(t, p)
|
|
}
|
|
|
|
requirePromParser := func(t *testing.T, p Parser) {
|
|
require.NotNil(t, p)
|
|
_, ok := p.(*PromParser)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
requireOpenMetricsParser := func(t *testing.T, p Parser) {
|
|
require.NotNil(t, p)
|
|
_, ok := p.(*OpenMetricsParser)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
requireProtobufParser := func(t *testing.T, p Parser) {
|
|
require.NotNil(t, p)
|
|
_, ok := p.(*ProtobufParser)
|
|
require.True(t, ok)
|
|
}
|
|
|
|
for name, tt := range map[string]*struct {
|
|
contentType string
|
|
fallbackScrapeProtocol config.ScrapeProtocol
|
|
validateParser func(*testing.T, Parser)
|
|
err string
|
|
}{
|
|
"empty-string": {
|
|
validateParser: requireNilParser,
|
|
err: "non-compliant scrape target sending blank Content-Type and no fallback_scrape_protocol specified for target",
|
|
},
|
|
"empty-string-fallback-text-plain": {
|
|
validateParser: requirePromParser,
|
|
fallbackScrapeProtocol: config.PrometheusText0_0_4,
|
|
err: "non-compliant scrape target sending blank Content-Type, using fallback_scrape_protocol \"text/plain\"",
|
|
},
|
|
"invalid-content-type-1": {
|
|
contentType: "invalid/",
|
|
validateParser: requireNilParser,
|
|
err: "expected token after slash",
|
|
},
|
|
"invalid-content-type-1-fallback-text-plain": {
|
|
contentType: "invalid/",
|
|
validateParser: requirePromParser,
|
|
fallbackScrapeProtocol: config.PrometheusText0_0_4,
|
|
err: "expected token after slash",
|
|
},
|
|
"invalid-content-type-1-fallback-openmetrics": {
|
|
contentType: "invalid/",
|
|
validateParser: requireOpenMetricsParser,
|
|
fallbackScrapeProtocol: config.OpenMetricsText0_0_1,
|
|
err: "expected token after slash",
|
|
},
|
|
"invalid-content-type-1-fallback-protobuf": {
|
|
contentType: "invalid/",
|
|
validateParser: requireProtobufParser,
|
|
fallbackScrapeProtocol: config.PrometheusProto,
|
|
err: "expected token after slash",
|
|
},
|
|
"invalid-content-type-2": {
|
|
contentType: "invalid/invalid/invalid",
|
|
validateParser: requireNilParser,
|
|
err: "unexpected content after media subtype",
|
|
},
|
|
"invalid-content-type-2-fallback-text-plain": {
|
|
contentType: "invalid/invalid/invalid",
|
|
validateParser: requirePromParser,
|
|
fallbackScrapeProtocol: config.PrometheusText1_0_0,
|
|
err: "unexpected content after media subtype",
|
|
},
|
|
"invalid-content-type-3": {
|
|
contentType: "/",
|
|
validateParser: requireNilParser,
|
|
err: "no media type",
|
|
},
|
|
"invalid-content-type-3-fallback-text-plain": {
|
|
contentType: "/",
|
|
validateParser: requirePromParser,
|
|
fallbackScrapeProtocol: config.PrometheusText1_0_0,
|
|
err: "no media type",
|
|
},
|
|
"invalid-content-type-4": {
|
|
contentType: "application/openmetrics-text; charset=UTF-8; charset=utf-8",
|
|
validateParser: requireNilParser,
|
|
err: "duplicate parameter name",
|
|
},
|
|
"invalid-content-type-4-fallback-open-metrics": {
|
|
contentType: "application/openmetrics-text; charset=UTF-8; charset=utf-8",
|
|
validateParser: requireOpenMetricsParser,
|
|
fallbackScrapeProtocol: config.OpenMetricsText1_0_0,
|
|
err: "duplicate parameter name",
|
|
},
|
|
"openmetrics": {
|
|
contentType: "application/openmetrics-text",
|
|
validateParser: requireOpenMetricsParser,
|
|
},
|
|
"openmetrics-with-charset": {
|
|
contentType: "application/openmetrics-text; charset=utf-8",
|
|
validateParser: requireOpenMetricsParser,
|
|
},
|
|
"openmetrics-with-charset-and-version": {
|
|
contentType: "application/openmetrics-text; version=1.0.0; charset=utf-8",
|
|
validateParser: requireOpenMetricsParser,
|
|
},
|
|
"plain-text": {
|
|
contentType: "text/plain",
|
|
validateParser: requirePromParser,
|
|
},
|
|
"protobuf": {
|
|
contentType: "application/vnd.google.protobuf",
|
|
validateParser: requireProtobufParser,
|
|
},
|
|
"plain-text-with-version": {
|
|
contentType: "text/plain; version=0.0.4",
|
|
validateParser: requirePromParser,
|
|
},
|
|
"some-other-valid-content-type": {
|
|
contentType: "text/html",
|
|
validateParser: requireNilParser,
|
|
err: "received unsupported Content-Type \"text/html\" and no fallback_scrape_protocol specified for target",
|
|
},
|
|
"some-other-valid-content-type-fallback-text-plain": {
|
|
contentType: "text/html",
|
|
validateParser: requirePromParser,
|
|
fallbackScrapeProtocol: config.PrometheusText0_0_4,
|
|
err: "received unsupported Content-Type \"text/html\", using fallback_scrape_protocol \"text/plain\"",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
tt := tt // Copy to local variable before going parallel.
|
|
t.Parallel()
|
|
|
|
fallbackProtoMediaType := tt.fallbackScrapeProtocol.HeaderMediaType()
|
|
|
|
p, err := New([]byte{}, tt.contentType, labels.NewSymbolTable(), ParserOptions{FallbackContentType: fallbackProtoMediaType})
|
|
tt.validateParser(t, p)
|
|
if tt.err == "" {
|
|
require.NoError(t, err)
|
|
} else {
|
|
require.ErrorContains(t, err, tt.err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// parsedEntry represents data that is parsed for each entry.
|
|
type parsedEntry struct {
|
|
// In all but EntryComment, EntryInvalid.
|
|
m string
|
|
|
|
// In EntryHistogram.
|
|
shs *histogram.Histogram
|
|
fhs *histogram.FloatHistogram
|
|
|
|
// In EntrySeries.
|
|
v float64
|
|
|
|
// In EntrySeries and EntryHistogram.
|
|
lset labels.Labels
|
|
t *int64
|
|
es []exemplar.Exemplar
|
|
ct int64
|
|
|
|
// In EntryType.
|
|
typ model.MetricType
|
|
// In EntryHelp.
|
|
help string
|
|
// In EntryUnit.
|
|
unit string
|
|
// In EntryComment.
|
|
comment string
|
|
}
|
|
|
|
func requireEntries(t *testing.T, exp, got []parsedEntry) {
|
|
t.Helper()
|
|
|
|
testutil.RequireEqualWithOptions(t, exp, got, []cmp.Option{
|
|
// We reuse slices so we sometimes have empty vs nil differences
|
|
// we need to ignore with cmpopts.EquateEmpty().
|
|
// However we have to filter out labels, as only
|
|
// one comparer per type has to be specified,
|
|
// and RequireEqualWithOptions uses
|
|
// cmp.Comparer(labels.Equal).
|
|
cmp.FilterValues(func(x, y any) bool {
|
|
_, xIsLabels := x.(labels.Labels)
|
|
_, yIsLabels := y.(labels.Labels)
|
|
return !xIsLabels && !yIsLabels
|
|
}, cmpopts.EquateEmpty()),
|
|
cmp.AllowUnexported(parsedEntry{}),
|
|
})
|
|
}
|
|
|
|
func testParse(t testing.TB, p Parser) (ret []parsedEntry) {
|
|
t.Helper()
|
|
|
|
for {
|
|
et, err := p.Next()
|
|
if errors.Is(err, io.EOF) {
|
|
break
|
|
}
|
|
require.NoError(t, err)
|
|
|
|
var got parsedEntry
|
|
var m []byte
|
|
switch et {
|
|
case EntryInvalid:
|
|
t.Fatal("entry invalid not expected")
|
|
case EntrySeries, EntryHistogram:
|
|
var ts *int64
|
|
if et == EntrySeries {
|
|
m, ts, got.v = p.Series()
|
|
} else {
|
|
m, ts, got.shs, got.fhs = p.Histogram()
|
|
}
|
|
if ts != nil {
|
|
// TODO(bwplotka): Change to 0 in the interface for set check to
|
|
// avoid pointer mangling.
|
|
got.t = int64p(*ts)
|
|
}
|
|
got.m = string(m)
|
|
p.Labels(&got.lset)
|
|
got.ct = p.CreatedTimestamp()
|
|
|
|
for e := (exemplar.Exemplar{}); p.Exemplar(&e); {
|
|
got.es = append(got.es, e)
|
|
}
|
|
case EntryType:
|
|
m, got.typ = p.Type()
|
|
got.m = string(m)
|
|
|
|
case EntryHelp:
|
|
m, h := p.Help()
|
|
got.m = string(m)
|
|
got.help = string(h)
|
|
|
|
case EntryUnit:
|
|
m, u := p.Unit()
|
|
got.m = string(m)
|
|
got.unit = string(u)
|
|
|
|
case EntryComment:
|
|
got.comment = string(p.Comment())
|
|
}
|
|
ret = append(ret, got)
|
|
}
|
|
return ret
|
|
}
|