mirror of
https://github.com/prometheus/prometheus.git
synced 2026-10-28 10:44:59 +08:00
storage/semconv: remove legacy 1.0/1.1 schemas, embedded registry, and testdata YAMLs
Drops the inherited 1.0.0/1.1.0 schema parser, __semconv_url__ corroboration engine, embedded toy registry fixtures (storage/semconv/registry/{1.0.0,1.1.0,1.2.0,registry.yaml}), and upstream testdata YAML dumps, keeping only transform/2.0 Telemetry Policy translation.
Signed-off-by: Bartek Plotka <bwplotka@google.com>
This commit is contained in:
1 parent
6509cd7a3c
commit
8e609892db
36 files changed
+163
-13042
No files matched your search
@@ -1,318 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/storage/semconv"
|
||||
"github.com/prometheus/prometheus/tsdb/chunkenc"
|
||||
"github.com/prometheus/prometheus/util/annotations"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
// benchSemconv declares the metric under test with one attribute, plus bench.solo,
|
||||
// which no schema below renames. A query for bench.solo therefore fans out to a
|
||||
// single variant with nothing rewritten but __name__, which is the common shape.
|
||||
const benchSemconv = `
|
||||
groups:
|
||||
- id: registry.attrs
|
||||
type: attribute_group
|
||||
brief: "Attributes"
|
||||
attributes:
|
||||
- id: %[2]s
|
||||
type: string
|
||||
stability: stable
|
||||
brief: "An attribute"
|
||||
examples: ["a"]
|
||||
- id: metric.%[1]s
|
||||
type: metric
|
||||
brief: "A metric"
|
||||
stability: stable
|
||||
metric_name: %[1]s
|
||||
instrument: counter
|
||||
unit: By
|
||||
attributes:
|
||||
- ref: %[2]s
|
||||
- id: metric.bench.solo
|
||||
type: metric
|
||||
brief: "A metric no schema renames"
|
||||
stability: stable
|
||||
metric_name: bench.solo
|
||||
instrument: counter
|
||||
unit: By
|
||||
attributes:
|
||||
- ref: attr.solo
|
||||
`
|
||||
|
||||
// benchMetricRenameSchema renames only the metric, so nothing a returned series
|
||||
// carries is rewritten apart from __name__.
|
||||
const benchMetricRenameSchema = `
|
||||
file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
bench.old: bench.new
|
||||
`
|
||||
|
||||
// benchAttributeRenameSchema renames the metric and one of its attributes, which
|
||||
// is what forces a variant to be buffered and re-sorted.
|
||||
const benchAttributeRenameSchema = `
|
||||
file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
bench.old: bench.new
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old: attr.new
|
||||
apply_to_metrics:
|
||||
- bench.new
|
||||
`
|
||||
|
||||
// benchAttrOf gives each benchmark metric the attribute name its era declares.
|
||||
var benchAttrOf = map[string]string{
|
||||
"bench.old": "attr.old",
|
||||
"bench.new": "attr.new",
|
||||
"bench.solo": "attr.solo",
|
||||
}
|
||||
|
||||
// benchStorage builds a storage holding seriesCount series, spread evenly over the
|
||||
// given metric names.
|
||||
func benchStorage(b *testing.B, schema string, seriesCount int, metrics ...string) storage.Storage {
|
||||
b.Helper()
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(teststorage.New(b), map[string][]byte{
|
||||
"registry.yaml": []byte(schema),
|
||||
"1.0.0": fmt.Appendf(nil, benchSemconv, "bench.old", "attr.old"),
|
||||
"1.1.0": fmt.Appendf(nil, benchSemconv, "bench.new", "attr.new"),
|
||||
})
|
||||
require.NoError(b, err)
|
||||
|
||||
app := wrapped.Appender(context.Background())
|
||||
for i := range seriesCount {
|
||||
metric := metrics[i%len(metrics)]
|
||||
attr := benchAttrOf[metric]
|
||||
// Zero-padded so the stored order is stable and the series are spread
|
||||
// across both eras rather than clustered.
|
||||
lset := labels.FromStrings(model.MetricNameLabel, metric, attr, fmt.Sprintf("v%05d", i))
|
||||
_, err := app.Append(0, lset, 1, float64(i))
|
||||
require.NoError(b, err)
|
||||
}
|
||||
require.NoError(b, app.Commit())
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// BenchmarkSelectFanOut measures a fanned-out Select, drained the way PromQL
|
||||
// drains one. Metric-only variants stream without querying label metadata;
|
||||
// attribute renames still require buffering and sorting.
|
||||
func BenchmarkSelectFanOut(b *testing.B) {
|
||||
for _, sc := range []struct {
|
||||
name string
|
||||
schema string
|
||||
query string
|
||||
metrics []string
|
||||
}{
|
||||
// One variant, with only the canonical __name__ rewrite.
|
||||
{"no rename", benchMetricRenameSchema, "bench.solo", []string{"bench.solo"}},
|
||||
// Two variants, still with only __name__ rewritten.
|
||||
{"metric rename only", benchMetricRenameSchema, "bench.new", []string{"bench.old", "bench.new"}},
|
||||
// Two variants with an attribute rewritten as well.
|
||||
{"attribute rename", benchAttributeRenameSchema, "bench.new", []string{"bench.old", "bench.new"}},
|
||||
} {
|
||||
for _, seriesCount := range []int{100, 10000} {
|
||||
b.Run(fmt.Sprintf("%s/%d series", sc.name, seriesCount), func(b *testing.B) {
|
||||
s := benchStorage(b, sc.schema, seriesCount, sc.metrics...)
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, sc.query),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for b.Loop() {
|
||||
q, err := s.Querier(0, 10)
|
||||
require.NoError(b, err)
|
||||
|
||||
var series, samples int
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
for set.Next() {
|
||||
series++
|
||||
it := set.At().Iterator(nil)
|
||||
for it.Next() == chunkenc.ValFloat {
|
||||
samples++
|
||||
}
|
||||
}
|
||||
require.NoError(b, set.Err())
|
||||
require.NoError(b, q.Close())
|
||||
if series != seriesCount {
|
||||
b.Fatalf("got %d series, want %d", series, seriesCount)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const benchLatencyVariants = 32
|
||||
|
||||
type latencyStorage struct {
|
||||
storage.Storage
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (s latencyStorage) Querier(mint, maxt int64) (storage.Querier, error) {
|
||||
q, err := s.Storage.Querier(mint, maxt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &latencyQuerier{Querier: q, delay: s.delay}, nil
|
||||
}
|
||||
|
||||
func (s latencyStorage) ChunkQuerier(mint, maxt int64) (storage.ChunkQuerier, error) {
|
||||
q, err := s.Storage.ChunkQuerier(mint, maxt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &latencyChunkQuerier{ChunkQuerier: q, delay: s.delay}, nil
|
||||
}
|
||||
|
||||
type latencyQuerier struct {
|
||||
storage.Querier
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (q *latencyQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.SeriesSet {
|
||||
time.Sleep(q.delay)
|
||||
return storage.NoopSeriesSet()
|
||||
}
|
||||
|
||||
func (q *latencyQuerier) LabelNames(context.Context, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
time.Sleep(q.delay)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (q *latencyQuerier) LabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
time.Sleep(q.delay)
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
type latencyChunkQuerier struct {
|
||||
storage.ChunkQuerier
|
||||
delay time.Duration
|
||||
}
|
||||
|
||||
func (q *latencyChunkQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.ChunkSeriesSet {
|
||||
time.Sleep(q.delay)
|
||||
return storage.NoopChunkedSeriesSet()
|
||||
}
|
||||
|
||||
func benchLatencyRegistry() map[string][]byte {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
`)
|
||||
for i := range benchLatencyVariants - 1 {
|
||||
schema = fmt.Appendf(schema, " bench.old.%02d: bench.current\n", i)
|
||||
}
|
||||
return map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.1.0": fmt.Appendf(nil, benchSemconv, "bench.current", "attr.current"),
|
||||
}
|
||||
}
|
||||
|
||||
func benchLatencyStorage(b *testing.B) storage.Storage {
|
||||
b.Helper()
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(
|
||||
latencyStorage{Storage: teststorage.New(b), delay: time.Millisecond},
|
||||
benchLatencyRegistry(),
|
||||
)
|
||||
require.NoError(b, err)
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func benchLatencyMatchers() []*labels.Matcher {
|
||||
return []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "bench.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkFanOutCallLatency measures the storage-call portion of schema
|
||||
// fan-out independently of series decoding and iteration.
|
||||
func BenchmarkFanOutCallLatency(b *testing.B) {
|
||||
for _, operation := range []string{"select", "chunk select", "label names", "label values"} {
|
||||
b.Run(operation, func(b *testing.B) {
|
||||
s := benchLatencyStorage(b)
|
||||
matchers := benchLatencyMatchers()
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
|
||||
for b.Loop() {
|
||||
switch operation {
|
||||
case "select":
|
||||
q, err := s.Querier(0, 10)
|
||||
require.NoError(b, err)
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
for set.Next() {
|
||||
}
|
||||
require.NoError(b, set.Err())
|
||||
require.NoError(b, q.Close())
|
||||
case "chunk select":
|
||||
q, err := s.ChunkQuerier(0, 10)
|
||||
require.NoError(b, err)
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
for set.Next() {
|
||||
}
|
||||
require.NoError(b, set.Err())
|
||||
require.NoError(b, q.Close())
|
||||
case "label names":
|
||||
q, err := s.Querier(0, 10)
|
||||
require.NoError(b, err)
|
||||
_, _, err = q.LabelNames(context.Background(), nil, matchers...)
|
||||
require.NoError(b, err)
|
||||
require.NoError(b, q.Close())
|
||||
case "label values":
|
||||
q, err := s.Querier(0, 10)
|
||||
require.NoError(b, err)
|
||||
_, _, err = q.LabelValues(context.Background(), "attr.current", nil, matchers...)
|
||||
require.NoError(b, err)
|
||||
require.NoError(b, q.Close())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+33
-1991
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+18
-781
@@ -14,663 +14,29 @@
|
||||
package semconv
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"maps"
|
||||
"path"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/grafana/regexp"
|
||||
"go.yaml.in/yaml/v3"
|
||||
)
|
||||
|
||||
// embeddedRegistry holds the bundled semconv and OTel schema files served as
|
||||
// the default source from which __semconv_url__ and __schema_url__ values are
|
||||
// resolved. Operators may replace it with their own registry via configuration
|
||||
// (see AwareStorageWithRegistry); either way, queries can only ever name files
|
||||
// inside the registry namespace. Accepting arbitrary HTTP URLs or local
|
||||
// filesystem paths from the matchers themselves would expose a server-side
|
||||
// fetch primitive to anyone able to issue a PromQL query, which is why the
|
||||
// matcher values are gated by registryURLRe rather than treated as locations.
|
||||
//
|
||||
//go:embed registry/*
|
||||
var embeddedRegistry embed.FS
|
||||
|
||||
// registryURLRe matches the only accepted shape of __semconv_url__ /
|
||||
// __schema_url__ values: a single non-empty path segment under registry/.
|
||||
// Path traversal (..) and absolute paths are rejected.
|
||||
// registryURLRe matches the only accepted shape of __schema_url__ values:
|
||||
// a single non-empty path segment under registry/. Path traversal (..) and
|
||||
// absolute paths are rejected.
|
||||
var registryURLRe = regexp.MustCompile(`^registry/[^/.][^/]*$`)
|
||||
|
||||
// semverRe matches plain MAJOR.MINOR.PATCH version strings. Pre-release and
|
||||
// build metadata are intentionally not accepted; semconv versions in the
|
||||
// registry are plain dotted integers.
|
||||
var semverRe = regexp.MustCompile(`^\d+\.\d+\.\d+$`)
|
||||
|
||||
// validateSemver returns an error if v is not a plain MAJOR.MINOR.PATCH string.
|
||||
func validateSemver(v string) error {
|
||||
if !semverRe.MatchString(v) {
|
||||
return fmt.Errorf("invalid semver %q: expected MAJOR.MINOR.PATCH", v)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// semconv represents a semantic conventions file containing metric definitions.
|
||||
// See: https://github.com/open-telemetry/semantic-conventions
|
||||
//
|
||||
// Example semconv YAML:
|
||||
//
|
||||
// groups:
|
||||
// - id: metric.http.server.duration
|
||||
// type: metric
|
||||
// metric_name: http.server.duration
|
||||
// instrument: histogram
|
||||
// unit: s
|
||||
type semconv struct {
|
||||
Groups []semconvGroup `yaml:"groups"`
|
||||
|
||||
version string
|
||||
|
||||
// metrics indexes this version's metric groups by their metric_name
|
||||
// (populated by loadSemconv). It answers both "is this name a metric at this
|
||||
// version" and "what does it declare", which is what validating a resolved
|
||||
// rename lineage needs.
|
||||
metrics map[string]metricDef
|
||||
|
||||
// ambiguousMetrics lists metric names declared by more than one group at
|
||||
// this version, sorted. Such a name has no single definition, so anything
|
||||
// resolved through it is unreliable and is reported to the caller rather
|
||||
// than silently resolved to whichever group happened to be parsed last.
|
||||
ambiguousMetrics []string
|
||||
}
|
||||
|
||||
// metricDef is the part of a semconv metric group that describes what the metric
|
||||
// measures, as opposed to what it is currently called. Upstream semantic
|
||||
// conventions forbid a stable metric from changing unit or instrument (see
|
||||
// policies/compatibility.rego in open-telemetry/semantic-conventions), so a
|
||||
// mismatch is evidence against a schema rename branch. Equality alone is not
|
||||
// identity evidence because unrelated metrics may share both fields.
|
||||
//
|
||||
// Note that a group's id is deliberately not part of this: upstream lints id to
|
||||
// be exactly "metric.<metric_name>" (policies/yaml_schema.rego), so it is a pure
|
||||
// function of the surface name and changes in lockstep with every rename. It
|
||||
// therefore carries no identity information a rename edge does not already have.
|
||||
type metricDef struct {
|
||||
unit string
|
||||
instrument string
|
||||
stability string
|
||||
attributes []string
|
||||
}
|
||||
|
||||
// sameMetricAs reports whether d and other have no unit or instrument
|
||||
// contradiction. Directional schema traversal, not this comparison, establishes
|
||||
// the candidate lineage.
|
||||
func (d metricDef) sameMetricAs(other metricDef) bool {
|
||||
return d.unit == other.unit && d.instrument == other.instrument
|
||||
}
|
||||
|
||||
// stablyContradicts reports whether both definitions are explicitly stable and
|
||||
// disagree on identity metadata that both of them specify.
|
||||
func (d metricDef) stablyContradicts(other metricDef) bool {
|
||||
if d.stability != "stable" || other.stability != "stable" {
|
||||
return false
|
||||
}
|
||||
return specifiedDifference(d.unit, other.unit) || specifiedDifference(d.instrument, other.instrument)
|
||||
}
|
||||
|
||||
func specifiedDifference(a, b string) bool {
|
||||
return a != "" && b != "" && a != b
|
||||
}
|
||||
|
||||
// attributesOf returns the attributes the named metric declares at this semconv
|
||||
// version, or nil if the name is not a metric here.
|
||||
func (s semconv) attributesOf(name string) []string {
|
||||
return s.metrics[name].attributes
|
||||
}
|
||||
|
||||
// otelSchema represents an OpenTelemetry schema file.
|
||||
// See: https://opentelemetry.io/docs/specs/otel/schemas/file_format_v1.1.0/
|
||||
//
|
||||
// Example schema YAML:
|
||||
//
|
||||
// file_format: 1.1.0
|
||||
// schema_url: https://example.com/schemas/1.0.0
|
||||
//
|
||||
// versions:
|
||||
// 1.0.0:
|
||||
// metrics:
|
||||
// changes:
|
||||
// - rename_attributes:
|
||||
// attribute_map:
|
||||
// http.method: http.request.method
|
||||
// apply_to_metrics:
|
||||
// - http.server.duration
|
||||
// otelSchema represents an OpenTelemetry Telemetry Policy transformation schema
|
||||
// file (file_format: transform/2.0, see open-telemetry/weaver#1850).
|
||||
type otelSchema struct {
|
||||
FileFormat string `yaml:"file_format"`
|
||||
SchemaURL string `yaml:"schema_url"`
|
||||
Versions map[string]otelSchemaVersion `yaml:"versions"`
|
||||
FileFormat string `yaml:"file_format"`
|
||||
SchemaURL string `yaml:"schema_url"`
|
||||
|
||||
// revisions holds the schema revisions that contain transformations, sorted
|
||||
// by version. Transformations retain their file order because applying a
|
||||
// revision backwards requires replaying them in the exact reverse order.
|
||||
revisions []schemaRevision
|
||||
|
||||
// allVersions lists every declared schema version in ascending order,
|
||||
// including versions without transformations.
|
||||
allVersions []string
|
||||
|
||||
// transform holds the parsed policy definition when FileFormat is transform/2.0.
|
||||
transform *transformFile
|
||||
}
|
||||
|
||||
// predecessorOfWithBudget returns the version immediately before v in the
|
||||
// complete schema history.
|
||||
func (s *otelSchema) predecessorOfWithBudget(v string, budget *schemaExpansionBudget) (string, bool, error) {
|
||||
for i, have := range s.allVersions {
|
||||
if err := budget.reserveWork(1); err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
if have == v {
|
||||
if i == 0 {
|
||||
return "", false, nil
|
||||
}
|
||||
return s.allVersions[i-1], true, nil
|
||||
}
|
||||
}
|
||||
return "", false, nil
|
||||
}
|
||||
|
||||
// eraVersionsOfWithBudget returns schema boundaries where name is current.
|
||||
// Names used only between transformations in one revision have no boundary.
|
||||
func (s *otelSchema) eraVersionsOfWithBudget(name string, budget *schemaExpansionBudget) ([]string, error) {
|
||||
seen := map[string]struct{}{}
|
||||
var versions []string
|
||||
add := func(version string) error {
|
||||
if version == "" {
|
||||
return nil
|
||||
}
|
||||
if _, exists := seen[version]; exists {
|
||||
return nil
|
||||
}
|
||||
if err := budget.reserveWork(1); err != nil {
|
||||
return err
|
||||
}
|
||||
seen[version] = struct{}{}
|
||||
versions = append(versions, version)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, revision := range s.revisions {
|
||||
if err := budget.reserveWork(1); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
before, after, _, err := revision.metricBoundaryWithBudget(name, budget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if before {
|
||||
predecessor, ok, err := s.predecessorOfWithBudget(revision.version, budget)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ok {
|
||||
if err := add(predecessor); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
if after {
|
||||
if err := add(revision.version); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.SortFunc(versions, compareSemver)
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
// metricBoundaryWithBudget reports whether name exists immediately before and
|
||||
// after this revision. A name used only between ordered transformations is
|
||||
// mentioned but absent at both version boundaries.
|
||||
func (r schemaRevision) metricBoundaryWithBudget(name string, budget *schemaExpansionBudget) (before, after, mentioned bool, err error) {
|
||||
for _, change := range r.changes {
|
||||
if err := budget.reserveWork(1); err != nil {
|
||||
return false, false, false, err
|
||||
}
|
||||
if change.metricRenames == nil {
|
||||
continue
|
||||
}
|
||||
if _, source := change.metricRenames.forward[name]; source {
|
||||
before = true
|
||||
mentioned = true
|
||||
break
|
||||
}
|
||||
if _, target := change.metricRenames.reverse[name]; target {
|
||||
mentioned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, change := range slices.Backward(r.changes) {
|
||||
if err := budget.reserveWork(1); err != nil {
|
||||
return false, false, false, err
|
||||
}
|
||||
if change.metricRenames == nil {
|
||||
continue
|
||||
}
|
||||
if _, target := change.metricRenames.reverse[name]; target {
|
||||
after = true
|
||||
break
|
||||
}
|
||||
if _, source := change.metricRenames.forward[name]; source {
|
||||
break
|
||||
}
|
||||
}
|
||||
return before, after, mentioned, nil
|
||||
}
|
||||
|
||||
// schemaRevision describes the ordered transformations that convert the
|
||||
// preceding schema version to version. Revisions without transformations are
|
||||
// omitted; allVersions retains them for predecessor lookup.
|
||||
type schemaRevision struct {
|
||||
version string
|
||||
changes []schemaChange
|
||||
}
|
||||
|
||||
type schemaChange struct {
|
||||
metricRenames *directedRenames
|
||||
attributeRenames *attributeRenameStep
|
||||
}
|
||||
|
||||
// directedRenames preserves the direction declared by the schema. reverse may
|
||||
// contain several predecessors because real schemas contain many-to-one metric
|
||||
// and attribute renames.
|
||||
type directedRenames struct {
|
||||
forward map[string]string
|
||||
reverse map[string][]string
|
||||
}
|
||||
|
||||
func newDirectedRenames(forward map[string]string) *directedRenames {
|
||||
if len(forward) == 0 {
|
||||
return nil
|
||||
}
|
||||
r := &directedRenames{
|
||||
forward: make(map[string]string, len(forward)),
|
||||
reverse: make(map[string][]string),
|
||||
}
|
||||
for oldName, newName := range forward {
|
||||
r.forward[oldName] = newName
|
||||
r.reverse[newName] = append(r.reverse[newName], oldName)
|
||||
}
|
||||
for newName := range r.reverse {
|
||||
slices.Sort(r.reverse[newName])
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type attributeRenameStep struct {
|
||||
renames *directedRenames
|
||||
applyToMetrics map[string]struct{}
|
||||
scopeSpecified bool
|
||||
}
|
||||
|
||||
func newAttributeRenameStep(rename *otelRenameAttributes, scoped bool) *attributeRenameStep {
|
||||
if rename == nil || len(rename.AttributeMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
step := &attributeRenameStep{renames: newDirectedRenames(rename.AttributeMap)}
|
||||
if scoped && rename.ApplyToMetrics != nil {
|
||||
step.scopeSpecified = true
|
||||
step.applyToMetrics = make(map[string]struct{}, len(*rename.ApplyToMetrics))
|
||||
for _, name := range *rename.ApplyToMetrics {
|
||||
step.applyToMetrics[name] = struct{}{}
|
||||
}
|
||||
}
|
||||
return step
|
||||
}
|
||||
|
||||
func (s *attributeRenameStep) appliesTo(metricName string) bool {
|
||||
if !s.scopeSpecified {
|
||||
return true
|
||||
}
|
||||
_, ok := s.applyToMetrics[metricName]
|
||||
return ok
|
||||
}
|
||||
|
||||
// collectSchemaRevision preserves the transformation order defined by the
|
||||
// schema format: all-section changes precede metric-section changes, and each
|
||||
// section is processed top-to-bottom.
|
||||
func collectSchemaRevision(versionStr string, version otelSchemaVersion) (*schemaRevision, error) {
|
||||
revision := &schemaRevision{version: versionStr}
|
||||
if version.All != nil {
|
||||
for i, change := range version.All.Changes {
|
||||
transformation, err := validateSchemaChange(versionStr, "all", i+1, change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if transformation != "rename_attributes" {
|
||||
return nil, fmt.Errorf("schema version %q all change %d contains unsupported transformation %q", versionStr, i+1, transformation)
|
||||
}
|
||||
if err := validateRenameAttributes(versionStr, "all", i+1, change.RenameAttributes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if step := newAttributeRenameStep(change.RenameAttributes, false); step != nil {
|
||||
revision.changes = append(revision.changes, schemaChange{attributeRenames: step})
|
||||
}
|
||||
}
|
||||
}
|
||||
if version.Metrics != nil {
|
||||
for i, change := range version.Metrics.Changes {
|
||||
transformation, err := validateSchemaChange(versionStr, "metrics", i+1, change)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch transformation {
|
||||
case "split":
|
||||
return nil, fmt.Errorf("schema version %q contains unsupported metric split transformation", versionStr)
|
||||
case "rename_attributes":
|
||||
if err := validateRenameAttributes(versionStr, "metrics", i+1, change.RenameAttributes); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if step := newAttributeRenameStep(change.RenameAttributes, true); step != nil {
|
||||
revision.changes = append(revision.changes, schemaChange{attributeRenames: step})
|
||||
}
|
||||
case "rename_metrics":
|
||||
if renames := newDirectedRenames(change.RenameMetrics); renames != nil {
|
||||
revision.changes = append(revision.changes, schemaChange{metricRenames: renames})
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("schema version %q metrics change %d contains unsupported transformation %q", versionStr, i+1, transformation)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(revision.changes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return revision, nil
|
||||
}
|
||||
|
||||
func validateSchemaChange(versionStr, section string, index int, change otelSchemaChange) (string, error) {
|
||||
if len(change.transformationKeys) != 1 {
|
||||
return "", fmt.Errorf("schema version %q %s change %d must contain exactly one transformation, found %q", versionStr, section, index, change.transformationKeys)
|
||||
}
|
||||
return change.transformationKeys[0], nil
|
||||
}
|
||||
|
||||
func validateRenameAttributes(versionStr, section string, index int, rename *otelRenameAttributes) error {
|
||||
if rename == nil {
|
||||
return nil
|
||||
}
|
||||
for _, field := range rename.fieldKeys {
|
||||
if field == "attribute_map" || section == "metrics" && field == "apply_to_metrics" {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("schema version %q %s change %d rename_attributes contains unsupported field %q", versionStr, section, index, field)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// semconvGroup represents a semantic conventions group definition.
|
||||
type semconvGroup struct {
|
||||
ID string `yaml:"id"`
|
||||
Type string `yaml:"type"` // "metric", "attribute", "span", etc.
|
||||
Extends string `yaml:"extends"`
|
||||
Prefix string `yaml:"prefix"`
|
||||
Stability string `yaml:"stability"`
|
||||
MetricName string `yaml:"metric_name"` // Only for type="metric"
|
||||
Unit string `yaml:"unit"` // Only for type="metric", e.g. "s", "By"
|
||||
Instrument string `yaml:"instrument"` // Only for type="metric", e.g. "histogram", "counter"
|
||||
Attributes []semconvAttribute `yaml:"attributes,omitempty"`
|
||||
}
|
||||
|
||||
type semconvAttribute struct {
|
||||
ID string `yaml:"id"`
|
||||
Ref string `yaml:"ref"`
|
||||
}
|
||||
|
||||
const (
|
||||
// maxSemconvFileAttributeSlots bounds the cumulative capacity of flattened
|
||||
// attribute slices materialized while loading one semconv file.
|
||||
maxSemconvFileAttributeSlots uint64 = maxSchemaExpansionWork
|
||||
|
||||
// maxSemconvRegistryAttributeSlots bounds that capacity across every semconv
|
||||
// file in an operator-provided registry.
|
||||
maxSemconvRegistryAttributeSlots uint64 = 1 << 20
|
||||
)
|
||||
|
||||
type semconvRegistryMaterializationBudget struct {
|
||||
attributeSlots uint64
|
||||
}
|
||||
|
||||
type semconvMaterializationBudget struct {
|
||||
attributeSlots uint64
|
||||
registry *semconvRegistryMaterializationBudget
|
||||
}
|
||||
|
||||
func (b *semconvMaterializationBudget) reserveAttributeSlots(n int) error {
|
||||
slots := uint64(n)
|
||||
if b.attributeSlots > maxSemconvFileAttributeSlots || slots > maxSemconvFileAttributeSlots-b.attributeSlots {
|
||||
return schemaExpansionLimitError("semconv file attribute slots", maxSemconvFileAttributeSlots)
|
||||
}
|
||||
if b.registry != nil && (b.registry.attributeSlots > maxSemconvRegistryAttributeSlots || slots > maxSemconvRegistryAttributeSlots-b.registry.attributeSlots) {
|
||||
return schemaExpansionLimitError("semconv registry attribute slots", maxSemconvRegistryAttributeSlots)
|
||||
}
|
||||
b.attributeSlots += slots
|
||||
if b.registry != nil {
|
||||
b.registry.attributeSlots += slots
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type semconvAttributeResolver struct {
|
||||
groups map[string]*semconvGroup
|
||||
resolved map[string]resolvedSemconvGroup
|
||||
resolving map[string]int
|
||||
stack []string
|
||||
budget *semconvMaterializationBudget
|
||||
}
|
||||
|
||||
type resolvedSemconvGroup struct {
|
||||
attributes []string
|
||||
}
|
||||
|
||||
func newSemconvAttributeResolver(groups []semconvGroup, registryBudget *semconvRegistryMaterializationBudget) (*semconvAttributeResolver, error) {
|
||||
byID := make(map[string]*semconvGroup, len(groups))
|
||||
for i := range groups {
|
||||
if groups[i].ID == "" {
|
||||
continue
|
||||
}
|
||||
if _, exists := byID[groups[i].ID]; exists {
|
||||
return nil, fmt.Errorf("duplicate semconv group id %q", groups[i].ID)
|
||||
}
|
||||
byID[groups[i].ID] = &groups[i]
|
||||
}
|
||||
return &semconvAttributeResolver{
|
||||
groups: byID,
|
||||
resolved: map[string]resolvedSemconvGroup{},
|
||||
resolving: map[string]int{},
|
||||
budget: &semconvMaterializationBudget{registry: registryBudget},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// resolve returns a group's deduplicated attributes, following extends
|
||||
// transitively. Full transitivity follows the language contract even though
|
||||
// historical build tools sometimes made schemas re-list grandparent references.
|
||||
func (r *semconvAttributeResolver) resolve(group *semconvGroup) (resolvedSemconvGroup, error) {
|
||||
if group.ID != "" {
|
||||
if resolved, ok := r.resolved[group.ID]; ok {
|
||||
return resolved, nil
|
||||
}
|
||||
if start, ok := r.resolving[group.ID]; ok {
|
||||
cycle := append(slices.Clone(r.stack[start:]), group.ID)
|
||||
return resolvedSemconvGroup{}, fmt.Errorf("semconv group inheritance cycle: %s", strings.Join(cycle, " -> "))
|
||||
}
|
||||
r.resolving[group.ID] = len(r.stack)
|
||||
r.stack = append(r.stack, group.ID)
|
||||
defer func() {
|
||||
delete(r.resolving, group.ID)
|
||||
r.stack = r.stack[:len(r.stack)-1]
|
||||
}()
|
||||
}
|
||||
|
||||
var inherited resolvedSemconvGroup
|
||||
if group.Extends != "" {
|
||||
parent, ok := r.groups[group.Extends]
|
||||
if !ok {
|
||||
return resolvedSemconvGroup{}, fmt.Errorf("semconv group %q extends unknown group %q", semconvGroupName(group), group.Extends)
|
||||
}
|
||||
var err error
|
||||
inherited, err = r.resolve(parent)
|
||||
if err != nil {
|
||||
return resolvedSemconvGroup{}, err
|
||||
}
|
||||
}
|
||||
|
||||
capacity := min(len(inherited.attributes)+len(group.Attributes), maxSchemaExpansion)
|
||||
if err := r.budget.reserveAttributeSlots(capacity); err != nil {
|
||||
return resolvedSemconvGroup{}, err
|
||||
}
|
||||
attributes := make([]string, 0, capacity)
|
||||
seen := make(map[string]struct{}, capacity)
|
||||
add := func(name string) error {
|
||||
if _, exists := seen[name]; exists {
|
||||
return nil
|
||||
}
|
||||
if len(attributes) >= maxSchemaExpansion {
|
||||
return schemaExpansionError("semconv group attributes")
|
||||
}
|
||||
seen[name] = struct{}{}
|
||||
attributes = append(attributes, name)
|
||||
return nil
|
||||
}
|
||||
for _, name := range inherited.attributes {
|
||||
if err := add(name); err != nil {
|
||||
return resolvedSemconvGroup{}, err
|
||||
}
|
||||
}
|
||||
for _, attribute := range group.Attributes {
|
||||
name, err := semconvAttributeName(*group, group.Prefix, attribute)
|
||||
if err != nil {
|
||||
return resolvedSemconvGroup{}, err
|
||||
}
|
||||
if err := add(name); err != nil {
|
||||
return resolvedSemconvGroup{}, err
|
||||
}
|
||||
}
|
||||
|
||||
resolved := resolvedSemconvGroup{attributes: attributes}
|
||||
if group.ID != "" {
|
||||
r.resolved[group.ID] = resolved
|
||||
}
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
func semconvGroupName(group *semconvGroup) string {
|
||||
if group.ID != "" {
|
||||
return group.ID
|
||||
}
|
||||
if group.MetricName != "" {
|
||||
return group.MetricName
|
||||
}
|
||||
return "(unnamed)"
|
||||
}
|
||||
|
||||
func semconvAttributeName(group semconvGroup, prefix string, attribute semconvAttribute) (string, error) {
|
||||
switch {
|
||||
case attribute.ID != "" && attribute.Ref != "":
|
||||
return "", fmt.Errorf("semconv group %q attribute declares both id %q and ref %q", semconvGroupName(&group), attribute.ID, attribute.Ref)
|
||||
case attribute.Ref != "":
|
||||
return attribute.Ref, nil
|
||||
case attribute.ID != "":
|
||||
if prefix == "" {
|
||||
return attribute.ID, nil
|
||||
}
|
||||
return prefix + "." + attribute.ID, nil
|
||||
default:
|
||||
return "", fmt.Errorf("semconv group %q attribute declares neither id nor ref", semconvGroupName(&group))
|
||||
}
|
||||
}
|
||||
|
||||
type otelSchemaVersion struct {
|
||||
All *otelSchemaSection `yaml:"all,omitempty"`
|
||||
Metrics *otelSchemaSection `yaml:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
type otelSchemaSection struct {
|
||||
Changes []otelSchemaChange `yaml:"changes,omitempty"`
|
||||
}
|
||||
|
||||
type otelSchemaChange struct {
|
||||
RenameAttributes *otelRenameAttributes `yaml:"rename_attributes,omitempty"`
|
||||
|
||||
// RenameMetrics maps each old metric name to its new name directly, with no
|
||||
// intervening key. This is asymmetric with RenameAttributes, which nests its
|
||||
// mapping under attribute_map, but it is what the file format specifies:
|
||||
//
|
||||
// metrics:
|
||||
// changes:
|
||||
// - rename_metrics:
|
||||
// http.server.duration: http.server.request.duration
|
||||
//
|
||||
// See the rename_metrics transformation in
|
||||
// https://opentelemetry.io/docs/specs/otel/schemas/file_format_v1.1.0/.
|
||||
RenameMetrics map[string]string `yaml:"rename_metrics,omitempty"`
|
||||
|
||||
// transformationKeys contains the sorted effective mapping keys, including
|
||||
// keys introduced by YAML merges.
|
||||
transformationKeys []string
|
||||
}
|
||||
|
||||
type otelRenameAttributes struct {
|
||||
AttributeMap map[string]string `yaml:"attribute_map,omitempty"`
|
||||
ApplyToMetrics *[]string `yaml:"apply_to_metrics,omitempty"`
|
||||
|
||||
// fieldKeys contains the sorted effective mapping keys, including keys
|
||||
// introduced by YAML merges.
|
||||
fieldKeys []string
|
||||
}
|
||||
|
||||
func (c *otelSchemaChange) UnmarshalYAML(value *yaml.Node) error {
|
||||
fields, keys, err := decodeEffectiveYAMLMapping(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.transformationKeys = keys
|
||||
if len(keys) != 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch keys[0] {
|
||||
case "rename_attributes":
|
||||
field := fields[keys[0]]
|
||||
return field.Decode(&c.RenameAttributes)
|
||||
case "rename_metrics":
|
||||
field := fields[keys[0]]
|
||||
return field.Decode(&c.RenameMetrics)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *otelRenameAttributes) UnmarshalYAML(value *yaml.Node) error {
|
||||
fields, keys, err := decodeEffectiveYAMLMapping(value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
r.fieldKeys = keys
|
||||
if field, ok := fields["attribute_map"]; ok {
|
||||
if err := field.Decode(&r.AttributeMap); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if field, ok := fields["apply_to_metrics"]; ok {
|
||||
if err := field.Decode(&r.ApplyToMetrics); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func decodeEffectiveYAMLMapping(value *yaml.Node) (map[string]yaml.Node, []string, error) {
|
||||
var fields map[string]yaml.Node
|
||||
if err := value.Decode(&fields); err != nil {
|
||||
@@ -680,14 +46,9 @@ func decodeEffectiveYAMLMapping(value *yaml.Node) (map[string]yaml.Node, []strin
|
||||
}
|
||||
|
||||
// staticCache is a generic, goroutine-safe cache keyed by URL for static
|
||||
// (immutable, bounded) values. Entries never expire and are not evicted:
|
||||
// the resolver only accepts paths inside the embedded registry, whose
|
||||
// contents are bounded and immutable, so TTL- and size-based eviction would
|
||||
// only add complexity for no benefit. Two callers racing on a cold key may
|
||||
// both invoke the loader and Store the same value; the result is identical
|
||||
// either way.
|
||||
// (immutable, bounded) values.
|
||||
type staticCache[T any] struct {
|
||||
m sync.Map // url → T
|
||||
m sync.Map // url -> T
|
||||
}
|
||||
|
||||
func newStaticCache[T any]() *staticCache[T] {
|
||||
@@ -721,153 +82,29 @@ func (e *schemaEngine) fetchOTelSchema(url string) (otelSchema, error) {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// loadOTelSchema parses raw OTel schema YAML bytes, validates its versions, and
|
||||
// collects its ordered transformations. It is the parsing core of
|
||||
// fetchOTelSchema and is directly callable from tests.
|
||||
// loadOTelSchema parses raw OTel transform/2.0 schema YAML bytes.
|
||||
func loadOTelSchema(b []byte) (otelSchema, error) {
|
||||
var s otelSchema
|
||||
if err := yaml.Unmarshal(b, &s); err != nil {
|
||||
return otelSchema{}, fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
if s.FileFormat == transformFileFormat {
|
||||
tf, err := loadTransformFile(b)
|
||||
if err != nil {
|
||||
return otelSchema{}, err
|
||||
}
|
||||
s.transform = tf
|
||||
if s.SchemaURL == "" {
|
||||
s.SchemaURL = tf.targetURL
|
||||
}
|
||||
return s, nil
|
||||
if s.FileFormat != transformFileFormat {
|
||||
return otelSchema{}, fmt.Errorf("unsupported OTel schema file format %q (expected %s)", s.FileFormat, transformFileFormat)
|
||||
}
|
||||
if s.FileFormat != "1.1.0" && s.FileFormat != "1.0.0" {
|
||||
return otelSchema{}, fmt.Errorf("unsupported OTel schema file format %q (expected 1.0.0, 1.1.0, or transform/2.0)", s.FileFormat)
|
||||
}
|
||||
|
||||
s.revisions = make([]schemaRevision, 0, len(s.Versions))
|
||||
s.allVersions = make([]string, 0, len(s.Versions))
|
||||
|
||||
for versionStr, version := range s.Versions {
|
||||
if err := validateSemver(versionStr); err != nil {
|
||||
return otelSchema{}, err
|
||||
}
|
||||
s.allVersions = append(s.allVersions, versionStr)
|
||||
revision, err := collectSchemaRevision(versionStr, version)
|
||||
if err != nil {
|
||||
return otelSchema{}, err
|
||||
}
|
||||
if revision != nil {
|
||||
s.revisions = append(s.revisions, *revision)
|
||||
}
|
||||
}
|
||||
|
||||
slices.SortFunc(s.revisions, func(a, b schemaRevision) int {
|
||||
return compareSemver(a.version, b.version)
|
||||
})
|
||||
slices.SortFunc(s.allVersions, compareSemver)
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// compareSemver compares two MAJOR.MINOR.PATCH version strings.
|
||||
// Returns -1 if a < b, 0 if a == b, 1 if a > b. Both inputs must have
|
||||
// already passed validateSemver; otherwise behaviour is undefined.
|
||||
func compareSemver(a, b string) int {
|
||||
partsA := strings.Split(a, ".")
|
||||
partsB := strings.Split(b, ".")
|
||||
for i := range partsA {
|
||||
numA, _ := strconv.Atoi(partsA[i])
|
||||
numB, _ := strconv.Atoi(partsB[i])
|
||||
if numA != numB {
|
||||
if numA < numB {
|
||||
return -1
|
||||
}
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// fetchSemconv reads a semconv file from the engine's registry source and parses
|
||||
// it. The version is derived from the last path segment of url; the URL must
|
||||
// satisfy registryURLRe and the derived version must satisfy validateSemver.
|
||||
func (e *schemaEngine) fetchSemconv(url string) (semconv, error) {
|
||||
b, err := e.readRegistryFile(url)
|
||||
tf, err := loadTransformFile(b)
|
||||
if err != nil {
|
||||
return semconv{}, fmt.Errorf("fetch semconv %q: %w", url, err)
|
||||
return otelSchema{}, err
|
||||
}
|
||||
_, version := path.Split(url)
|
||||
s, err := loadSemconv(b, version)
|
||||
if err != nil {
|
||||
return semconv{}, fmt.Errorf("parse semconv %q: %w", url, err)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// loadSemconv parses raw semconv YAML bytes and post-processes them. The
|
||||
// version is supplied by the caller (semconv files do not record their own
|
||||
// version inside the YAML) and must satisfy validateSemver.
|
||||
func loadSemconv(b []byte, version string) (semconv, error) {
|
||||
return loadSemconvWithBudget(b, version, nil)
|
||||
}
|
||||
|
||||
func loadSemconvWithBudget(b []byte, version string, registryBudget *semconvRegistryMaterializationBudget) (semconv, error) {
|
||||
if err := validateSemver(version); err != nil {
|
||||
return semconv{}, err
|
||||
}
|
||||
var s semconv
|
||||
if err := yaml.Unmarshal(b, &s); err != nil {
|
||||
return semconv{}, fmt.Errorf("unmarshal: %w", err)
|
||||
}
|
||||
s.version = version
|
||||
s.metrics = make(map[string]metricDef)
|
||||
resolver, err := newSemconvAttributeResolver(s.Groups, registryBudget)
|
||||
if err != nil {
|
||||
return semconv{}, err
|
||||
}
|
||||
resolvedGroups := make([]resolvedSemconvGroup, len(s.Groups))
|
||||
for i := range s.Groups {
|
||||
group := &s.Groups[i]
|
||||
resolved, err := resolver.resolve(group)
|
||||
if err != nil {
|
||||
return semconv{}, fmt.Errorf("resolve attributes for semconv group %q: %w", semconvGroupName(group), err)
|
||||
}
|
||||
resolvedGroups[i] = resolved
|
||||
}
|
||||
// A metric name declared by two groups has no single definition. Keeping the
|
||||
// first declaration rather than the last makes the outcome depend on file
|
||||
// order in one direction only, but either choice is arbitrary, so the name is
|
||||
// also recorded as ambiguous and reported to the querier as a warning.
|
||||
var ambiguous map[string]struct{}
|
||||
for i := range s.Groups {
|
||||
group := &s.Groups[i]
|
||||
if group.Type != "metric" || group.MetricName == "" {
|
||||
continue
|
||||
}
|
||||
if _, dup := s.metrics[group.MetricName]; dup {
|
||||
if ambiguous == nil {
|
||||
ambiguous = map[string]struct{}{}
|
||||
}
|
||||
ambiguous[group.MetricName] = struct{}{}
|
||||
continue
|
||||
}
|
||||
s.metrics[group.MetricName] = metricDef{
|
||||
unit: group.Unit,
|
||||
instrument: group.Instrument,
|
||||
stability: group.Stability,
|
||||
attributes: resolvedGroups[i].attributes,
|
||||
}
|
||||
}
|
||||
if len(ambiguous) > 0 {
|
||||
s.ambiguousMetrics = slices.Sorted(maps.Keys(ambiguous))
|
||||
s.transform = tf
|
||||
if s.SchemaURL == "" {
|
||||
s.SchemaURL = tf.targetURL
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// readRegistryFile reads a file from the engine's registry source. The path must
|
||||
// satisfy registryURLRe — HTTP URLs and arbitrary local files are rejected to
|
||||
// keep the __semconv_url__/__schema_url__ matchers from acting as a server-side
|
||||
// fetch primitive. The gate applies to every source, embedded or operator-provided.
|
||||
// keep the __schema_url__ matcher from acting as a server-side fetch primitive.
|
||||
func (e *schemaEngine) readRegistryFile(url string) ([]byte, error) {
|
||||
if !registryURLRe.MatchString(url) {
|
||||
return nil, fmt.Errorf("invalid registry URL %q: only registry paths (registry/<name>) are accepted", url)
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -33,7 +33,7 @@ import (
|
||||
// matchers, label_replace / label_join wrappers, and sample value filters so the
|
||||
// resulting query can be executed against an unmodified Prometheus engine.
|
||||
func RewritePromQLQuery(query, defaultSchemaURL string) (string, annotations.Annotations, error) {
|
||||
return rewritePromQLWithEngine(newSchemaEngine(embeddedRegistry), query, defaultSchemaURL)
|
||||
return rewritePromQLWithEngine(newSchemaEngine(newRegistrySource(nil)), query, defaultSchemaURL)
|
||||
}
|
||||
|
||||
// RewritePromQLQueryWithRegistry is RewritePromQLQuery backed by an operator-provided
|
||||
|
||||
@@ -85,26 +85,12 @@ func newRegistrySource(files map[string][]byte) registrySource {
|
||||
// listRegistryYAMLFiles returns the sorted registry/<name>.yaml paths available
|
||||
// in src so transform/2.0 policies can be discovered by target schema_url.
|
||||
func listRegistryYAMLFiles(src registrySource) []string {
|
||||
switch s := src.(type) {
|
||||
case fs.ReadDirFS:
|
||||
entries, err := s.ReadDir(registryPrefix)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var names []string
|
||||
for _, e := range entries {
|
||||
if !e.IsDir() && strings.HasSuffix(e.Name(), ".yaml") {
|
||||
names = append(names, registryPrefix+"/"+e.Name())
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
return names
|
||||
case prefixedRegistry:
|
||||
if rf, ok := s.inner.(registryFiles); ok {
|
||||
if p, ok := src.(prefixedRegistry); ok {
|
||||
if rf, ok := p.inner.(registryFiles); ok {
|
||||
var names []string
|
||||
for k := range rf {
|
||||
if strings.HasSuffix(k, ".yaml") {
|
||||
names = append(names, s.prefix+"/"+k)
|
||||
names = append(names, p.prefix+"/"+k)
|
||||
}
|
||||
}
|
||||
slices.Sort(names)
|
||||
@@ -123,14 +109,7 @@ func cloneRegistryFiles(files map[string][]byte) map[string][]byte {
|
||||
}
|
||||
|
||||
// validateRegistryFiles reports whether files form a usable registry: a non-empty
|
||||
// set in which every semver-named file (e.g. "1.0.0") parses as a semconv file,
|
||||
// every other file (e.g. "registry.yaml") parses as an OTel schema, and at least
|
||||
// one OTel schema is present. Semconv materialization is bounded cumulatively
|
||||
// across the registry. It rejects invalid input at startup instead of letting it
|
||||
// fail only at query time. Only versions queried as __semconv_url__ anchors
|
||||
// require a semconv file; a missing sibling version disables corroboration for
|
||||
// that boundary and produces a query warning. It reuses loadSemconv/loadOTelSchema
|
||||
// so validation matches what queries rely on.
|
||||
// set in which every file parses as a valid transform/2.0 schema.
|
||||
func validateRegistryFiles(files map[string][]byte) error {
|
||||
if len(files) == 0 {
|
||||
return errors.New("registry is empty")
|
||||
@@ -140,23 +119,10 @@ func validateRegistryFiles(files map[string][]byte) error {
|
||||
names = append(names, name)
|
||||
}
|
||||
slices.Sort(names)
|
||||
registryBudget := &semconvRegistryMaterializationBudget{}
|
||||
hasSchema := false
|
||||
for _, name := range names {
|
||||
b := files[name]
|
||||
if semverRe.MatchString(name) {
|
||||
if _, err := loadSemconvWithBudget(b, name, registryBudget); err != nil {
|
||||
return fmt.Errorf("registry semconv %q: %w", name, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if _, err := loadOTelSchema(b); err != nil {
|
||||
if _, err := loadOTelSchema(files[name]); err != nil {
|
||||
return fmt.Errorf("registry schema %q: %w", name, err)
|
||||
}
|
||||
hasSchema = true
|
||||
}
|
||||
if !hasSchema {
|
||||
return errors.New("registry has no OTel schema file")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
groups:
|
||||
- id: registry.http
|
||||
type: attribute_group
|
||||
brief: "HTTP attributes"
|
||||
attributes:
|
||||
- id: http.response.status_code
|
||||
type: int
|
||||
stability: stable
|
||||
brief: "HTTP response status code"
|
||||
examples: [200, 404, 500]
|
||||
- id: registry.user
|
||||
type: attribute_group
|
||||
brief: "User attributes"
|
||||
attributes:
|
||||
- id: user
|
||||
type: string
|
||||
stability: stable
|
||||
brief: "The username or user identifier"
|
||||
examples: ["alice", "bob"]
|
||||
- id: metric.test.counter
|
||||
type: metric
|
||||
brief: "Counts the number of iterations in the test application"
|
||||
stability: development
|
||||
metric_name: test.counter
|
||||
instrument: counter
|
||||
unit: By
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
requirement_level: recommended
|
||||
- ref: user
|
||||
@@ -1,30 +0,0 @@
|
||||
groups:
|
||||
- id: registry.http
|
||||
type: attribute_group
|
||||
brief: "HTTP attributes"
|
||||
attributes:
|
||||
- id: http.response.status_code
|
||||
type: int
|
||||
stability: stable
|
||||
brief: "HTTP response status code"
|
||||
examples: [200, 404, 500]
|
||||
- id: registry.tenant
|
||||
type: attribute_group
|
||||
brief: "Tenant attributes"
|
||||
attributes:
|
||||
- id: tenant
|
||||
type: string
|
||||
stability: stable
|
||||
brief: "The tenant identifier"
|
||||
examples: ["alice", "bob"]
|
||||
- id: metric.test
|
||||
type: metric
|
||||
brief: "Counts the number of iterations in the test application"
|
||||
stability: stable
|
||||
metric_name: test
|
||||
instrument: counter
|
||||
unit: By
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
requirement_level: recommended
|
||||
- ref: tenant
|
||||
@@ -1,30 +0,0 @@
|
||||
groups:
|
||||
- id: registry.http
|
||||
type: attribute_group
|
||||
brief: "HTTP attributes"
|
||||
attributes:
|
||||
- id: http.response.status_code
|
||||
type: int
|
||||
stability: stable
|
||||
brief: "HTTP response status code"
|
||||
examples: [200, 404, 500]
|
||||
- id: registry.tenant
|
||||
type: attribute_group
|
||||
brief: "Tenant attributes"
|
||||
attributes:
|
||||
- id: tenant
|
||||
type: string
|
||||
stability: stable
|
||||
brief: "The tenant identifier"
|
||||
examples: ["alice", "bob"]
|
||||
- id: metric.test
|
||||
type: metric
|
||||
brief: "Counts the number of iterations in the test application"
|
||||
stability: stable
|
||||
metric_name: test.v2
|
||||
instrument: counter
|
||||
unit: By
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
requirement_level: recommended
|
||||
- ref: tenant
|
||||
@@ -1,22 +0,0 @@
|
||||
file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
|
||||
versions:
|
||||
1.0.0:
|
||||
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
test.counter: test
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
user: tenant
|
||||
apply_to_metrics:
|
||||
- test
|
||||
|
||||
1.2.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
test: test.v2
|
||||
@@ -29,24 +29,23 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// embeddedRegistryFiles reads the embedded registry into a base-name → bytes
|
||||
// map, the shape an operator-provided registry takes. Each call returns a fresh
|
||||
// map so tests may mutate it.
|
||||
func embeddedRegistryFiles(t *testing.T) map[string][]byte {
|
||||
t.Helper()
|
||||
entries, err := embeddedRegistry.ReadDir(registryPrefix)
|
||||
require.NoError(t, err)
|
||||
files := map[string][]byte{}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
b, err := embeddedRegistry.ReadFile(registryPrefix + "/" + e.Name())
|
||||
require.NoError(t, err)
|
||||
files[e.Name()] = b
|
||||
func sampleRegistryFiles() map[string][]byte {
|
||||
return map[string][]byte{
|
||||
"policy.yaml": []byte(`file_format: transform/2.0
|
||||
source:
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
target:
|
||||
schema_url: https://example.com/schemas/2.0.0
|
||||
policies:
|
||||
- id: rename-test
|
||||
metric_transform:
|
||||
matches:
|
||||
- target: {descriptor_field: METRIC_DESCRIPTOR_FIELD_NAME}
|
||||
predicate: {equals: test.counter}
|
||||
add:
|
||||
- {target: {descriptor_field: METRIC_DESCRIPTOR_FIELD_NAME}, value: test, upsert: true}
|
||||
`),
|
||||
}
|
||||
require.NotEmpty(t, files)
|
||||
return files
|
||||
}
|
||||
|
||||
// tarGz builds a gzip-compressed tar of files (keyed by entry name) for tests.
|
||||
@@ -66,110 +65,46 @@ func tarGz(t *testing.T, files map[string][]byte) []byte {
|
||||
}
|
||||
|
||||
func TestNewRegistrySource(t *testing.T) {
|
||||
files := embeddedRegistryFiles(t)
|
||||
files := sampleRegistryFiles()
|
||||
e := newSchemaEngine(newRegistrySource(files))
|
||||
|
||||
t.Run("resolves a registry/<name> path", func(t *testing.T) {
|
||||
b, err := e.readRegistryFile("registry/1.0.0")
|
||||
b, err := e.readRegistryFile("registry/policy.yaml")
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, files["1.0.0"], b)
|
||||
})
|
||||
|
||||
t.Run("resolves the schema index", func(t *testing.T) {
|
||||
b, err := e.readRegistryFile("registry/registry.yaml")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, b)
|
||||
require.Equal(t, files["policy.yaml"], b)
|
||||
})
|
||||
|
||||
t.Run("rejects unprefixed, traversal, and remote paths", func(t *testing.T) {
|
||||
for _, bad := range []string{"1.0.0", "registry/..", "registry/../etc/passwd", "/etc/passwd", "http://example.com/x"} {
|
||||
for _, bad := range []string{"policy.yaml", "registry/..", "registry/../etc/passwd", "/etc/passwd", "http://example.com/x"} {
|
||||
_, err := e.readRegistryFile(bad)
|
||||
require.Errorf(t, err, "expected %q to be rejected", bad)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("reports a missing entry", func(t *testing.T) {
|
||||
_, err := e.readRegistryFile("registry/9.9.9")
|
||||
_, err := e.readRegistryFile("registry/missing.yaml")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateRegistryFiles(t *testing.T) {
|
||||
t.Run("accepts the embedded layout", func(t *testing.T) {
|
||||
require.NoError(t, validateRegistryFiles(embeddedRegistryFiles(t)))
|
||||
})
|
||||
|
||||
registryWithFanoutVersions := func(t *testing.T, versionCount int) map[string][]byte {
|
||||
t.Helper()
|
||||
exactFanoutChildren := int(maxSemconvFileAttributeSlots)/maxSchemaExpansion - 1
|
||||
semconvFile := inheritedAttributeFanoutSemconv(t, exactFanoutChildren)
|
||||
files := map[string][]byte{"registry.yaml": []byte("file_format: 1.1.0\n")}
|
||||
for i := range versionCount {
|
||||
files[fmt.Sprintf("1.%d.0", i)] = semconvFile
|
||||
}
|
||||
return files
|
||||
}
|
||||
|
||||
registryVersionsAtLimit := int(maxSemconvRegistryAttributeSlots / maxSemconvFileAttributeSlots)
|
||||
t.Run("accepts exact registry attribute slot limit", func(t *testing.T) {
|
||||
require.NoError(t, validateRegistryFiles(registryWithFanoutVersions(t, registryVersionsAtLimit)))
|
||||
})
|
||||
|
||||
t.Run("rejects registry attribute slots above limit", func(t *testing.T) {
|
||||
err := validateRegistryFiles(registryWithFanoutVersions(t, registryVersionsAtLimit+1))
|
||||
require.ErrorContains(t, err, "semconv registry attribute slots would exceed 1048576")
|
||||
t.Run("accepts a valid transform/2.0 registry", func(t *testing.T) {
|
||||
require.NoError(t, validateRegistryFiles(sampleRegistryFiles()))
|
||||
})
|
||||
|
||||
t.Run("rejects an empty registry", func(t *testing.T) {
|
||||
require.Error(t, validateRegistryFiles(nil))
|
||||
})
|
||||
|
||||
t.Run("rejects an unparseable schema", func(t *testing.T) {
|
||||
require.Error(t, validateRegistryFiles(map[string][]byte{"registry.yaml": []byte("file_format: 9.9.9\n")}))
|
||||
})
|
||||
|
||||
t.Run("rejects a registry with no schema", func(t *testing.T) {
|
||||
files := embeddedRegistryFiles(t)
|
||||
for name := range files {
|
||||
if !semverRe.MatchString(name) {
|
||||
delete(files, name)
|
||||
}
|
||||
}
|
||||
require.Error(t, validateRegistryFiles(files))
|
||||
})
|
||||
|
||||
t.Run("accepts a registry shipping a subset of referenced versions", func(t *testing.T) {
|
||||
// The schema references 1.0.0/1.1.0/1.2.0; shipping only some is valid —
|
||||
// operators provide the versions they query as anchors, not the whole history.
|
||||
files := embeddedRegistryFiles(t)
|
||||
delete(files, "1.2.0")
|
||||
require.NoError(t, validateRegistryFiles(files))
|
||||
})
|
||||
|
||||
t.Run("rejects an unparseable semconv version file", func(t *testing.T) {
|
||||
files := embeddedRegistryFiles(t)
|
||||
files["1.0.0"] = []byte("groups: [unterminated")
|
||||
require.Error(t, validateRegistryFiles(files))
|
||||
})
|
||||
|
||||
t.Run("validates files in name order", func(t *testing.T) {
|
||||
files := map[string][]byte{
|
||||
"registry.yaml": []byte("file_format: 1.1.0\n"),
|
||||
"2.0.0": []byte("groups: [second"),
|
||||
"1.0.0": []byte("groups: [first"),
|
||||
}
|
||||
err := validateRegistryFiles(files)
|
||||
require.ErrorContains(t, err, `registry semconv "1.0.0"`)
|
||||
t.Run("rejects an unparseable or unsupported schema format", func(t *testing.T) {
|
||||
require.Error(t, validateRegistryFiles(map[string][]byte{"policy.yaml": []byte("file_format: 9.9.9\n")}))
|
||||
require.Error(t, validateRegistryFiles(map[string][]byte{"policy.yaml": []byte("file_format: 1.1.0\n")}))
|
||||
})
|
||||
}
|
||||
|
||||
func TestUntargz(t *testing.T) {
|
||||
t.Run("flattens entry paths to base names", func(t *testing.T) {
|
||||
want := map[string][]byte{
|
||||
"registry.yaml": []byte("file_format: 1.1.0\n"),
|
||||
"1.0.0": []byte("groups: []\n"),
|
||||
}
|
||||
// Lay entries under a top-level directory to exercise flattening.
|
||||
want := sampleRegistryFiles()
|
||||
nested := map[string][]byte{}
|
||||
for name, b := range want {
|
||||
nested["registry/"+name] = b
|
||||
@@ -180,7 +115,7 @@ func TestUntargz(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("rejects a duplicate base name", func(t *testing.T) {
|
||||
archive := tarGz(t, map[string][]byte{"a/1.0.0": []byte("x"), "b/1.0.0": []byte("y")})
|
||||
archive := tarGz(t, map[string][]byte{"a/policy.yaml": []byte("x"), "b/policy.yaml": []byte("y")})
|
||||
_, err := untargz(bytes.NewReader(archive))
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "duplicate")
|
||||
@@ -202,7 +137,7 @@ func TestUntargz(t *testing.T) {
|
||||
tw := tar.NewWriter(gz)
|
||||
size := int64(maxRegistryDecompressedBytes) + (1 << 20)
|
||||
require.NoError(t, tw.WriteHeader(&tar.Header{Name: "big", Typeflag: tar.TypeReg, Mode: 0o644, Size: size}))
|
||||
chunk := make([]byte, 1<<20) // Zeros compress to almost nothing.
|
||||
chunk := make([]byte, 1<<20)
|
||||
for written := int64(0); written < size; {
|
||||
n := int64(len(chunk))
|
||||
if rem := size - written; rem < n {
|
||||
@@ -224,7 +159,7 @@ func TestUntargz(t *testing.T) {
|
||||
func TestLoadRegistryFiles(t *testing.T) {
|
||||
t.Run("reads matched files keyed by base name", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
want := embeddedRegistryFiles(t)
|
||||
want := sampleRegistryFiles()
|
||||
for name, b := range want {
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, name), b, 0o644))
|
||||
}
|
||||
@@ -240,8 +175,8 @@ func TestLoadRegistryFiles(t *testing.T) {
|
||||
|
||||
t.Run("rejects a duplicate base name across globs", func(t *testing.T) {
|
||||
dirA, dirB := t.TempDir(), t.TempDir()
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dirA, "1.0.0"), []byte("a"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dirB, "1.0.0"), []byte("b"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dirA, "policy.yaml"), []byte("a"), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dirB, "policy.yaml"), []byte("b"), 0o644))
|
||||
_, err := LoadRegistryFiles([]string{filepath.Join(dirA, "*"), filepath.Join(dirB, "*")})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "duplicate")
|
||||
@@ -249,7 +184,7 @@ func TestLoadRegistryFiles(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestFetchRegistry(t *testing.T) {
|
||||
want := embeddedRegistryFiles(t)
|
||||
want := sampleRegistryFiles()
|
||||
|
||||
t.Run("downloads and unpacks an archive", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
|
||||
@@ -64,7 +64,7 @@ func schemaWarning(msg string) error {
|
||||
// canonical naming. Queries without those matchers are passed through
|
||||
// unchanged.
|
||||
func AwareStorage(s storage.Storage) storage.Storage {
|
||||
return newAwareStorage(s, newSchemaEngine(embeddedRegistry))
|
||||
return newAwareStorage(s, newSchemaEngine(newRegistrySource(nil)))
|
||||
}
|
||||
|
||||
// AwareStorageWithRegistry behaves like AwareStorage but resolves __semconv_url__
|
||||
|
||||
@@ -1,879 +0,0 @@
|
||||
// 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 semconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/util/annotations"
|
||||
)
|
||||
|
||||
func TestStorageFanOutContracts(t *testing.T) {
|
||||
t.Run("canonical labels are valid", func(t *testing.T) {
|
||||
mapping := buildLabelMapping("metric.new", map[string]string{"old": "new"})
|
||||
got, err := transformOTelSchemaLabels(labels.FromStrings(
|
||||
model.MetricNameLabel, "metric.old",
|
||||
semconvURLLabel, "registry/1.0.0",
|
||||
"old", "same",
|
||||
"new", "same",
|
||||
), mapping)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, labels.FromStrings(model.MetricNameLabel, "metric.new", "new", "same"), got)
|
||||
})
|
||||
|
||||
t.Run("conflicting canonical labels fail", func(t *testing.T) {
|
||||
mapping := buildLabelMapping("metric.new", map[string]string{"old": "new"})
|
||||
_, err := transformOTelSchemaLabels(labels.FromStrings("old", "first", "new", "second"), mapping)
|
||||
require.ErrorContains(t, err, "conflicting values")
|
||||
})
|
||||
|
||||
t.Run("only attribute mappings require resorting", func(t *testing.T) {
|
||||
require.False(t, mappingNeedsResort(buildLabelMapping("metric.new", nil)))
|
||||
require.True(t, mappingNeedsResort(buildLabelMapping("metric.new", map[string]string{"old": "new"})))
|
||||
})
|
||||
|
||||
t.Run("identity mappings preserve underlying objects", func(t *testing.T) {
|
||||
mapping := buildLabelMapping("metric", nil)
|
||||
series := storage.NewListSeries(labels.FromStrings(model.MetricNameLabel, "metric", "instance", "a"), nil)
|
||||
seriesSet := &awareSeriesSet{
|
||||
SeriesSet: &singleSeriesSet{series: series},
|
||||
mapping: mapping,
|
||||
}
|
||||
require.True(t, seriesSet.Next())
|
||||
require.Same(t, series, seriesSet.At())
|
||||
|
||||
chunkSeries := storage.NewSeriesToChunkEncoder(series)
|
||||
chunkSet := &awareChunkSeriesSet{
|
||||
ChunkSeriesSet: &singleChunkSeriesSet{series: chunkSeries},
|
||||
mapping: mapping,
|
||||
}
|
||||
require.True(t, chunkSet.Next())
|
||||
require.Same(t, chunkSeries, chunkSet.At())
|
||||
})
|
||||
|
||||
t.Run("select hints are cloned", func(t *testing.T) {
|
||||
original := &storage.SelectHints{
|
||||
Start: 1,
|
||||
End: 2,
|
||||
Limit: 3,
|
||||
Grouping: []string{"group"},
|
||||
ProjectionLabels: []string{"project"},
|
||||
}
|
||||
cloned := cloneSelectHints(original)
|
||||
cloned.Limit = 0
|
||||
cloned.Grouping[0] = "mutated group"
|
||||
cloned.ProjectionLabels[0] = "mutated projection"
|
||||
require.Equal(t, 3, original.Limit)
|
||||
require.Equal(t, []string{"group"}, original.Grouping)
|
||||
require.Equal(t, []string{"project"}, original.ProjectionLabels)
|
||||
})
|
||||
|
||||
t.Run("canonical materialization is bounded", func(t *testing.T) {
|
||||
budget := canonicalSeriesBudget{kind: "series", limit: 2, remaining: 2}
|
||||
require.NoError(t, budget.take())
|
||||
require.NoError(t, budget.take())
|
||||
require.ErrorIs(t, budget.take(), errCanonicalSeriesMaterialization)
|
||||
})
|
||||
|
||||
t.Run("canonical post-filter skips nonmatches and charges input", func(t *testing.T) {
|
||||
matcher := labels.MustNewMatcher(labels.MatchEqual, "job", "keep")
|
||||
filtered := storage.NewListSeries(labels.FromStrings(model.MetricNameLabel, "metric", "job", "drop"), nil)
|
||||
matched := storage.NewListSeries(labels.FromStrings(model.MetricNameLabel, "metric", "job", "keep"), nil)
|
||||
|
||||
seriesBudget := newCanonicalSeriesBudget("series", 2)
|
||||
seriesSet := &awareSeriesSet{
|
||||
SeriesSet: &sliceSeriesSet{series: []storage.Series{filtered, matched}, index: -1},
|
||||
mapping: buildLabelMapping("metric", nil),
|
||||
canonicalMatchers: []*labels.Matcher{matcher},
|
||||
budget: seriesBudget,
|
||||
}
|
||||
require.True(t, seriesSet.Next())
|
||||
require.Same(t, matched, seriesSet.At())
|
||||
require.False(t, seriesSet.Next())
|
||||
require.NoError(t, seriesSet.Err())
|
||||
require.Zero(t, seriesBudget.remaining)
|
||||
|
||||
chunkBudget := newCanonicalSeriesBudget("chunks", 2)
|
||||
matchedChunk := storage.NewSeriesToChunkEncoder(matched)
|
||||
chunkSet := &awareChunkSeriesSet{
|
||||
ChunkSeriesSet: &sliceChunkSeriesSet{
|
||||
series: []storage.ChunkSeries{storage.NewSeriesToChunkEncoder(filtered), matchedChunk},
|
||||
index: -1,
|
||||
},
|
||||
mapping: buildLabelMapping("metric", nil),
|
||||
canonicalMatchers: []*labels.Matcher{matcher},
|
||||
budget: chunkBudget,
|
||||
}
|
||||
require.True(t, chunkSet.Next())
|
||||
require.Same(t, matchedChunk, chunkSet.At())
|
||||
require.False(t, chunkSet.Next())
|
||||
require.NoError(t, chunkSet.Err())
|
||||
require.Zero(t, chunkBudget.remaining)
|
||||
})
|
||||
|
||||
t.Run("label value fan-out is bounded", func(t *testing.T) {
|
||||
variants := []matcherVariant{
|
||||
{mapping: buildLabelMapping("metric", map[string]string{"old": "new"})},
|
||||
{mapping: buildLabelMapping("metric", nil)},
|
||||
}
|
||||
jobs, err := buildLabelValueJobs(variants, "new")
|
||||
require.NoError(t, err)
|
||||
require.Len(t, jobs, 3)
|
||||
|
||||
_, err = buildLabelValueJobsUpTo(variants, "new", 2)
|
||||
require.ErrorIs(t, err, errSchemaExpansion)
|
||||
})
|
||||
}
|
||||
|
||||
type singleSeriesSet struct {
|
||||
series storage.Series
|
||||
next bool
|
||||
}
|
||||
|
||||
func (s *singleSeriesSet) Next() bool {
|
||||
if s.next {
|
||||
return false
|
||||
}
|
||||
s.next = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *singleSeriesSet) At() storage.Series { return s.series }
|
||||
func (*singleSeriesSet) Err() error { return nil }
|
||||
func (*singleSeriesSet) Warnings() annotations.Annotations { return nil }
|
||||
|
||||
type singleChunkSeriesSet struct {
|
||||
series storage.ChunkSeries
|
||||
next bool
|
||||
}
|
||||
|
||||
func (s *singleChunkSeriesSet) Next() bool {
|
||||
if s.next {
|
||||
return false
|
||||
}
|
||||
s.next = true
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *singleChunkSeriesSet) At() storage.ChunkSeries { return s.series }
|
||||
func (*singleChunkSeriesSet) Err() error { return nil }
|
||||
func (*singleChunkSeriesSet) Warnings() annotations.Annotations { return nil }
|
||||
|
||||
type sliceSeriesSet struct {
|
||||
series []storage.Series
|
||||
index int
|
||||
}
|
||||
|
||||
func (s *sliceSeriesSet) Next() bool {
|
||||
s.index++
|
||||
return s.index < len(s.series)
|
||||
}
|
||||
|
||||
func (s *sliceSeriesSet) At() storage.Series { return s.series[s.index] }
|
||||
func (*sliceSeriesSet) Err() error { return nil }
|
||||
func (*sliceSeriesSet) Warnings() annotations.Annotations { return nil }
|
||||
|
||||
type sliceChunkSeriesSet struct {
|
||||
series []storage.ChunkSeries
|
||||
index int
|
||||
}
|
||||
|
||||
func (s *sliceChunkSeriesSet) Next() bool {
|
||||
s.index++
|
||||
return s.index < len(s.series)
|
||||
}
|
||||
|
||||
func (s *sliceChunkSeriesSet) At() storage.ChunkSeries { return s.series[s.index] }
|
||||
func (*sliceChunkSeriesSet) Err() error { return nil }
|
||||
func (*sliceChunkSeriesSet) Warnings() annotations.Annotations { return nil }
|
||||
|
||||
type resolverBudgetCallCounts struct {
|
||||
series int
|
||||
chunk int
|
||||
labelNames int
|
||||
labelValues int
|
||||
}
|
||||
|
||||
type optionalSearchStorage struct {
|
||||
storage.Storage
|
||||
|
||||
querier storage.Querier
|
||||
chunkQuerier storage.ChunkQuerier
|
||||
}
|
||||
|
||||
func (s *optionalSearchStorage) Querier(int64, int64) (storage.Querier, error) {
|
||||
return s.querier, nil
|
||||
}
|
||||
|
||||
func (s *optionalSearchStorage) ChunkQuerier(int64, int64) (storage.ChunkQuerier, error) {
|
||||
return s.chunkQuerier, nil
|
||||
}
|
||||
|
||||
type nonSearchQuerier struct {
|
||||
storage.Querier
|
||||
closes *int
|
||||
}
|
||||
|
||||
func (q *nonSearchQuerier) Close() error {
|
||||
*q.closes++
|
||||
return nil
|
||||
}
|
||||
|
||||
type nonSearchChunkQuerier struct {
|
||||
storage.ChunkQuerier
|
||||
closes *int
|
||||
}
|
||||
|
||||
func (q *nonSearchChunkQuerier) Close() error {
|
||||
*q.closes++
|
||||
return nil
|
||||
}
|
||||
|
||||
type searchCall struct {
|
||||
kind string
|
||||
ctx context.Context
|
||||
name string
|
||||
hints *storage.SearchHints
|
||||
matchers []*labels.Matcher
|
||||
}
|
||||
|
||||
type searchRecorder struct {
|
||||
calls []searchCall
|
||||
result storage.SearchResultSet
|
||||
}
|
||||
|
||||
func (r *searchRecorder) labelNames(ctx context.Context, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
r.calls = append(r.calls, searchCall{kind: "names", ctx: ctx, hints: hints, matchers: matchers})
|
||||
return r.result
|
||||
}
|
||||
|
||||
func (r *searchRecorder) labelValues(ctx context.Context, name string, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
r.calls = append(r.calls, searchCall{kind: "values", ctx: ctx, name: name, hints: hints, matchers: matchers})
|
||||
return r.result
|
||||
}
|
||||
|
||||
type searchableQuerier struct {
|
||||
*nonSearchQuerier
|
||||
recorder *searchRecorder
|
||||
}
|
||||
|
||||
func (q *searchableQuerier) SearchLabelNames(ctx context.Context, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
return q.recorder.labelNames(ctx, hints, matchers...)
|
||||
}
|
||||
|
||||
func (q *searchableQuerier) SearchLabelValues(ctx context.Context, name string, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
return q.recorder.labelValues(ctx, name, hints, matchers...)
|
||||
}
|
||||
|
||||
type searchableChunkQuerier struct {
|
||||
*nonSearchChunkQuerier
|
||||
recorder *searchRecorder
|
||||
}
|
||||
|
||||
func (q *searchableChunkQuerier) SearchLabelNames(ctx context.Context, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
return q.recorder.labelNames(ctx, hints, matchers...)
|
||||
}
|
||||
|
||||
func (q *searchableChunkQuerier) SearchLabelValues(ctx context.Context, name string, hints *storage.SearchHints, matchers ...*labels.Matcher) storage.SearchResultSet {
|
||||
return q.recorder.labelValues(ctx, name, hints, matchers...)
|
||||
}
|
||||
|
||||
type recordingSearchResultSet struct {
|
||||
closes int
|
||||
}
|
||||
|
||||
func (*recordingSearchResultSet) Next() bool { return false }
|
||||
func (*recordingSearchResultSet) At() storage.SearchResult { return storage.SearchResult{} }
|
||||
func (*recordingSearchResultSet) Warnings() annotations.Annotations { return nil }
|
||||
func (*recordingSearchResultSet) Err() error { return nil }
|
||||
|
||||
func (s *recordingSearchResultSet) Close() error {
|
||||
s.closes++
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAwareStoragePreservesOptionalSearcher(t *testing.T) {
|
||||
assertDelegates := func(t *testing.T, searcher storage.Searcher, recorder *searchRecorder) {
|
||||
t.Helper()
|
||||
type contextKey struct{}
|
||||
ctx := context.WithValue(t.Context(), contextKey{}, "sentinel")
|
||||
hints := &storage.SearchHints{Limit: 7}
|
||||
matchers := []*labels.Matcher{labels.MustNewMatcher(labels.MatchEqual, "job", "api")}
|
||||
|
||||
result := searcher.SearchLabelNames(ctx, hints, matchers...)
|
||||
require.Same(t, recorder.result, result)
|
||||
require.NoError(t, result.Close())
|
||||
result = searcher.SearchLabelValues(ctx, "instance", hints, matchers...)
|
||||
require.Same(t, recorder.result, result)
|
||||
require.NoError(t, result.Close())
|
||||
|
||||
require.Equal(t, []searchCall{
|
||||
{kind: "names", ctx: ctx, hints: hints, matchers: matchers},
|
||||
{kind: "values", ctx: ctx, name: "instance", hints: hints, matchers: matchers},
|
||||
}, recorder.calls)
|
||||
}
|
||||
assertRejectsReserved := func(t *testing.T, searcher storage.Searcher, recorder *searchRecorder) {
|
||||
t.Helper()
|
||||
calls := len(recorder.calls)
|
||||
for _, reserved := range []string{semconvURLLabel, schemaURLLabel} {
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, "job", "api"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, reserved, "registry/test"),
|
||||
}
|
||||
for _, result := range []storage.SearchResultSet{
|
||||
searcher.SearchLabelNames(t.Context(), nil, matchers...),
|
||||
searcher.SearchLabelValues(t.Context(), "instance", nil, matchers...),
|
||||
} {
|
||||
require.False(t, result.Next())
|
||||
require.ErrorIs(t, result.Err(), errSchemaAwareSearchUnsupported)
|
||||
require.NoError(t, result.Close())
|
||||
}
|
||||
}
|
||||
require.Len(t, recorder.calls, calls)
|
||||
}
|
||||
|
||||
t.Run("does not advertise absent capability", func(t *testing.T) {
|
||||
querierCloses, chunkCloses := 0, 0
|
||||
wrapped := AwareStorage(&optionalSearchStorage{
|
||||
querier: &nonSearchQuerier{Querier: storage.NoopQuerier(), closes: &querierCloses},
|
||||
chunkQuerier: &nonSearchChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), closes: &chunkCloses},
|
||||
})
|
||||
|
||||
querier, err := wrapped.Querier(0, 1)
|
||||
require.NoError(t, err)
|
||||
_, ok := querier.(storage.Searcher)
|
||||
require.False(t, ok)
|
||||
require.NoError(t, querier.Close())
|
||||
|
||||
chunkQuerier, err := wrapped.ChunkQuerier(0, 1)
|
||||
require.NoError(t, err)
|
||||
_, ok = chunkQuerier.(storage.Searcher)
|
||||
require.False(t, ok)
|
||||
require.NoError(t, chunkQuerier.Close())
|
||||
require.Equal(t, 1, querierCloses)
|
||||
require.Equal(t, 1, chunkCloses)
|
||||
})
|
||||
|
||||
t.Run("delegates present capability", func(t *testing.T) {
|
||||
querierCloses, chunkCloses := 0, 0
|
||||
querierResult := &recordingSearchResultSet{}
|
||||
chunkResult := &recordingSearchResultSet{}
|
||||
querierRecorder := &searchRecorder{result: querierResult}
|
||||
chunkRecorder := &searchRecorder{result: chunkResult}
|
||||
wrapped := AwareStorage(&optionalSearchStorage{
|
||||
querier: &searchableQuerier{
|
||||
nonSearchQuerier: &nonSearchQuerier{Querier: storage.NoopQuerier(), closes: &querierCloses},
|
||||
recorder: querierRecorder,
|
||||
},
|
||||
chunkQuerier: &searchableChunkQuerier{
|
||||
nonSearchChunkQuerier: &nonSearchChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), closes: &chunkCloses},
|
||||
recorder: chunkRecorder,
|
||||
},
|
||||
})
|
||||
|
||||
querier, err := wrapped.Querier(0, 1)
|
||||
require.NoError(t, err)
|
||||
searcher, ok := querier.(storage.Searcher)
|
||||
require.True(t, ok)
|
||||
assertDelegates(t, searcher, querierRecorder)
|
||||
assertRejectsReserved(t, searcher, querierRecorder)
|
||||
require.NoError(t, querier.Close())
|
||||
|
||||
chunkQuerier, err := wrapped.ChunkQuerier(0, 1)
|
||||
require.NoError(t, err)
|
||||
searcher, ok = chunkQuerier.(storage.Searcher)
|
||||
require.True(t, ok)
|
||||
assertDelegates(t, searcher, chunkRecorder)
|
||||
assertRejectsReserved(t, searcher, chunkRecorder)
|
||||
require.NoError(t, chunkQuerier.Close())
|
||||
|
||||
require.Equal(t, 2, querierResult.closes)
|
||||
require.Equal(t, 2, chunkResult.closes)
|
||||
require.Equal(t, 1, querierCloses)
|
||||
require.Equal(t, 1, chunkCloses)
|
||||
})
|
||||
}
|
||||
|
||||
type resolverBudgetQuerier struct {
|
||||
storage.Querier
|
||||
calls *resolverBudgetCallCounts
|
||||
}
|
||||
|
||||
func (q *resolverBudgetQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.SeriesSet {
|
||||
q.calls.series++
|
||||
return storage.NoopSeriesSet()
|
||||
}
|
||||
|
||||
func (q *resolverBudgetQuerier) LabelNames(context.Context, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
q.calls.labelNames++
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (q *resolverBudgetQuerier) LabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
q.calls.labelValues++
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
type resolverBudgetChunkQuerier struct {
|
||||
storage.ChunkQuerier
|
||||
calls *resolverBudgetCallCounts
|
||||
}
|
||||
|
||||
func (q *resolverBudgetChunkQuerier) Select(context.Context, bool, *storage.SelectHints, ...*labels.Matcher) storage.ChunkSeriesSet {
|
||||
q.calls.chunk++
|
||||
return storage.NoopChunkedSeriesSet()
|
||||
}
|
||||
|
||||
func (q *resolverBudgetChunkQuerier) LabelNames(context.Context, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
q.calls.labelNames++
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (q *resolverBudgetChunkQuerier) LabelValues(context.Context, string, *storage.LabelHints, ...*labels.Matcher) ([]string, annotations.Annotations, error) {
|
||||
q.calls.labelValues++
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func TestSchemaExpansionFailsBeforeStorage(t *testing.T) {
|
||||
engine := newSchemaEngine(embeddedRegistry)
|
||||
engine.limits = schemaExpansionLimits{work: 1, keyBytes: 1_000}
|
||||
calls := &resolverBudgetCallCounts{}
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
querier := &awareQuerier{
|
||||
Querier: &resolverBudgetQuerier{Querier: storage.NoopQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
series := querier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, series.Next())
|
||||
require.ErrorIs(t, series.Err(), errSchemaExpansion)
|
||||
|
||||
chunkQuerier := &awareChunkQuerier{
|
||||
ChunkQuerier: &resolverBudgetChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
chunks := chunkQuerier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, chunks.Next())
|
||||
require.ErrorIs(t, chunks.Err(), errSchemaExpansion)
|
||||
|
||||
_, _, err := querier.LabelNames(t.Context(), nil, matchers...)
|
||||
require.ErrorIs(t, err, errSchemaExpansion)
|
||||
_, _, err = querier.LabelValues(t.Context(), "tenant", nil, matchers...)
|
||||
require.ErrorIs(t, err, errSchemaExpansion)
|
||||
|
||||
require.Equal(t, &resolverBudgetCallCounts{}, calls)
|
||||
}
|
||||
|
||||
func TestUnsafeSchemaMatcherFailsBeforeStorage(t *testing.T) {
|
||||
registry := map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old: attr.current
|
||||
`),
|
||||
"1.1.0": []byte(`groups:
|
||||
- id: metric.metric
|
||||
type: metric
|
||||
metric_name: metric
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
`),
|
||||
}
|
||||
engine := newSchemaEngine(newRegistrySource(registry))
|
||||
calls := &resolverBudgetCallCounts{}
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric"),
|
||||
labels.MustNewMatcher(labels.MatchNotEqual, "attr.current", "excluded"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
querier := &awareQuerier{
|
||||
Querier: &resolverBudgetQuerier{Querier: storage.NoopQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
series := querier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, series.Next())
|
||||
require.ErrorIs(t, series.Err(), errUnsafeSchemaMatcher)
|
||||
|
||||
chunkQuerier := &awareChunkQuerier{
|
||||
ChunkQuerier: &resolverBudgetChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
chunks := chunkQuerier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, chunks.Next())
|
||||
require.ErrorIs(t, chunks.Err(), errUnsafeSchemaMatcher)
|
||||
|
||||
_, _, err := querier.LabelNames(t.Context(), nil, matchers...)
|
||||
require.ErrorIs(t, err, errUnsafeSchemaMatcher)
|
||||
_, _, err = querier.LabelValues(t.Context(), "attr.current", nil, matchers...)
|
||||
require.ErrorIs(t, err, errUnsafeSchemaMatcher)
|
||||
require.Equal(t, &resolverBudgetCallCounts{}, calls)
|
||||
}
|
||||
|
||||
func TestAmbiguousSchemaReuseFailsBeforeStorage(t *testing.T) {
|
||||
metricReuseSchema := `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.2.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
foo: bar
|
||||
1.2.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
baz: foo
|
||||
`
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
metric string
|
||||
attribute string
|
||||
labelValue string
|
||||
schema string
|
||||
}{
|
||||
{
|
||||
name: "historical metric alias",
|
||||
metric: "bar",
|
||||
labelValue: model.MetricNameLabel,
|
||||
schema: metricReuseSchema,
|
||||
},
|
||||
{
|
||||
name: "reused metric anchor",
|
||||
metric: "foo",
|
||||
labelValue: model.MetricNameLabel,
|
||||
schema: metricReuseSchema,
|
||||
},
|
||||
{
|
||||
name: "reused attribute alias",
|
||||
metric: "metric",
|
||||
attribute: "user",
|
||||
labelValue: "user",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.2.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
user: tenant
|
||||
1.2.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
account: user
|
||||
`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
registry := map[string][]byte{
|
||||
"registry.yaml": []byte(tc.schema),
|
||||
"1.2.0": []byte(`groups:
|
||||
- id: metric.metric
|
||||
type: metric
|
||||
metric_name: metric
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
- id: metric.foo
|
||||
type: metric
|
||||
metric_name: foo
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
- id: metric.bar
|
||||
type: metric
|
||||
metric_name: bar
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
`),
|
||||
}
|
||||
engine := newSchemaEngine(newRegistrySource(registry))
|
||||
calls := &resolverBudgetCallCounts{}
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, tc.metric),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.2.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
if tc.attribute != "" {
|
||||
matchers = append(matchers, labels.MustNewMatcher(labels.MatchEqual, tc.attribute, "value"))
|
||||
}
|
||||
|
||||
querier := &awareQuerier{
|
||||
Querier: &resolverBudgetQuerier{Querier: storage.NoopQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
series := querier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, series.Next())
|
||||
require.ErrorIs(t, series.Err(), errAmbiguousSchemaRename)
|
||||
|
||||
chunkQuerier := &awareChunkQuerier{
|
||||
ChunkQuerier: &resolverBudgetChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
chunks := chunkQuerier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, chunks.Next())
|
||||
require.ErrorIs(t, chunks.Err(), errAmbiguousSchemaRename)
|
||||
|
||||
_, _, err := querier.LabelNames(t.Context(), nil, matchers...)
|
||||
require.ErrorIs(t, err, errAmbiguousSchemaRename)
|
||||
_, _, err = querier.LabelValues(t.Context(), tc.labelValue, nil, matchers...)
|
||||
require.ErrorIs(t, err, errAmbiguousSchemaRename)
|
||||
require.Equal(t, &resolverBudgetCallCounts{}, calls)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardMetricConvergenceFansOutToStorage(t *testing.T) {
|
||||
registry := map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.old.one: metric.current
|
||||
metric.old.two: metric.current
|
||||
`),
|
||||
"1.0.0": []byte(`groups:
|
||||
- id: metric.metric.old.one
|
||||
type: metric
|
||||
metric_name: metric.old.one
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
`),
|
||||
}
|
||||
engine := newSchemaEngine(newRegistrySource(registry))
|
||||
calls := &resolverBudgetCallCounts{}
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric.old.one"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.0.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
querier := &awareQuerier{
|
||||
Querier: &resolverBudgetQuerier{Querier: storage.NoopQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
series := querier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, series.Next())
|
||||
require.NoError(t, series.Err())
|
||||
|
||||
chunkQuerier := &awareChunkQuerier{
|
||||
ChunkQuerier: &resolverBudgetChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
chunks := chunkQuerier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, chunks.Next())
|
||||
require.NoError(t, chunks.Err())
|
||||
|
||||
_, _, err := querier.LabelNames(t.Context(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
_, _, err = querier.LabelValues(t.Context(), model.MetricNameLabel, nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Positive(t, calls.series)
|
||||
require.Positive(t, calls.chunk)
|
||||
require.Positive(t, calls.labelNames)
|
||||
require.Positive(t, calls.labelValues)
|
||||
}
|
||||
|
||||
func TestSchemaConvergenceFansOutToStorage(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
metric string
|
||||
attribute string
|
||||
schema string
|
||||
}{
|
||||
{
|
||||
name: "within one change",
|
||||
metric: "metric",
|
||||
attribute: "attr.current",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old.one: attr.current
|
||||
attr.old.two: attr.current
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "across ordered changes",
|
||||
metric: "metric",
|
||||
attribute: "attr.current",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old.one: attr.current
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old.two: attr.current
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "through an intermediate predecessor",
|
||||
metric: "metric",
|
||||
attribute: "attr.current",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old: attr.current
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old: attr.middle
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.middle: attr.current
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "attribute convergence across revisions",
|
||||
metric: "metric",
|
||||
attribute: "attr.current",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old.one: attr.current
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old.two: attr.current
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "metric convergence across revisions",
|
||||
metric: "metric.current",
|
||||
schema: `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.old.one: metric.current
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.old.two: metric.current
|
||||
`,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
registry := map[string][]byte{
|
||||
"registry.yaml": []byte(tc.schema),
|
||||
"1.1.0": []byte(`groups:
|
||||
- id: metric.metric
|
||||
type: metric
|
||||
metric_name: metric
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
- id: metric.metric.current
|
||||
type: metric
|
||||
metric_name: metric.current
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
`),
|
||||
}
|
||||
engine := newSchemaEngine(newRegistrySource(registry))
|
||||
calls := &resolverBudgetCallCounts{}
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, tc.metric),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
if tc.attribute != "" {
|
||||
matchers = append(matchers, labels.MustNewMatcher(labels.MatchEqual, tc.attribute, "value"))
|
||||
}
|
||||
|
||||
querier := &awareQuerier{
|
||||
Querier: &resolverBudgetQuerier{Querier: storage.NoopQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
series := querier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, series.Next())
|
||||
require.NoError(t, series.Err())
|
||||
|
||||
chunkQuerier := &awareChunkQuerier{
|
||||
ChunkQuerier: &resolverBudgetChunkQuerier{ChunkQuerier: storage.NoopChunkedQuerier(), calls: calls},
|
||||
engine: engine,
|
||||
canonicalSeriesLimit: maxCanonicalSeriesMaterialization,
|
||||
}
|
||||
chunks := chunkQuerier.Select(t.Context(), true, nil, matchers...)
|
||||
require.False(t, chunks.Next())
|
||||
require.NoError(t, chunks.Err())
|
||||
|
||||
_, _, err := querier.LabelNames(t.Context(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
labelName := tc.attribute
|
||||
if labelName == "" {
|
||||
labelName = model.MetricNameLabel
|
||||
}
|
||||
_, _, err = querier.LabelValues(t.Context(), labelName, nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Positive(t, calls.series)
|
||||
require.Positive(t, calls.chunk)
|
||||
require.Positive(t, calls.labelNames)
|
||||
require.Positive(t, calls.labelValues)
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,171 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage/semconv"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
func inheritedAttributeRegistry() map[string][]byte {
|
||||
return map[string][]byte{
|
||||
"registry.yaml": []byte(`
|
||||
file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
legacy.user: tenant.id
|
||||
legacy.partition: tenant.partition
|
||||
apply_to_metrics:
|
||||
- queue.depth
|
||||
`),
|
||||
"1.0.0": []byte(`
|
||||
groups:
|
||||
- id: attributes.identity
|
||||
type: attribute_group
|
||||
prefix: legacy
|
||||
attributes:
|
||||
- id: user
|
||||
- id: attributes.queue.base
|
||||
type: attribute_group
|
||||
extends: attributes.identity
|
||||
- id: attributes.queue
|
||||
type: attribute_group
|
||||
extends: attributes.queue.base
|
||||
prefix: legacy
|
||||
attributes:
|
||||
- id: partition
|
||||
- id: metric.queue.depth
|
||||
type: metric
|
||||
metric_name: queue.depth
|
||||
instrument: updowncounter
|
||||
unit: "{item}"
|
||||
extends: attributes.queue
|
||||
`),
|
||||
"1.1.0": []byte(`
|
||||
groups:
|
||||
- id: metric.queue.depth
|
||||
type: metric
|
||||
metric_name: queue.depth
|
||||
instrument: updowncounter
|
||||
unit: "{item}"
|
||||
extends: attributes.queue
|
||||
- id: attributes.queue
|
||||
type: attribute_group
|
||||
extends: attributes.queue.base
|
||||
prefix: tenant
|
||||
attributes:
|
||||
- id: partition
|
||||
- id: attributes.identity
|
||||
type: attribute_group
|
||||
prefix: tenant
|
||||
attributes:
|
||||
- id: id
|
||||
- id: attributes.queue.base
|
||||
type: attribute_group
|
||||
extends: attributes.identity
|
||||
`),
|
||||
}
|
||||
}
|
||||
|
||||
func TestInheritedMetricAttributesAreCanonicalized(t *testing.T) {
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(teststorage.New(t), inheritedAttributeRegistry())
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "queue.depth", 1, 7, "legacy.user", "alice", "legacy.partition", "primary")
|
||||
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "queue.depth"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "tenant.id", "alice"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "tenant.partition", "primary"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
series := collectSeries(t, set)
|
||||
require.Len(t, series, 1)
|
||||
for key := range series {
|
||||
require.Contains(t, key, `"tenant.id"="alice"`)
|
||||
require.Contains(t, key, `"tenant.partition"="primary"`)
|
||||
require.NotContains(t, key, "legacy.user")
|
||||
require.NotContains(t, key, "legacy.partition")
|
||||
}
|
||||
require.Empty(t, warningStrings(set.Warnings()))
|
||||
|
||||
names, warnings, err := q.LabelNames(context.Background(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, names, "tenant.id")
|
||||
require.Contains(t, names, "tenant.partition")
|
||||
require.NotContains(t, names, "legacy.user")
|
||||
require.NotContains(t, names, "legacy.partition")
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
|
||||
values, warnings, err := q.LabelValues(context.Background(), "tenant.id", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"alice"}, values)
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
|
||||
values, warnings, err = q.LabelValues(context.Background(), "tenant.partition", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"primary"}, values)
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
|
||||
cq, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = cq.Close() })
|
||||
|
||||
chunkSet := cq.Select(context.Background(), false, nil, matchers...)
|
||||
require.True(t, chunkSet.Next())
|
||||
require.Equal(t, "alice", chunkSet.At().Labels().Get("tenant.id"))
|
||||
require.Equal(t, "primary", chunkSet.At().Labels().Get("tenant.partition"))
|
||||
require.Empty(t, chunkSet.At().Labels().Get("legacy.user"))
|
||||
require.Empty(t, chunkSet.At().Labels().Get("legacy.partition"))
|
||||
require.False(t, chunkSet.Next())
|
||||
require.NoError(t, chunkSet.Err())
|
||||
require.Empty(t, warningStrings(chunkSet.Warnings()))
|
||||
|
||||
names, warnings, err = cq.LabelNames(context.Background(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Contains(t, names, "tenant.id")
|
||||
require.Contains(t, names, "tenant.partition")
|
||||
require.NotContains(t, names, "legacy.user")
|
||||
require.NotContains(t, names, "legacy.partition")
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
|
||||
values, warnings, err = cq.LabelValues(context.Background(), "tenant.id", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"alice"}, values)
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
|
||||
values, warnings, err = cq.LabelValues(context.Background(), "tenant.partition", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"primary"}, values)
|
||||
require.Empty(t, warningStrings(warnings))
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
)
|
||||
|
||||
// TestLabelValuesCanonicalisesTheQueriedName asks for the values of a renamed
|
||||
// attribute under its pre-rename name. The name is canonicalised first, so the
|
||||
// fan-out covers the same aliases as asking under the anchor-version name;
|
||||
// otherwise only the era that happens to store the attribute under the name as
|
||||
// given contributes any values.
|
||||
func TestLabelValuesCanonicalisesTheQueriedName(t *testing.T) {
|
||||
wrapped, _ := newAwareStorage(t)
|
||||
appendSeries(t, wrapped, "test.counter", 1, 1.0, "user", "a")
|
||||
appendSeries(t, wrapped, "test", 1, 2.0, "tenant", "b")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
// "user" is the 1.0.0 name of the attribute the 1.1.0 anchor calls "tenant".
|
||||
values, _, err := q.LabelValues(context.Background(), "user", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"a", "b"}, values)
|
||||
}
|
||||
|
||||
// TestLabelNamesHidesReservedLabels checks that the reserved matcher labels are
|
||||
// not reported as label names. Select strips them from the series it returns, so
|
||||
// reporting them would advertise labels no series carries.
|
||||
func TestLabelNamesHidesReservedLabels(t *testing.T) {
|
||||
wrapped, _ := newAwareStorage(t)
|
||||
appendSeries(t, wrapped, "test", 1, 1.0, "tenant", "a", "__schema_url__", "registry/registry.yaml")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
names, _, err := q.LabelNames(context.Background(), nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{model.MetricNameLabel, "tenant"}, names)
|
||||
}
|
||||
|
||||
func TestLabelValuesHidesReservedLabels(t *testing.T) {
|
||||
wrapped, counts := newCountingAwareStorage(t, canonicalLabelRegistry())
|
||||
appendSeries(t, wrapped, "jvm.thread.count", 1, 1.0,
|
||||
"__schema_url__", "stored-schema",
|
||||
"__semconv_url__", "stored-semconv",
|
||||
)
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
cq, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = cq.Close() })
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
query func(string) ([]string, error)
|
||||
}{
|
||||
{
|
||||
name: "querier",
|
||||
query: func(name string) ([]string, error) {
|
||||
values, _, err := q.LabelValues(context.Background(), name, nil, canonicalLabelMatchers()...)
|
||||
return values, err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "chunk querier",
|
||||
query: func(name string) ([]string, error) {
|
||||
values, _, err := cq.LabelValues(context.Background(), name, nil, canonicalLabelMatchers()...)
|
||||
return values, err
|
||||
},
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
for _, name := range []string{"__schema_url__", "__semconv_url__"} {
|
||||
counts.reset()
|
||||
values, err := tc.query(name)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, values)
|
||||
require.Empty(t, values)
|
||||
require.Zero(t, counts.total(), "reserved values must not schedule storage jobs")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
counts.reset()
|
||||
values, _, err := q.LabelValues(context.Background(), "__schema_url__", nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "jvm.thread.count"),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, []string{"stored-schema"}, values, "ordinary queries must retain passthrough behavior")
|
||||
require.Equal(t, int64(1), counts.total())
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
// 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 semconv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
var materializationTestRegistry = map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
budget.old: budget.current
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.old: attr.current
|
||||
apply_to_metrics:
|
||||
- budget.current
|
||||
1.2.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
budget.current: budget.future
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.current: attr.future
|
||||
apply_to_metrics:
|
||||
- budget.future
|
||||
`),
|
||||
"1.0.0": []byte(`groups:
|
||||
- id: registry.attr.old
|
||||
type: attribute_group
|
||||
brief: Old attribute.
|
||||
attributes:
|
||||
- id: attr.old
|
||||
type: string
|
||||
brief: Old attribute.
|
||||
examples: [value]
|
||||
- id: metric.budget.old
|
||||
type: metric
|
||||
brief: Old metric.
|
||||
metric_name: budget.old
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
attributes:
|
||||
- ref: attr.old
|
||||
`),
|
||||
"1.1.0": []byte(`groups:
|
||||
- id: registry.attr.current
|
||||
type: attribute_group
|
||||
brief: Current attribute.
|
||||
attributes:
|
||||
- id: attr.current
|
||||
type: string
|
||||
brief: Current attribute.
|
||||
examples: [value]
|
||||
- id: metric.budget.current
|
||||
type: metric
|
||||
brief: Current metric.
|
||||
metric_name: budget.current
|
||||
instrument: counter
|
||||
unit: "1"
|
||||
attributes:
|
||||
- ref: attr.current
|
||||
`),
|
||||
}
|
||||
|
||||
func newMaterializationTestStorage(t *testing.T, limit, oldSeries, futureSeries int) *awareStorage {
|
||||
t.Helper()
|
||||
underlying := teststorage.New(t)
|
||||
wrapped := newAwareStorage(underlying, newSchemaEngine(newRegistrySource(materializationTestRegistry)))
|
||||
wrapped.canonicalSeriesLimit = limit
|
||||
app := wrapped.Appender(context.Background())
|
||||
appendSeries := func(metric, attribute string, index int) {
|
||||
labelValues := []string{
|
||||
model.MetricNameLabel, metric,
|
||||
attribute, "value",
|
||||
"instance", fmt.Sprintf("%03d", index),
|
||||
}
|
||||
_, err := app.Append(0, labels.FromStrings(labelValues...), 1, float64(index))
|
||||
require.NoError(t, err)
|
||||
}
|
||||
for i := range oldSeries {
|
||||
appendSeries("budget.old", "attr.old", i)
|
||||
}
|
||||
for i := range futureSeries {
|
||||
appendSeries("budget.future", "attr.future", oldSeries+i)
|
||||
}
|
||||
require.NoError(t, app.Commit())
|
||||
variants, _, err := wrapped.engine.findMatcherVariants("registry/1.1.0", "registry/registry.yaml", materializationTestMatchers())
|
||||
require.NoError(t, err)
|
||||
require.Len(t, variants, 3)
|
||||
var names []string
|
||||
for _, variant := range variants {
|
||||
name, err := extractMetricName(variant.matchers)
|
||||
require.NoError(t, err)
|
||||
names = append(names, name)
|
||||
}
|
||||
require.ElementsMatch(t, []string{"budget.old", "budget.current", "budget.future"}, names)
|
||||
return wrapped
|
||||
}
|
||||
|
||||
func materializationTestMatchers() []*labels.Matcher {
|
||||
return []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "budget.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, semconvURLLabel, "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, schemaURLLabel, "registry/registry.yaml"),
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonicalSeriesMaterializationBudget(t *testing.T) {
|
||||
for _, query := range []string{"series", "chunks"} {
|
||||
t.Run(query+" allows exact limit", func(t *testing.T) {
|
||||
wrapped := newMaterializationTestStorage(t, 2, 1, 1)
|
||||
if query == "series" {
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, nil, materializationTestMatchers()...)
|
||||
for set.Next() {
|
||||
}
|
||||
require.NoError(t, set.Err())
|
||||
return
|
||||
}
|
||||
|
||||
q, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, nil, materializationTestMatchers()...)
|
||||
for set.Next() {
|
||||
}
|
||||
require.NoError(t, set.Err())
|
||||
})
|
||||
|
||||
t.Run(query+" rejects the next cross-variant input", func(t *testing.T) {
|
||||
wrapped := newMaterializationTestStorage(t, 2, 2, 1)
|
||||
hints := &storage.SelectHints{Start: 0, End: 10, Limit: 1}
|
||||
if query == "series" {
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, hints, materializationTestMatchers()...)
|
||||
require.False(t, set.Next())
|
||||
require.ErrorIs(t, set.Err(), errCanonicalSeriesMaterialization)
|
||||
require.ErrorContains(t, set.Err(), "more than 2 input series")
|
||||
return
|
||||
}
|
||||
|
||||
q, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, hints, materializationTestMatchers()...)
|
||||
require.False(t, set.Next())
|
||||
require.ErrorIs(t, set.Err(), errCanonicalSeriesMaterialization)
|
||||
require.ErrorContains(t, set.Err(), "more than 2 input series")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,221 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage/semconv"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
// readRegistryDir loads an on-disk registry directory into a base-name → bytes
|
||||
// map, mirroring how an operator supplies a registry via configuration.
|
||||
func readRegistryDir(t *testing.T, dir string) map[string][]byte {
|
||||
t.Helper()
|
||||
entries, err := os.ReadDir(dir)
|
||||
require.NoError(t, err)
|
||||
files := map[string][]byte{}
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
b, err := os.ReadFile(filepath.Join(dir, e.Name()))
|
||||
require.NoError(t, err)
|
||||
files[e.Name()] = b
|
||||
}
|
||||
require.NotEmpty(t, files)
|
||||
return files
|
||||
}
|
||||
|
||||
func registryWithSemconv(contents []byte) map[string][]byte {
|
||||
return map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
versions:
|
||||
1.0.0:
|
||||
`),
|
||||
"1.0.0": contents,
|
||||
}
|
||||
}
|
||||
|
||||
func writeAttributeRefs(b *strings.Builder, prefix string, count int) {
|
||||
for i := range count {
|
||||
b.WriteString(" - ref: ")
|
||||
b.WriteString(prefix)
|
||||
b.WriteString(strconv.Itoa(i))
|
||||
b.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
|
||||
func inheritedAttributeLimitRegistry(parentAttrs, metricAttrs int) map[string][]byte {
|
||||
var b strings.Builder
|
||||
b.WriteString(`groups:
|
||||
- id: attributes.base
|
||||
type: attribute_group
|
||||
attributes:
|
||||
`)
|
||||
writeAttributeRefs(&b, "base.", parentAttrs)
|
||||
b.WriteString(` - id: metric.queue.depth
|
||||
type: metric
|
||||
metric_name: queue.depth
|
||||
extends: attributes.base
|
||||
attributes:
|
||||
`)
|
||||
writeAttributeRefs(&b, "metric.", metricAttrs)
|
||||
return registryWithSemconv([]byte(b.String()))
|
||||
}
|
||||
|
||||
func inheritedAttributeFanoutRegistry(children int) map[string][]byte {
|
||||
var b strings.Builder
|
||||
b.WriteString(`groups:
|
||||
- id: attributes.base
|
||||
type: attribute_group
|
||||
attributes:
|
||||
`)
|
||||
writeAttributeRefs(&b, "base.", 256)
|
||||
for i := range children {
|
||||
b.WriteString(" - id: attributes.child.")
|
||||
b.WriteString(strconv.Itoa(i))
|
||||
b.WriteString("\n type: attribute_group\n extends: attributes.base\n")
|
||||
}
|
||||
return registryWithSemconv([]byte(b.String()))
|
||||
}
|
||||
|
||||
func TestAwareStorageWithRegistry(t *testing.T) {
|
||||
t.Run("rejects an invalid registry", func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), map[string][]byte{
|
||||
"registry.yaml": []byte("file_format: 9.9.9\n"),
|
||||
})
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported metric splits", func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- split:
|
||||
apply_to_metric: http.server.duration
|
||||
by_attribute: http.request.method
|
||||
metrics_from_attributes:
|
||||
http.server.get.duration: GET
|
||||
`),
|
||||
})
|
||||
require.EqualError(t, err, `registry schema "registry.yaml": schema version "1.1.0" contains unsupported metric split transformation`)
|
||||
})
|
||||
|
||||
t.Run("rejects unknown schema transformations", func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), map[string][]byte{
|
||||
"registry.yaml": []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metric: {}
|
||||
`),
|
||||
})
|
||||
require.EqualError(t, err, `registry schema "registry.yaml": schema version "1.1.0" metrics change 1 contains unsupported transformation "rename_metric"`)
|
||||
})
|
||||
|
||||
t.Run("rejects an empty registry", func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), nil)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
t.Run("rejects invalid unused semconv metadata", func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), registryWithSemconv([]byte(`
|
||||
groups:
|
||||
- id: attributes.unused
|
||||
type: attribute_group
|
||||
extends: attributes.missing
|
||||
`)))
|
||||
require.ErrorContains(t, err, `semconv group "attributes.unused" extends unknown group "attributes.missing"`)
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
metricAttrs int
|
||||
wantErrContains string
|
||||
}{
|
||||
{name: "accepts 256 resolved attributes", metricAttrs: 6},
|
||||
{name: "rejects 257 resolved attributes", metricAttrs: 7, wantErrContains: "semconv group attributes would exceed 256"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), inheritedAttributeLimitRegistry(250, tc.metricAttrs))
|
||||
if tc.wantErrContains != "" {
|
||||
require.ErrorContains(t, err, tc.wantErrContains)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("rejects excessive cumulative inherited attributes", func(t *testing.T) {
|
||||
files := inheritedAttributeFanoutRegistry(256)
|
||||
require.Less(t, len(files["1.0.0"]), 16<<20)
|
||||
_, err := semconv.AwareStorageWithRegistry(teststorage.New(t), files)
|
||||
require.ErrorContains(t, err, "semconv file attribute slots would exceed 65536")
|
||||
})
|
||||
|
||||
// An operator registry that mirrors the embedded one must drive the same
|
||||
// schema-version rename fan-out, proving the injected source behaves
|
||||
// identically to the embedded default.
|
||||
t.Run("an operator registry mirroring the embedded one resolves identically", func(t *testing.T) {
|
||||
files := readRegistryDir(t, "registry")
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, files)
|
||||
require.NoError(t, err)
|
||||
// The wrapper must retain its validated copy, not aliases owned by the caller.
|
||||
for name, b := range files {
|
||||
clear(b)
|
||||
files[name] = []byte("mutated after construction")
|
||||
}
|
||||
|
||||
// Written under the semconv 1.0.0 name; semconv 1.1.0 renamed it to "test".
|
||||
appendSeries(t, wrapped, "test.counter", 1, 7.0, "http.response.status_code", "200")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "test"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.NotEmpty(t, got, "expected the historical name to surface via the operator registry")
|
||||
var found bool
|
||||
for k := range got {
|
||||
if strings.Contains(k, `__name__="test"`) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected the renamed metric under its 1.1.0 name in: %v", got)
|
||||
})
|
||||
}
|
||||
@@ -1,947 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage/semconv"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
// renameSchema is a two-version schema renaming old→new at 1.1.0, the shape
|
||||
// every case below varies the semconv files around.
|
||||
const renameSchema = `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
%s: %s
|
||||
`
|
||||
|
||||
// metricSemconv renders a semconv file declaring a single metric group. The
|
||||
// group id follows the "metric.<metric_name>" form that upstream semantic
|
||||
// conventions lint for, so it necessarily changes whenever the metric name does.
|
||||
func metricSemconv(metricName, unit, instrument string) []byte {
|
||||
return metricSemconvWithStability(metricName, unit, instrument, "stable")
|
||||
}
|
||||
|
||||
func metricSemconvWithStability(metricName, unit, instrument, stability string) []byte {
|
||||
stabilityLine := ""
|
||||
if stability != "" {
|
||||
stabilityLine = fmt.Sprintf(" stability: %s\n", stability)
|
||||
}
|
||||
return fmt.Appendf(nil, `groups:
|
||||
- id: metric.%s
|
||||
type: metric
|
||||
metric_name: %s
|
||||
unit: %q
|
||||
instrument: %s
|
||||
%s attributes:
|
||||
- ref: http.response.status_code
|
||||
`, metricName, metricName, unit, instrument, stabilityLine)
|
||||
}
|
||||
|
||||
// selectRenamed queries metricName anchored at semconv 1.1.0 over the given
|
||||
// registry and returns the series found plus any warnings raised.
|
||||
func selectRenamed(t *testing.T, files map[string][]byte, appendUnder, metricName string) (map[string]float64, []string) {
|
||||
t.Helper()
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, files)
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, appendUnder, 1, 7.0, "http.response.status_code", "200")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, metricName),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
return got, warningStrings(set.Warnings())
|
||||
}
|
||||
|
||||
func selectRenamedError(t *testing.T, files map[string][]byte, appendUnder, metricName string) error {
|
||||
t.Helper()
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, files)
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, appendUnder, 1, 7.0, "http.response.status_code", "200")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, metricName),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
require.False(t, set.Next())
|
||||
return set.Err()
|
||||
}
|
||||
|
||||
func TestRenameEdgeValidation(t *testing.T) {
|
||||
// A legitimate rename: the metric is the same measurement under a new name,
|
||||
// so unit and instrument agree and the historical series must still merge.
|
||||
//
|
||||
// This is also the case that rules out using the semconv group id as a
|
||||
// cross-version identity: the ids here are metric.old.name and
|
||||
// metric.new.name, so requiring them to be equal across the edge would
|
||||
// reject this rename even though it is exactly the rename the schema exists
|
||||
// to describe.
|
||||
t.Run("merges a rename whose unit and instrument agree", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "s", "histogram"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "old.name", "new.name")
|
||||
|
||||
require.Len(t, got, 1, "expected the pre-rename series under the queried name, got %v", got)
|
||||
for k := range got {
|
||||
require.Contains(t, k, `__name__="new.name"`)
|
||||
}
|
||||
require.Empty(t, warnings, "a corroborated rename must not warn")
|
||||
})
|
||||
|
||||
// The case the schema format cannot express and name traversal cannot
|
||||
// detect: two unrelated metrics that happen to share a surface name at
|
||||
// different versions. Their units disagree, so merging them would average
|
||||
// seconds with a queue depth.
|
||||
t.Run("does not merge a rename whose unit disagrees", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "{item}", "updowncounter"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "old.name", "new.name")
|
||||
|
||||
require.Empty(t, got, "series of a differently-united metric must not be merged in, got %v", got)
|
||||
requireWarningsContain(t, warnings, "treating them as different metrics")
|
||||
requireWarningsContain(t, warnings, `resolves it to "old.name"`)
|
||||
})
|
||||
|
||||
// Same unit, different instrument: a histogram and a counter are not the
|
||||
// same metric even when they measure in the same unit.
|
||||
t.Run("does not merge a rename whose instrument disagrees", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "s", "counter"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "old.name", "new.name")
|
||||
|
||||
require.Empty(t, got, "got %v", got)
|
||||
requireWarningsContain(t, warnings, "treating them as different metrics")
|
||||
})
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
oldStability string
|
||||
newStability string
|
||||
}{
|
||||
{name: "development definitions", oldStability: "development", newStability: "development"},
|
||||
{name: "one stable definition", oldStability: "development", newStability: "stable"},
|
||||
{name: "unspecified definitions"},
|
||||
} {
|
||||
t.Run("follows a metadata-changing rename for "+tc.name, func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconvWithStability("old.name", "{item}", "updowncounter", tc.oldStability),
|
||||
"1.1.0": metricSemconvWithStability("new.name", "s", "histogram", tc.newStability),
|
||||
}, "old.name", "new.name")
|
||||
|
||||
require.Len(t, got, 1, "an explicit non-stable rename must still surface its historical series, got %v", got)
|
||||
requireWarningsContain(t, warnings, "following the explicit schema rename")
|
||||
})
|
||||
}
|
||||
|
||||
// A registry need not ship a semconv for every version its schema
|
||||
// references, so an absent file means "cannot verify" and must leave the
|
||||
// existing name-traversal behaviour untouched while warning the caller.
|
||||
t.Run("warns but traverses an unverifiable rename when a semconv is absent", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "old.name", "new.name")
|
||||
|
||||
require.Len(t, got, 1, "expected the pre-rename series to still merge, got %v", got)
|
||||
require.Len(t, warnings, 1)
|
||||
requireWarningsContain(t, warnings, "version 1.0.0 is unavailable")
|
||||
requireWarningsContain(t, warnings, "without corroboration")
|
||||
})
|
||||
|
||||
// A name the semconv does not declare as a metric is a strong hint of a
|
||||
// mis-authored edge, but a trimmed registry is a legitimate shape, so this
|
||||
// is reported without dropping series.
|
||||
t.Run("warns but still merges when a name is not declared as a metric", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "typo.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "s", "histogram"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "typo.name", "new.name")
|
||||
|
||||
require.Len(t, got, 1, "expected the series to still merge, got %v", got)
|
||||
requireWarningsContain(t, warnings, "could not be corroborated")
|
||||
})
|
||||
}
|
||||
|
||||
// TestRecycledMetricName covers the case the schema format cannot express: a
|
||||
// metric name that was renamed away and later reused by an unrelated metric.
|
||||
//
|
||||
// 1.0.0 foo exists, counting bytes
|
||||
// 2.0.0 schema renames foo to bar
|
||||
// 5.0.0 a new, unrelated metric claims the name foo, measuring seconds
|
||||
//
|
||||
// Querying foo at 5.0.0 means the new metric. The 2.0.0 rename edge concerns the
|
||||
// old foo and must not drag bar's series in. Walking backwards encounters the
|
||||
// queried name only on the old side of that edge, which marks the lifecycle
|
||||
// boundary even when the old and new metrics have identical signatures.
|
||||
func TestRecycledMetricName(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/5.0.0
|
||||
versions:
|
||||
1.0.0:
|
||||
2.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
foo: bar
|
||||
5.0.0:
|
||||
`)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
unit string
|
||||
instrument string
|
||||
}{
|
||||
{name: "different signature", unit: "s", instrument: "histogram"},
|
||||
{name: "same signature", unit: "By", instrument: "counter"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// 5.0.0 declares both the reused name and the metric the old foo became.
|
||||
currentSemconv := fmt.Appendf(nil, `groups:
|
||||
- id: metric.foo
|
||||
type: metric
|
||||
metric_name: foo
|
||||
unit: %q
|
||||
instrument: %s
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
- id: metric.bar
|
||||
type: metric
|
||||
metric_name: bar
|
||||
unit: "By"
|
||||
instrument: counter
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
`, tc.unit, tc.instrument)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.0.0": metricSemconv("foo", "By", "counter"),
|
||||
"2.0.0": metricSemconv("bar", "By", "counter"),
|
||||
"5.0.0": currentSemconv,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Series of the metric the old foo became. Querying today's foo must
|
||||
// not pick these up, even when identity fields happen to match.
|
||||
appendSeries(t, wrapped, "bar", 1, 7.0, "http.response.status_code", "200")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/5.0.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Empty(t, got, "an unrelated metric that once held this name must not be merged in, got %v", got)
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "metric lifecycle boundary")
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("trimmed anchor omits the reused name", func(t *testing.T) {
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.0.0": metricSemconv("foo", "By", "counter"),
|
||||
"2.0.0": metricSemconv("bar", "By", "counter"),
|
||||
"5.0.0": metricSemconv("unrelated", "s", "histogram"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, "bar", 1, 7.0, "http.response.status_code", "200")
|
||||
appendSeries(t, wrapped, "foo", 1, 9.0, "http.response.status_code", "500")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/5.0.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Equal(t, map[string]float64{
|
||||
labels.FromStrings(model.MetricNameLabel, "foo", "http.response.status_code", "500").String(): 9,
|
||||
}, got)
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "metric lifecycle boundary")
|
||||
})
|
||||
}
|
||||
|
||||
// TestAttributeRenameScope checks that an attribute rename restricted by
|
||||
// apply_to_metrics does not rewrite the attributes of other metrics. The schema
|
||||
// scopes user→tenant to scoped.metric only, so a series of other.metric that
|
||||
// carries user must keep it.
|
||||
func TestAttributeRenameScope(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
user: tenant
|
||||
apply_to_metrics:
|
||||
- scoped.metric
|
||||
`)
|
||||
semconv110 := []byte(`groups:
|
||||
- id: metric.scoped.metric
|
||||
type: metric
|
||||
metric_name: scoped.metric
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: tenant
|
||||
- id: metric.other.metric
|
||||
type: metric
|
||||
metric_name: other.metric
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: tenant
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.1.0": semconv110,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, "scoped.metric", 1, 1.0, "user", "alice")
|
||||
appendSeries(t, wrapped, "other.metric", 1, 2.0, "user", "bob")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
selectMetric := func(name string) map[string]float64 {
|
||||
return collectSeries(t, q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, name),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
))
|
||||
}
|
||||
|
||||
// The metric the rename is scoped to still has its historical attribute
|
||||
// normalised to the anchor version's name.
|
||||
got := selectMetric("scoped.metric")
|
||||
require.Len(t, got, 1, "got %v", got)
|
||||
for k := range got {
|
||||
require.Contains(t, k, `tenant="alice"`, "the scoped metric must be normalised")
|
||||
}
|
||||
|
||||
// Any other metric must be left alone. Before apply_to_metrics was honoured,
|
||||
// this attribute was rewritten to tenant as well.
|
||||
got = selectMetric("other.metric")
|
||||
require.Len(t, got, 1, "got %v", got)
|
||||
for k := range got {
|
||||
require.Contains(t, k, `user="bob"`, "a rename scoped to another metric must not apply here")
|
||||
require.NotContains(t, k, "tenant=")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRejectedLineageDoesNotLeakAttributeScope(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/5.0.0
|
||||
versions:
|
||||
1.0.0:
|
||||
2.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
foo: bar
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
user: tenant
|
||||
apply_to_metrics:
|
||||
- bar
|
||||
5.0.0:
|
||||
`)
|
||||
semconv500 := []byte(`groups:
|
||||
- id: metric.foo
|
||||
type: metric
|
||||
metric_name: foo
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: tenant
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"5.0.0": semconv500,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "foo", 1, 1.0, "user", "alice")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "foo"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/5.0.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 1)
|
||||
for key := range got {
|
||||
require.Contains(t, key, `user="alice"`)
|
||||
require.NotContains(t, key, `tenant=`)
|
||||
}
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "metric lifecycle boundary")
|
||||
}
|
||||
|
||||
func TestManyToOneAttributeScopeStaysBranchLocal(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
user: tenant
|
||||
apply_to_metrics:
|
||||
- metric.a
|
||||
- rename_metrics:
|
||||
metric.a: metric.current
|
||||
metric.b: metric.current
|
||||
`)
|
||||
semconv100 := []byte(`groups:
|
||||
- id: metric.metric.a
|
||||
type: metric
|
||||
metric_name: metric.a
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: user
|
||||
- id: metric.metric.b
|
||||
type: metric
|
||||
metric_name: metric.b
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: user
|
||||
`)
|
||||
semconv110 := []byte(`groups:
|
||||
- id: metric.metric.current
|
||||
type: metric
|
||||
metric_name: metric.current
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: tenant
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.0.0": semconv100,
|
||||
"1.1.0": semconv110,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "metric.a", 1, 1.0, "user", "alice")
|
||||
appendSeries(t, wrapped, "metric.b", 1, 2.0, "user", "bob")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
baseMatchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
set := q.Select(context.Background(), false, nil, baseMatchers...)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 2, "got %v", got)
|
||||
var sawScoped, sawUnscoped bool
|
||||
for key := range got {
|
||||
sawScoped = sawScoped || strings.Contains(key, `tenant="alice"`)
|
||||
sawUnscoped = sawUnscoped || strings.Contains(key, `user="bob"`)
|
||||
}
|
||||
require.True(t, sawScoped, "metric.a's scoped alias was not canonicalised: %v", got)
|
||||
require.True(t, sawUnscoped, "metric.a's scope leaked into metric.b: %v", got)
|
||||
require.Empty(t, warningStrings(set.Warnings()))
|
||||
|
||||
matched := q.Select(context.Background(), false, nil, append(baseMatchers,
|
||||
labels.MustNewMatcher(labels.MatchEqual, "tenant", "alice"),
|
||||
)...)
|
||||
require.Len(t, collectSeries(t, matched), 1)
|
||||
|
||||
names, anns, err := q.LabelNames(context.Background(), nil, baseMatchers...)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, warningStrings(anns))
|
||||
require.Contains(t, names, "tenant")
|
||||
require.Contains(t, names, "user")
|
||||
|
||||
values, anns, err := q.LabelValues(context.Background(), "tenant", nil, baseMatchers...)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, warningStrings(anns))
|
||||
require.Equal(t, []string{"alice"}, values)
|
||||
|
||||
cq, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = cq.Close() })
|
||||
chunks := cq.Select(context.Background(), false, nil, baseMatchers...)
|
||||
var chunkLabels []string
|
||||
for chunks.Next() {
|
||||
chunkLabels = append(chunkLabels, chunks.At().Labels().String())
|
||||
}
|
||||
require.NoError(t, chunks.Err())
|
||||
require.Len(t, chunkLabels, 2)
|
||||
require.Condition(t, func() bool {
|
||||
return strings.Contains(strings.Join(chunkLabels, "\n"), `tenant="alice"`) &&
|
||||
strings.Contains(strings.Join(chunkLabels, "\n"), `user="bob"`)
|
||||
}, "chunk variants used the wrong mappings: %v", chunkLabels)
|
||||
}
|
||||
|
||||
func TestOrderedConvergingMetricRenames(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.a: metric.current
|
||||
- rename_metrics:
|
||||
metric.b: metric.current
|
||||
`)
|
||||
semconv100 := []byte(`groups:
|
||||
- id: metric.metric.a
|
||||
type: metric
|
||||
metric_name: metric.a
|
||||
unit: s
|
||||
instrument: histogram
|
||||
- id: metric.metric.b
|
||||
type: metric
|
||||
metric_name: metric.b
|
||||
unit: s
|
||||
instrument: histogram
|
||||
`)
|
||||
semconv110 := metricSemconv("metric.current", "s", "histogram")
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.0.0": semconv100,
|
||||
"1.1.0": semconv110,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "metric.a", 1, 1.0, "source", "a")
|
||||
appendSeries(t, wrapped, "metric.b", 1, 2.0, "source", "b")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 2, "both ordered predecessors must resolve to the current metric: %v", got)
|
||||
require.Empty(t, warningStrings(set.Warnings()))
|
||||
}
|
||||
|
||||
func TestOrderedConvergingAttributeRenames(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.a: attr.current
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.b: attr.current
|
||||
`)
|
||||
semconv110 := []byte(`groups:
|
||||
- id: metric.metric.current
|
||||
type: metric
|
||||
metric_name: metric.current
|
||||
unit: s
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: attr.current
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.1.0": semconv110,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "metric.current", 1, 1.0, "attr.a", "match", "source", "a")
|
||||
appendSeries(t, wrapped, "metric.current", 1, 2.0, "attr.b", "match", "source", "b")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "attr.current", "match"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 2, "both ordered attribute predecessors must be queried: %v", got)
|
||||
for key := range got {
|
||||
require.Contains(t, key, `"attr.current"="match"`)
|
||||
require.NotContains(t, key, `"attr.a"=`)
|
||||
require.NotContains(t, key, `"attr.b"=`)
|
||||
}
|
||||
require.Empty(t, warningStrings(set.Warnings()))
|
||||
}
|
||||
|
||||
func TestAmbiguousHistoricalBranchIsRejectedIndependently(t *testing.T) {
|
||||
schema := []byte(`file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
old.bad: metric.current
|
||||
old.good: metric.current
|
||||
`)
|
||||
semconv100 := []byte(`groups:
|
||||
- id: metric.old.bad.one
|
||||
type: metric
|
||||
metric_name: old.bad
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
- id: metric.old.bad.two
|
||||
type: metric
|
||||
metric_name: old.bad
|
||||
unit: "By"
|
||||
instrument: counter
|
||||
- id: metric.old.good
|
||||
type: metric
|
||||
metric_name: old.good
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": schema,
|
||||
"1.0.0": semconv100,
|
||||
"1.1.0": metricSemconv("metric.current", "s", "histogram"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "old.bad", 1, 1.0, "source", "bad")
|
||||
appendSeries(t, wrapped, "old.good", 1, 2.0, "source", "good")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "metric.current"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 1, "the ambiguous sibling branch must be excluded: %v", got)
|
||||
for key := range got {
|
||||
require.Contains(t, key, `source="good"`)
|
||||
}
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "ambiguous rename branch")
|
||||
}
|
||||
|
||||
func TestAmbiguousMetricNameWarns(t *testing.T) {
|
||||
// Two groups declaring the same metric_name: the collision that previously
|
||||
// resolved to whichever group was parsed last, with no warning.
|
||||
ambiguous := []byte(`groups:
|
||||
- id: metric.shared.name
|
||||
type: metric
|
||||
metric_name: shared.name
|
||||
unit: "s"
|
||||
instrument: histogram
|
||||
attributes:
|
||||
- ref: http.response.status_code
|
||||
- id: metric.shared.name.other
|
||||
type: metric
|
||||
metric_name: shared.name
|
||||
unit: "{item}"
|
||||
instrument: updowncounter
|
||||
attributes:
|
||||
- ref: queue.name
|
||||
`)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "shared.name"),
|
||||
"1.1.0": ambiguous,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, "shared.name", 1, 7.0)
|
||||
appendSeries(t, wrapped, "old.name", 1, 9.0)
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
matchers := []*labels.Matcher{
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "shared.name"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
}
|
||||
|
||||
t.Run("Select", func(t *testing.T) {
|
||||
set := q.Select(context.Background(), false, nil, matchers...)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 1, "an ambiguous anchor must not traverse to old.name: %v", got)
|
||||
for _, value := range got {
|
||||
require.Equal(t, 7.0, value)
|
||||
}
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "declared by more than one group")
|
||||
})
|
||||
|
||||
t.Run("LabelNames", func(t *testing.T) {
|
||||
_, anns, err := q.LabelNames(context.Background(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
requireWarningsContain(t, warningStrings(anns), "declared by more than one group")
|
||||
})
|
||||
|
||||
t.Run("LabelValues", func(t *testing.T) {
|
||||
_, anns, err := q.LabelValues(context.Background(), "http.response.status_code", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
requireWarningsContain(t, warningStrings(anns), "declared by more than one group")
|
||||
})
|
||||
|
||||
cq, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = cq.Close() })
|
||||
|
||||
t.Run("ChunkSelect", func(t *testing.T) {
|
||||
set := cq.Select(context.Background(), false, nil, matchers...)
|
||||
var n int
|
||||
for set.Next() {
|
||||
n++
|
||||
}
|
||||
require.NoError(t, set.Err())
|
||||
require.Equal(t, 1, n, "an ambiguous anchor must use only the direct chunk variant")
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "declared by more than one group")
|
||||
})
|
||||
|
||||
t.Run("ChunkLabelNames", func(t *testing.T) {
|
||||
_, anns, err := cq.LabelNames(context.Background(), nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
requireWarningsContain(t, warningStrings(anns), "declared by more than one group")
|
||||
})
|
||||
|
||||
t.Run("ChunkLabelValues", func(t *testing.T) {
|
||||
_, anns, err := cq.LabelValues(context.Background(), "http.response.status_code", nil, matchers...)
|
||||
require.NoError(t, err)
|
||||
requireWarningsContain(t, warningStrings(anns), "declared by more than one group")
|
||||
})
|
||||
}
|
||||
|
||||
// TestChunkQuerierSurfacesWarnings checks the ChunkQuerier path annotates too,
|
||||
// since it fans out through the same resolver as Querier.
|
||||
func TestChunkQuerierSurfacesWarnings(t *testing.T) {
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "{item}", "updowncounter"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
appendSeries(t, wrapped, "new.name", 1, 7.0)
|
||||
|
||||
cq, err := wrapped.ChunkQuerier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = cq.Close() })
|
||||
|
||||
set := cq.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "new.name"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.1.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
var n int
|
||||
for set.Next() {
|
||||
n++
|
||||
}
|
||||
require.NoError(t, set.Err())
|
||||
require.Equal(t, 1, n, "the queried metric's own series must still be returned")
|
||||
|
||||
var found bool
|
||||
for _, w := range warningStrings(set.Warnings()) {
|
||||
if strings.Contains(w, "treating them as different metrics") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
require.True(t, found, "expected the chunk querier to surface the mis-linked rename warning")
|
||||
}
|
||||
|
||||
// recycledSchema retires old.name at 1.1.0 and hands the name to an unrelated
|
||||
// metric at 1.2.0, so the name has two eras and they need not mean the same thing.
|
||||
const recycledSchema = `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
versions:
|
||||
1.0.0:
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
old.name: new.name
|
||||
1.2.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
other.name: old.name
|
||||
`
|
||||
|
||||
// TestRenameValidationWithoutAnchorDeclaration covers querying a name the anchor
|
||||
// semconv does not declare, which is ordinary input here: the fan-out deliberately
|
||||
// supports asking for a name the anchor version has already renamed away.
|
||||
//
|
||||
// Corroboration used to switch itself off silently for exactly that query, which is
|
||||
// the case it exists for. The identity of the queried metric is instead taken from a
|
||||
// version where the name is the current one, and only where no version settles it is
|
||||
// the check skipped — and then said so.
|
||||
func TestRenameValidationWithoutAnchorDeclaration(t *testing.T) {
|
||||
t.Run("corroborates against the era that declares the queried name", func(t *testing.T) {
|
||||
// Semconv 1.1.0 knows only new.name, so the queried old.name is resolved
|
||||
// against 1.0.0, where it is current. The two disagree on unit and
|
||||
// instrument, so the rename joins unrelated metrics and must not merge.
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "{item}", "updowncounter"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "new.name", "old.name")
|
||||
|
||||
require.Empty(t, got, "the contradicted rename must not pull in the other metric's series, got %v", got)
|
||||
requireWarningsContain(t, warnings, "treating them as different metrics")
|
||||
})
|
||||
|
||||
t.Run("merges when the era that declares the queried name agrees", func(t *testing.T) {
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("old.name", "s", "histogram"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "new.name", "old.name")
|
||||
|
||||
require.Len(t, got, 1, "expected the renamed series under the queried name, got %v", got)
|
||||
for k := range got {
|
||||
require.Contains(t, k, `__name__="old.name"`)
|
||||
}
|
||||
require.Empty(t, warnings, "a corroborated rename must not warn")
|
||||
})
|
||||
|
||||
t.Run("warns when no version declares the queried name", func(t *testing.T) {
|
||||
// Nothing declares old.name anywhere, so there is no identity to check
|
||||
// hops against. The series still merge, but the caller is told the
|
||||
// corroboration did not run rather than left to assume it passed.
|
||||
got, warnings := selectRenamed(t, map[string][]byte{
|
||||
"registry.yaml": fmt.Appendf(nil, renameSchema, "old.name", "new.name"),
|
||||
"1.0.0": metricSemconv("unrelated.name", "s", "histogram"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
}, "new.name", "old.name")
|
||||
|
||||
require.Len(t, got, 1, "an unverifiable rename is still followed, got %v", got)
|
||||
requireWarningsContain(t, warnings, "without corroboration")
|
||||
})
|
||||
|
||||
t.Run("rejects when a disconnected identity reuses the queried name", func(t *testing.T) {
|
||||
// old.name is current at 1.0.0 and again at 1.2.0, as two different
|
||||
// metrics. Neither era can stand in for the other, so picking one would
|
||||
// check hops against an identity the caller never asked for.
|
||||
err := selectRenamedError(t, map[string][]byte{
|
||||
"registry.yaml": []byte(recycledSchema),
|
||||
"1.0.0": metricSemconv("old.name", "{item}", "updowncounter"),
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
"1.2.0": metricSemconv("old.name", "s", "histogram"),
|
||||
}, "new.name", "old.name")
|
||||
|
||||
require.ErrorContains(t, err, "semconv schema rename is ambiguous")
|
||||
})
|
||||
|
||||
t.Run("rejects reuse before checking an ambiguous era declaration", func(t *testing.T) {
|
||||
ambiguous := []byte(`groups:
|
||||
- id: metric.old.name.one
|
||||
type: metric
|
||||
metric_name: old.name
|
||||
unit: s
|
||||
instrument: histogram
|
||||
- id: metric.old.name.two
|
||||
type: metric
|
||||
metric_name: old.name
|
||||
unit: By
|
||||
instrument: counter
|
||||
`)
|
||||
err := selectRenamedError(t, map[string][]byte{
|
||||
"registry.yaml": []byte(recycledSchema),
|
||||
"1.0.0": ambiguous,
|
||||
"1.1.0": metricSemconv("new.name", "s", "histogram"),
|
||||
"1.2.0": metricSemconv("old.name", "s", "histogram"),
|
||||
}, "new.name", "old.name")
|
||||
|
||||
require.ErrorContains(t, err, "semconv schema rename is ambiguous")
|
||||
})
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+72
-1055
File diff suppressed because it is too large.
Load diff
@@ -1,83 +0,0 @@
|
||||
// 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 semconv
|
||||
|
||||
import (
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
)
|
||||
|
||||
type topKTestSeries struct {
|
||||
labels labels.Labels
|
||||
contributors int
|
||||
}
|
||||
|
||||
func (s *topKTestSeries) Labels() labels.Labels {
|
||||
return s.labels
|
||||
}
|
||||
|
||||
func collectTopKTestSeries(input []*topKTestSeries, limit int) ([]*topKTestSeries, int) {
|
||||
position := -1
|
||||
got := collectAndChainTopK(
|
||||
func() bool {
|
||||
position++
|
||||
return position < len(input)
|
||||
},
|
||||
func() *topKTestSeries { return input[position] },
|
||||
limit,
|
||||
func(series ...*topKTestSeries) *topKTestSeries {
|
||||
merged := &topKTestSeries{labels: series[0].labels}
|
||||
for _, item := range series {
|
||||
merged.contributors += item.contributors
|
||||
}
|
||||
return merged
|
||||
},
|
||||
)
|
||||
return got, position
|
||||
}
|
||||
|
||||
func TestCollectAndChainTopK(t *testing.T) {
|
||||
series := func(value string) *topKTestSeries {
|
||||
return &topKTestSeries{
|
||||
labels: labels.FromStrings("instance", value),
|
||||
contributors: 1,
|
||||
}
|
||||
}
|
||||
|
||||
got, drained := collectTopKTestSeries([]*topKTestSeries{
|
||||
series("z"), series("a"), series("c"), series("a"), series("b"), series("d"),
|
||||
}, 2)
|
||||
require.Equal(t, 6, drained)
|
||||
require.Equal(t, []labels.Labels{
|
||||
labels.FromStrings("instance", "a"),
|
||||
labels.FromStrings("instance", "b"),
|
||||
}, []labels.Labels{got[0].Labels(), got[1].Labels()})
|
||||
require.Equal(t, 2, got[0].contributors)
|
||||
require.Equal(t, 1, got[1].contributors)
|
||||
}
|
||||
|
||||
func TestCollectAndChainTopKDoesNotPreallocateFromLimit(t *testing.T) {
|
||||
input := []*topKTestSeries{{
|
||||
labels: labels.FromStrings("instance", "a"),
|
||||
contributors: 1,
|
||||
}}
|
||||
got, drained := collectTopKTestSeries(input, math.MaxInt)
|
||||
require.Equal(t, 1, drained)
|
||||
require.Equal(t, input, got)
|
||||
require.LessOrEqual(t, cap(got), len(input))
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage"
|
||||
)
|
||||
|
||||
// selectAt runs a fanned-out Select against the embedded registry, anchored at
|
||||
// semconv version anchor.
|
||||
func selectAt(t *testing.T, s storage.Storage, anchor, metricName string) storage.SeriesSet {
|
||||
t.Helper()
|
||||
q, err := s.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
return q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, metricName),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/"+anchor),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
}
|
||||
|
||||
// TestFanOutReachesEveryEraFromAnyAnchor walks the embedded registry's rename
|
||||
// chain (test.counter → test at 1.1.0 → test.v2 at 1.2.0) from valid anchors.
|
||||
//
|
||||
// Anchoring at the newest version happens to work with a truncated walk, because
|
||||
// then every rename is on the backward side; the oldest and middle anchors are what
|
||||
// expose a walk that mis-slices the version history or fails to chain across the
|
||||
// anchor. Each anchor must reach all three eras.
|
||||
func TestFanOutReachesEveryEraFromAnyAnchor(t *testing.T) {
|
||||
// eras names the attribute each era's series is expected to carry in the
|
||||
// result. Ordered schema changes determine the anchor-era name even when the
|
||||
// anchor semconv does not declare attributes for the queried metric.
|
||||
eras := func(attrs ...string) []string { return attrs }
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
anchor string
|
||||
queried string
|
||||
attrs []string
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "oldest anchor",
|
||||
anchor: "1.0.0",
|
||||
queried: "test.counter",
|
||||
attrs: eras("user", "user", "user"),
|
||||
description: "no version is at or before 1.0.0, so the whole chain has to be walked forward",
|
||||
},
|
||||
{
|
||||
name: "middle anchor",
|
||||
anchor: "1.1.0",
|
||||
queried: "test",
|
||||
attrs: eras("tenant", "tenant", "tenant"),
|
||||
description: "1.1.0 is walked backward and 1.2.0 forward",
|
||||
},
|
||||
{
|
||||
name: "newest anchor",
|
||||
anchor: "1.2.0",
|
||||
queried: "test.v2",
|
||||
attrs: eras("tenant", "tenant", "tenant"),
|
||||
description: "every rename is on the backward side",
|
||||
},
|
||||
{
|
||||
name: "middle anchor with a name that version renamed away",
|
||||
anchor: "1.1.0",
|
||||
queried: "test.counter",
|
||||
attrs: eras("user", "user", "user"),
|
||||
description: "test.counter is the name 1.1.0 renamed, so the backward walk crosses " +
|
||||
"that rename and the forward walk has to continue from where it landed",
|
||||
},
|
||||
{
|
||||
name: "newest anchor with the name that version renamed away",
|
||||
anchor: "1.2.0",
|
||||
queried: "test",
|
||||
attrs: eras("tenant", "tenant", "tenant"),
|
||||
description: "the retirement revision is oriented forward when it is the exact anchor",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wrapped, _ := newAwareStorage(t)
|
||||
// One series per naming era, distinguished by attribute value so the
|
||||
// three stay distinct series rather than merging into one.
|
||||
appendSeries(t, wrapped, "test.counter", 1, 1.0, "user", "a")
|
||||
appendSeries(t, wrapped, "test", 1, 2.0, "tenant", "b")
|
||||
appendSeries(t, wrapped, "test.v2", 1, 3.0, "tenant", "c")
|
||||
|
||||
got := collectSeries(t, selectAt(t, wrapped, tc.anchor, tc.queried))
|
||||
|
||||
require.Len(t, got, 3, "%s: got %v", tc.description, got)
|
||||
for i, value := range []string{"a", "b", "c"} {
|
||||
require.Contains(t, got,
|
||||
labels.FromStrings(model.MetricNameLabel, tc.queried, tc.attrs[i], value).String())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFanOutStopsAtRetirementBeforeLaterAnchor(t *testing.T) {
|
||||
wrapped, _ := newAwareStorage(t)
|
||||
appendSeries(t, wrapped, "test.counter", 1, 1.0, "user", "a")
|
||||
appendSeries(t, wrapped, "test", 1, 2.0, "tenant", "b")
|
||||
appendSeries(t, wrapped, "test.v2", 1, 3.0, "tenant", "c")
|
||||
|
||||
set := selectAt(t, wrapped, "1.2.0", "test.counter")
|
||||
got := collectSeries(t, set)
|
||||
|
||||
require.Equal(t, map[string]float64{
|
||||
labels.FromStrings(model.MetricNameLabel, "test.counter", "user", "a").String(): 1,
|
||||
}, got)
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "metric lifecycle boundary")
|
||||
}
|
||||
Vendored
-40
@@ -1,40 +0,0 @@
|
||||
file_format: "1.1.0"
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
|
||||
versions:
|
||||
1.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
http.method: http.request.method
|
||||
http.status_code: http.response.status_code
|
||||
url.path: url.full
|
||||
apply_to_metrics:
|
||||
- http.server.duration
|
||||
- http.server.request.count
|
||||
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
process.cpu.state: cpu.mode
|
||||
apply_to_metrics:
|
||||
- process.cpu.time
|
||||
|
||||
groups:
|
||||
- id: metric.http.server.duration
|
||||
type: metric
|
||||
metric_name: http.server.duration
|
||||
instrument: histogram
|
||||
unit: s
|
||||
|
||||
- id: metric.http.server.request.count
|
||||
type: metric
|
||||
metric_name: http.server.request.count
|
||||
instrument: counter
|
||||
unit: "{request}"
|
||||
|
||||
- id: metric.process.cpu.time
|
||||
type: metric
|
||||
metric_name: process.cpu.time
|
||||
instrument: counter
|
||||
unit: s
|
||||
@@ -1,7 +0,0 @@
|
||||
file_format: "2.0.0"
|
||||
schema_url: https://example.com/schemas/2.0.0
|
||||
|
||||
versions:
|
||||
2.0.0:
|
||||
metrics:
|
||||
changes: []
|
||||
@@ -1,17 +0,0 @@
|
||||
file_format: "1.1.0"
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
|
||||
versions:
|
||||
1.0.0:
|
||||
all:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
global.old: global.new
|
||||
metrics:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
metric.old: metric.new
|
||||
apply_to_metrics:
|
||||
- my.metric
|
||||
@@ -1,20 +0,0 @@
|
||||
file_format: "1.1.0"
|
||||
schema_url: https://example.com/schemas/1.1.0
|
||||
|
||||
versions:
|
||||
1.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.v1: metric.v2
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.v1: attr.v2
|
||||
1.1.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
metric.v2: metric.v3
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
attr.v2: attr.v3
|
||||
@@ -1,10 +0,0 @@
|
||||
file_format: "1.1.0"
|
||||
schema_url: https://example.com/schemas/1.0.0
|
||||
|
||||
versions:
|
||||
1.0.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
old.metric.name: new.metric.name
|
||||
another.old.metric: another.new.metric
|
||||
@@ -1,766 +0,0 @@
|
||||
file_format: 1.1.0
|
||||
schema_url: https://opentelemetry.io/schemas/1.44.0
|
||||
versions:
|
||||
1.44.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
container.memory.paging.faults: container.paging.faults
|
||||
k8s.node.memory.paging.faults: k8s.node.paging.faults
|
||||
k8s.pod.memory.paging.faults: k8s.pod.paging.faults
|
||||
1.43.0:
|
||||
1.42.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
v8js.memory.heap.limit: v8js.memory.heap.space.size
|
||||
1.41.1:
|
||||
1.41.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
k8s.container.cpu.limit: k8s.container.cpu.limit.desired
|
||||
k8s.container.cpu.limit_utilization: k8s.container.cpu.limit.utilization
|
||||
k8s.container.cpu.request: k8s.container.cpu.request.desired
|
||||
k8s.container.cpu.request_utilization: k8s.container.cpu.request.utilization
|
||||
k8s.container.memory.limit: k8s.container.memory.limit.desired
|
||||
k8s.container.memory.request: k8s.container.memory.request.desired
|
||||
1.40.0:
|
||||
all:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
feature_flag.evaluation.error.message: feature_flag.error.message
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
system.memory.shared: system.memory.linux.shared
|
||||
1.39.0:
|
||||
all:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
linux.memory.slab.state: system.memory.linux.slab.state
|
||||
peer.service: service.peer.name
|
||||
rpc.connect_rpc.error_code: rpc.response.status_code
|
||||
rpc.connect_rpc.request.metadata: rpc.request.metadata
|
||||
rpc.connect_rpc.response.metadata: rpc.response.metadata
|
||||
rpc.grpc.request.metadata: rpc.request.metadata
|
||||
rpc.grpc.response.metadata: rpc.response.metadata
|
||||
rpc.jsonrpc.request_id: jsonrpc.request.id
|
||||
rpc.jsonrpc.version: jsonrpc.protocol.version
|
||||
rpc.system: rpc.system.name
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
process.open_file_descriptor.count: process.unix.file_descriptor.count
|
||||
system.linux.memory.available: system.memory.linux.available
|
||||
system.linux.memory.slab.usage: system.memory.linux.slab.usage
|
||||
1.38.0:
|
||||
all:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
process.context_switch_type: process.context_switch.type
|
||||
process.paging.fault_type: system.paging.fault.type
|
||||
system.cpu.logical_number: cpu.logical_number
|
||||
system.paging.type: system.paging.fault.type
|
||||
system.process.status: process.state
|
||||
system.processes.status: process.state
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
k8s.cronjob.active_jobs: k8s.cronjob.job.active
|
||||
k8s.daemonset.current_scheduled_nodes: k8s.daemonset.node.current_scheduled
|
||||
k8s.daemonset.desired_scheduled_nodes: k8s.daemonset.node.desired_scheduled
|
||||
k8s.daemonset.misscheduled_nodes: k8s.daemonset.node.misscheduled
|
||||
k8s.daemonset.ready_nodes: k8s.daemonset.node.ready
|
||||
k8s.deployment.available_pods: k8s.deployment.pod.available
|
||||
k8s.deployment.desired_pods: k8s.deployment.pod.desired
|
||||
k8s.hpa.current_pods: k8s.hpa.pod.current
|
||||
k8s.hpa.desired_pods: k8s.hpa.pod.desired
|
||||
k8s.hpa.max_pods: k8s.hpa.pod.max
|
||||
k8s.hpa.min_pods: k8s.hpa.pod.min
|
||||
k8s.job.active_pods: k8s.job.pod.active
|
||||
k8s.job.desired_successful_pods: k8s.job.pod.desired_successful
|
||||
k8s.job.failed_pods: k8s.job.pod.failed
|
||||
k8s.job.max_parallel_pods: k8s.job.pod.max_parallel
|
||||
k8s.job.successful_pods: k8s.job.pod.successful
|
||||
k8s.node.allocatable.cpu: k8s.node.cpu.allocatable
|
||||
k8s.node.allocatable.ephemeral_storage: k8s.node.ephemeral_storage.allocatable
|
||||
k8s.node.allocatable.memory: k8s.node.memory.allocatable
|
||||
k8s.node.allocatable.pods: k8s.node.pod.allocatable
|
||||
k8s.replicaset.available_pods: k8s.replicaset.pod.available
|
||||
k8s.replicaset.desired_pods: k8s.replicaset.pod.desired
|
||||
k8s.replication_controller.available_pods: k8s.replicationcontroller.pod.available
|
||||
k8s.replication_controller.desired_pods: k8s.replicationcontroller.pod.desired
|
||||
k8s.replicationcontroller.available_pods: k8s.replicationcontroller.pod.available
|
||||
k8s.replicationcontroller.desired_pods: k8s.replicationcontroller.pod.desired
|
||||
k8s.statefulset.current_pods: k8s.statefulset.pod.current
|
||||
k8s.statefulset.desired_pods: k8s.statefulset.pod.desired
|
||||
k8s.statefulset.ready_pods: k8s.statefulset.pod.ready
|
||||
k8s.statefulset.updated_pods: k8s.statefulset.pod.updated
|
||||
v8js.heap.space.available_size: v8js.memory.heap.space.available_size
|
||||
v8js.heap.space.physical_size: v8js.memory.heap.space.physical_size
|
||||
1.37.0:
|
||||
all:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
android.state: android.app.state
|
||||
container.runtime: container.runtime.name
|
||||
enduser.role: user.roles
|
||||
gen_ai.openai.request.service_tier: openai.request.service_tier
|
||||
gen_ai.openai.response.service_tier: openai.response.service_tier
|
||||
gen_ai.openai.response.system_fingerprint: openai.response.system_fingerprint
|
||||
gen_ai.system: gen_ai.provider.name
|
||||
ios.state: ios.app.state
|
||||
1.36.0:
|
||||
1.35.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1698
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
az.namespace: azure.resource_provider.namespace
|
||||
az.service_request_id: azure.service.request.id
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/issues/1800
|
||||
- rename_metrics:
|
||||
system.network.connections: system.network.connection.count
|
||||
1.34.0:
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/2295
|
||||
- rename_metrics:
|
||||
cpu.time: system.cpu.time
|
||||
cpu.utilization: system.cpu.utilization
|
||||
cpu.frequency: system.cpu.frequency
|
||||
1.33.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1982
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
feature_flag.provider_name: feature_flag.provider.name
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1994
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
feature_flag.evaluation.error.message: error.message
|
||||
1.32.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1989
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
feature_flag.evaluation.reason: feature_flag.result.reason
|
||||
feature_flag.variant: feature_flag.result.variant
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/2042
|
||||
- rename_metrics:
|
||||
otel.sdk.span.live.count: otel.sdk.span.live
|
||||
otel.sdk.span.ended.count: otel.sdk.span.ended
|
||||
otel.sdk.processor.span.processed.count: otel.sdk.processor.span.processed
|
||||
otel.sdk.exporter.span.inflight.count: otel.sdk.exporter.span.inflight
|
||||
otel.sdk.exporter.span.exported.count: otel.sdk.exporter.span.exported
|
||||
1.31.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1880
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
android.state: android.app.state
|
||||
io.state: ios.app.state
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
k8s.replication_controller.desired_pods: k8s.replicationcontroller.desired_pods
|
||||
k8s.replication_controller.available_pods: k8s.replicationcontroller.available_pods
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1896
|
||||
- rename_metrics:
|
||||
system.cpu.time: cpu.time
|
||||
system.cpu.utilization: cpu.utilization
|
||||
system.cpu.frequency: cpu.frequency
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1896
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
system.cpu.logical_number: cpu.logical_number
|
||||
1.30.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1632
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
gen_ai.openai.request.seed: gen_ai.request.seed
|
||||
system.network.state: network.connection.state
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1624
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
code.function: code.function.name
|
||||
code.filepath: code.file.path
|
||||
code.lineno: code.line.number
|
||||
code.column: code.column.number
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1734
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.system: db.system.name
|
||||
db.cassandra.coordinator.dc: cassandra.coordinator.dc
|
||||
db.cassandra.coordinator.id: cassandra.coordinator.id
|
||||
db.cassandra.consistency_level: cassandra.consistency.level
|
||||
db.cassandra.idempotence: cassandra.query.idempotent
|
||||
db.cassandra.page_size: cassandra.page.size
|
||||
db.cassandra.speculative_execution_count: cassandra.speculative_execution.count
|
||||
db.cosmosdb.client_id: azure.client.id
|
||||
db.cosmosdb.connection_mode: azure.cosmosdb.connection.mode
|
||||
db.cosmosdb.consistency_level: azure.cosmosdb.consistency.level
|
||||
db.cosmosdb.request_charge: azure.cosmosdb.operation.request_charge
|
||||
db.cosmosdb.request_content_length: azure.cosmosdb.request.body.size
|
||||
db.cosmosdb.regions_contacted: azure.cosmosdb.operation.contacted_regions
|
||||
db.cosmosdb.sub_status_code: azure.cosmosdb.response.sub_status_code
|
||||
db.elasticsearch.node.name: elasticsearch.node.name
|
||||
# db.elasticsearch.path_parts is a template attribute, schema transformation
|
||||
# does not support it, adding as a comment for consistency
|
||||
# db.elasticsearch.path_parts.<key> -> db.operation.parameter.<key>
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
db.client.cosmosdb.operation.request_charge: azure.cosmosdb.client.operation.request_charge
|
||||
db.client.cosmosdb.active_instance.count: azure.cosmosdb.client.active_instance.count
|
||||
|
||||
1.29.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1520
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
process.executable.build_id.profiling: process.executable.build_id.htlhash
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1383
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
vcs.repository.change.id: vcs.change.id
|
||||
vcs.repository.change.title: vcs.change.title
|
||||
vcs.repository.ref.name: vcs.ref.head.name
|
||||
vcs.repository.ref.revision: vcs.ref.head.revision
|
||||
vcs.repository.ref.type: vcs.ref.head.type
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1492
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
system.device: network.interface.name
|
||||
apply_to_metrics:
|
||||
- container.network.io
|
||||
- system.network.dropped
|
||||
- system.network.errors
|
||||
- system.network.io
|
||||
- system.network.connections
|
||||
1.28.0:
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1422
|
||||
- rename_metrics:
|
||||
messaging.client.published.messages: messaging.client.sent.messages
|
||||
1.27.0:
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1216
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
tls.client.server_name: server.address
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1075
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
deployment.environment: deployment.environment.name
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1245
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.kafka.message.offset: messaging.kafka.offset
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/815
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.kafka.consumer.group: messaging.consumer.group.name
|
||||
messaging.rocketmq.client_group: messaging.consumer.group.name
|
||||
messaging.eventhubs.consumer.group: messaging.consumer.group.name
|
||||
messaging.servicebus.destination.subscription_name: messaging.destination.subscription.name
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1200
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
gen_ai.usage.completion_tokens: gen_ai.usage.output_tokens
|
||||
gen_ai.usage.prompt_tokens: gen_ai.usage.input_tokens
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1002
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.elasticsearch.cluster.name: db.namespace
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1125
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.client.connections.state: db.client.connection.state
|
||||
apply_to_metrics:
|
||||
- db.client.connection.count
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.client.connections.pool.name: db.client.connection.pool.name
|
||||
apply_to_metrics:
|
||||
- db.client.connection.count
|
||||
- db.client.connection.idle.max
|
||||
- db.client.connection.idle.min
|
||||
- db.client.connection.max
|
||||
- db.client.connection.pending_requests
|
||||
- db.client.connection.timeouts
|
||||
- db.client.connection.create_time
|
||||
- db.client.connection.wait_time
|
||||
- db.client.connection.use_time
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1006
|
||||
- rename_metrics:
|
||||
messaging.publish.messages: messaging.client.published.messages
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1026
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
system.cpu.state: cpu.mode
|
||||
process.cpu.state: cpu.mode
|
||||
container.cpu.state: cpu.mode
|
||||
apply_to_metrics:
|
||||
- system.cpu.time
|
||||
- system.cpu.utilization
|
||||
- process.cpu.time
|
||||
- process.cpu.utilization
|
||||
- container.cpu.time
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/1265
|
||||
- rename_metrics:
|
||||
jvm.buffer.memory.usage: jvm.buffer.memory.used
|
||||
1.26.0:
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/966
|
||||
- rename_metrics:
|
||||
db.client.connections.usage: db.client.connection.count
|
||||
db.client.connections.idle.max: db.client.connection.idle.max
|
||||
db.client.connections.idle.min: db.client.connection.idle.min
|
||||
db.client.connections.max: db.client.connection.max
|
||||
db.client.connections.pending_requests: db.client.connection.pending_requests
|
||||
db.client.connections.timeouts: db.client.connection.timeouts
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/948
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.client_id: messaging.client.id
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/909
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
state: db.client.connections.state
|
||||
apply_to_metrics:
|
||||
- db.client.connections.usage
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
pool.name: db.client.connections.pool.name
|
||||
apply_to_metrics:
|
||||
- db.client.connections.usage
|
||||
- db.client.connections.idle.max
|
||||
- db.client.connections.idle.min
|
||||
- db.client.connections.max
|
||||
- db.client.connections.pending_requests
|
||||
- db.client.connections.timeouts
|
||||
- db.client.connections.create_time
|
||||
- db.client.connections.wait_time
|
||||
- db.client.connections.use_time
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/731
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
enduser.id: user.id
|
||||
|
||||
1.25.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/911
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.name: db.namespace
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/870
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.sql.table: db.collection.name
|
||||
db.mongodb.collection: db.collection.name
|
||||
db.cosmosdb.container: db.collection.name
|
||||
db.cassandra.table: db.collection.name
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/798
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.kafka.destination.partition: messaging.destination.partition.id
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/875
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.operation: db.operation.name
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/913
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.operation: messaging.operation.type
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/866
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.statement: db.query.text
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/484
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
system.processes.status: system.process.status
|
||||
apply_to_metrics:
|
||||
- system.processes.count
|
||||
- rename_metrics:
|
||||
system.processes.count: system.process.count
|
||||
system.processes.created: system.process.created
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/625
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
container.labels: container.label
|
||||
k8s.pod.labels: k8s.pod.label
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/330
|
||||
- rename_metrics:
|
||||
process.threads: process.thread.count
|
||||
process.open_file_descriptors: process.open_file_descriptor.count
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
state: process.cpu.state
|
||||
apply_to_metrics:
|
||||
- process.cpu.time
|
||||
- process.cpu.utilization
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
direction: disk.io.direction
|
||||
apply_to_metrics:
|
||||
- process.disk.io
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
type: process.context_switch_type
|
||||
apply_to_metrics:
|
||||
- process.context_switches
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
direction: network.io.direction
|
||||
apply_to_metrics:
|
||||
- process.network.io
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
type: process.paging.fault_type
|
||||
apply_to_metrics:
|
||||
- process.paging.faults
|
||||
all:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/854
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
message.type: rpc.message.type
|
||||
message.id: rpc.message.id
|
||||
message.compressed_size: rpc.message.compressed_size
|
||||
message.uncompressed_size: rpc.message.uncompressed_size
|
||||
|
||||
1.24.0:
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/536
|
||||
- rename_metrics:
|
||||
jvm.memory.usage: jvm.memory.used
|
||||
jvm.memory.usage_after_last_gc: jvm.memory.used_after_last_gc
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/530
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
system.network.io.direction: network.io.direction
|
||||
system.disk.io.direction: disk.io.direction
|
||||
1.23.1:
|
||||
1.23.0:
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/20
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
thread.daemon: jvm.thread.daemon
|
||||
apply_to_metrics:
|
||||
- jvm.thread.count
|
||||
1.22.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/229
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.message.payload_size_bytes: messaging.message.body.size
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/374
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
http.resend_count: http.request.resend_count
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/224
|
||||
- rename_metrics:
|
||||
http.client.duration: http.client.request.duration
|
||||
http.server.duration: http.server.request.duration
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/241
|
||||
- rename_metrics:
|
||||
process.runtime.jvm.memory.usage: jvm.memory.usage
|
||||
process.runtime.jvm.memory.committed: jvm.memory.committed
|
||||
process.runtime.jvm.memory.limit: jvm.memory.limit
|
||||
process.runtime.jvm.memory.usage_after_last_gc: jvm.memory.usage_after_last_gc
|
||||
process.runtime.jvm.gc.duration: jvm.gc.duration
|
||||
# also https://github.com/open-telemetry/semantic-conventions/pull/252
|
||||
process.runtime.jvm.threads.count: jvm.thread.count
|
||||
# also https://github.com/open-telemetry/semantic-conventions/pull/252
|
||||
process.runtime.jvm.classes.loaded: jvm.class.loaded
|
||||
# also https://github.com/open-telemetry/semantic-conventions/pull/252
|
||||
process.runtime.jvm.classes.unloaded: jvm.class.unloaded
|
||||
# also https://github.com/open-telemetry/semantic-conventions/pull/252
|
||||
# and https://github.com/open-telemetry/semantic-conventions/pull/60
|
||||
process.runtime.jvm.classes.current_loaded: jvm.class.count
|
||||
process.runtime.jvm.cpu.time: jvm.cpu.time
|
||||
process.runtime.jvm.cpu.recent_utilization: jvm.cpu.recent_utilization
|
||||
process.runtime.jvm.memory.init: jvm.memory.init
|
||||
process.runtime.jvm.system.cpu.utilization: jvm.system.cpu.utilization
|
||||
process.runtime.jvm.system.cpu.load_1m: jvm.system.cpu.load_1m
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/253
|
||||
process.runtime.jvm.buffer.usage: jvm.buffer.memory.usage
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/253
|
||||
process.runtime.jvm.buffer.limit: jvm.buffer.memory.limit
|
||||
process.runtime.jvm.buffer.count: jvm.buffer.count
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/20
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
type: jvm.memory.type
|
||||
pool: jvm.memory.pool.name
|
||||
apply_to_metrics:
|
||||
- jvm.memory.usage
|
||||
- jvm.memory.committed
|
||||
- jvm.memory.limit
|
||||
- jvm.memory.usage_after_last_gc
|
||||
- jvm.memory.init
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
name: jvm.gc.name
|
||||
action: jvm.gc.action
|
||||
apply_to_metrics:
|
||||
- jvm.gc.duration
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
daemon: thread.daemon
|
||||
apply_to_metrics:
|
||||
- jvm.threads.count
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
pool: jvm.buffer.pool.name
|
||||
apply_to_metrics:
|
||||
- jvm.buffer.memory.usage
|
||||
- jvm.buffer.memory.limit
|
||||
- jvm.buffer.count
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/89
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
state: system.cpu.state
|
||||
cpu: system.cpu.logical_number
|
||||
apply_to_metrics:
|
||||
- system.cpu.time
|
||||
- system.cpu.utilization
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
state: system.memory.state
|
||||
apply_to_metrics:
|
||||
- system.memory.usage
|
||||
- system.memory.utilization
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
state: system.paging.state
|
||||
apply_to_metrics:
|
||||
- system.paging.usage
|
||||
- system.paging.utilization
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
type: system.paging.type
|
||||
direction: system.paging.direction
|
||||
apply_to_metrics:
|
||||
- system.paging.faults
|
||||
- system.paging.operations
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
device: system.device
|
||||
direction: system.disk.direction
|
||||
apply_to_metrics:
|
||||
- system.disk.io
|
||||
- system.disk.operations
|
||||
- system.disk.io_time
|
||||
- system.disk.operation_time
|
||||
- system.disk.merged
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
device: system.device
|
||||
state: system.filesystem.state
|
||||
type: system.filesystem.type
|
||||
mode: system.filesystem.mode
|
||||
mountpoint: system.filesystem.mountpoint
|
||||
apply_to_metrics:
|
||||
- system.filesystem.usage
|
||||
- system.filesystem.utilization
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
device: system.device
|
||||
direction: system.network.direction
|
||||
protocol: network.protocol
|
||||
state: system.network.state
|
||||
apply_to_metrics:
|
||||
- system.network.dropped
|
||||
- system.network.packets
|
||||
- system.network.errors
|
||||
- system.network.io
|
||||
- system.network.connections
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
status: system.processes.status
|
||||
apply_to_metrics:
|
||||
- system.processes.count
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/247
|
||||
- rename_metrics:
|
||||
http.server.request.size: http.server.request.body.size
|
||||
http.server.response.size: http.server.response.body.size
|
||||
resources:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/178
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
telemetry.auto.version: telemetry.distro.version
|
||||
1.21.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3336
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.kafka.client_id: messaging.client_id
|
||||
messaging.rocketmq.client_id: messaging.client_id
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3402
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
# net.peer.(name|port) attributes were usually populated on client side
|
||||
# so they should be usually translated to server.(address|port)
|
||||
# net.host.* attributes were only populated on server side
|
||||
net.host.name: server.address
|
||||
net.host.port: server.port
|
||||
# was only populated on client side
|
||||
net.sock.peer.name: server.socket.domain
|
||||
# net.sock.peer.(addr|port) mapping is not possible
|
||||
# since they applied to both client and server side
|
||||
# were only populated on server side
|
||||
net.sock.host.addr: server.socket.address
|
||||
net.sock.host.port: server.socket.port
|
||||
http.client_ip: client.address
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3426
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
net.protocol.name: network.protocol.name
|
||||
net.protocol.version: network.protocol.version
|
||||
net.host.connection.type: network.connection.type
|
||||
net.host.connection.subtype: network.connection.subtype
|
||||
net.host.carrier.name: network.carrier.name
|
||||
net.host.carrier.mcc: network.carrier.mcc
|
||||
net.host.carrier.mnc: network.carrier.mnc
|
||||
net.host.carrier.icc: network.carrier.icc
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3355
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
http.method: http.request.method
|
||||
http.status_code: http.response.status_code
|
||||
http.scheme: url.scheme
|
||||
http.url: url.full
|
||||
http.request_content_length: http.request.body.size
|
||||
http.response_content_length: http.response.body.size
|
||||
metrics:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/semantic-conventions/pull/53
|
||||
- rename_metrics:
|
||||
process.runtime.jvm.cpu.utilization: process.runtime.jvm.cpu.recent_utilization
|
||||
1.20.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3272
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
net.app.protocol.name: net.protocol.name
|
||||
net.app.protocol.version: net.protocol.version
|
||||
1.19.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3209
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
faas.execution: faas.invocation_id
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3188
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
faas.id: cloud.resource_id
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3190
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
http.user_agent: user_agent.original
|
||||
resources:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/3190
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
browser.user_agent: user_agent.original
|
||||
1.18.0:
|
||||
1.17.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/2957
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
messaging.consumer_id: messaging.consumer.id
|
||||
messaging.protocol: net.app.protocol.name
|
||||
messaging.protocol_version: net.app.protocol.version
|
||||
messaging.destination: messaging.destination.name
|
||||
messaging.temp_destination: messaging.destination.temporary
|
||||
messaging.destination_kind: messaging.destination.kind
|
||||
messaging.message_id: messaging.message.id
|
||||
messaging.conversation_id: messaging.message.conversation_id
|
||||
messaging.message_payload_size_bytes: messaging.message.payload_size_bytes
|
||||
messaging.message_payload_compressed_size_bytes: messaging.message.payload_compressed_size_bytes
|
||||
messaging.rabbitmq.routing_key: messaging.rabbitmq.destination.routing_key
|
||||
messaging.kafka.message_key: messaging.kafka.message.key
|
||||
messaging.kafka.partition: messaging.kafka.destination.partition
|
||||
messaging.kafka.tombstone: messaging.kafka.message.tombstone
|
||||
messaging.rocketmq.message_type: messaging.rocketmq.message.type
|
||||
messaging.rocketmq.message_tag: messaging.rocketmq.message.tag
|
||||
messaging.rocketmq.message_keys: messaging.rocketmq.message.keys
|
||||
messaging.kafka.consumer_group: messaging.kafka.consumer.group
|
||||
1.16.0:
|
||||
1.15.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/2743
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
http.retry_count: http.resend_count
|
||||
1.14.0:
|
||||
1.13.0:
|
||||
spans:
|
||||
changes:
|
||||
# https://github.com/open-telemetry/opentelemetry-specification/pull/2614
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
net.peer.ip: net.sock.peer.addr
|
||||
net.host.ip: net.sock.host.addr
|
||||
1.12.0:
|
||||
1.11.0:
|
||||
1.10.0:
|
||||
1.9.0:
|
||||
1.8.0:
|
||||
spans:
|
||||
changes:
|
||||
- rename_attributes:
|
||||
attribute_map:
|
||||
db.cassandra.keyspace: db.name
|
||||
db.hbase.namespace: db.name
|
||||
1.7.0:
|
||||
1.6.1:
|
||||
1.5.0:
|
||||
1.4.0:
|
||||
@@ -1,293 +0,0 @@
|
||||
# Verbatim excerpts of the HTTP attribute and metric groups from
|
||||
# open-telemetry/semantic-conventions v1.21.0 : model/http-common.yaml
|
||||
# open-telemetry/semantic-conventions v1.21.0 : model/metrics/http.yaml
|
||||
# open-telemetry/semantic-conventions v1.21.0 : model/metrics/process-runtime-jvm-metrics-experimental.yaml
|
||||
# Combined under one groups key so inheritance is exercised exactly as it is in
|
||||
# the upstream registry.
|
||||
groups:
|
||||
- id: attributes.http.common
|
||||
type: attribute_group
|
||||
brief: "Describes HTTP attributes."
|
||||
prefix: http
|
||||
attributes:
|
||||
- id: request.method
|
||||
type:
|
||||
allow_custom_values: true
|
||||
members:
|
||||
- id: connect
|
||||
value: "CONNECT"
|
||||
brief: 'CONNECT method.'
|
||||
- id: delete
|
||||
value: "DELETE"
|
||||
brief: 'DELETE method.'
|
||||
- id: get
|
||||
value: "GET"
|
||||
brief: 'GET method.'
|
||||
- id: head
|
||||
value: "HEAD"
|
||||
brief: 'HEAD method.'
|
||||
- id: options
|
||||
value: "OPTIONS"
|
||||
brief: 'OPTIONS method.'
|
||||
- id: patch
|
||||
value: "PATCH"
|
||||
brief: 'PATCH method.'
|
||||
- id: post
|
||||
value: "POST"
|
||||
brief: 'POST method.'
|
||||
- id: put
|
||||
value: "PUT"
|
||||
brief: 'PUT method.'
|
||||
- id: trace
|
||||
value: "TRACE"
|
||||
brief: 'TRACE method.'
|
||||
- id: other
|
||||
value: "_OTHER"
|
||||
brief: 'Any HTTP method that the instrumentation has no prior knowledge of.'
|
||||
requirement_level: required
|
||||
brief: 'HTTP request method.'
|
||||
examples: ["GET", "POST", "HEAD"]
|
||||
note: |
|
||||
HTTP request method value SHOULD be "known" to the instrumentation.
|
||||
By default, this convention defines "known" methods as the ones listed in [RFC9110](https://www.rfc-editor.org/rfc/rfc9110.html#name-methods)
|
||||
and the PATCH method defined in [RFC5789](https://www.rfc-editor.org/rfc/rfc5789.html).
|
||||
|
||||
If the HTTP request method is not known to instrumentation, it MUST set the `http.request.method` attribute to `_OTHER` and, except if reporting a metric, MUST
|
||||
set the exact method received in the request line as value of the `http.request.method_original` attribute.
|
||||
|
||||
If the HTTP instrumentation could end up converting valid HTTP request methods to `_OTHER`, then it MUST provide a way to override
|
||||
the list of known HTTP methods. If this override is done via environment variable, then the environment variable MUST be named
|
||||
OTEL_INSTRUMENTATION_HTTP_KNOWN_METHODS and support a comma-separated list of case-sensitive known HTTP methods
|
||||
(this list MUST be a full override of the default known method, it is not a list of known methods in addition to the defaults).
|
||||
|
||||
HTTP method names are case-sensitive and `http.request.method` attribute value MUST match a known HTTP method name exactly.
|
||||
Instrumentations for specific web frameworks that consider HTTP methods to be case insensitive, SHOULD populate a canonical equivalent.
|
||||
Tracing instrumentations that do so, MUST also set `http.request.method_original` to the original value.
|
||||
- id: response.status_code
|
||||
type: int
|
||||
requirement_level:
|
||||
conditionally_required: If and only if one was received/sent.
|
||||
brief: '[HTTP response status code](https://tools.ietf.org/html/rfc7231#section-6).'
|
||||
examples: [200]
|
||||
- ref: network.protocol.name
|
||||
examples: ['http', 'spdy']
|
||||
requirement_level:
|
||||
recommended: if not default (`http`).
|
||||
- ref: network.protocol.version
|
||||
examples: ['1.0', '1.1', '2.0']
|
||||
|
||||
- id: attributes.http.client
|
||||
prefix: http
|
||||
type: attribute_group
|
||||
brief: 'HTTP Client attributes'
|
||||
attributes:
|
||||
- ref: server.address
|
||||
requirement_level: required
|
||||
brief: >
|
||||
Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if capturing it would require an extra DNS lookup.
|
||||
- ref: server.port
|
||||
requirement_level:
|
||||
conditionally_required: If not default (`80` for `http` scheme, `443` for `https`).
|
||||
brief: >
|
||||
Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.
|
||||
note: >
|
||||
When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match
|
||||
URI port identifier, otherwise it MUST match `Host` header port identifier.
|
||||
|
||||
- id: attributes.http.server
|
||||
prefix: http
|
||||
type: attribute_group
|
||||
brief: 'HTTP Server attributes'
|
||||
attributes:
|
||||
- id: route
|
||||
type: string
|
||||
requirement_level:
|
||||
conditionally_required: If and only if it's available
|
||||
brief: >
|
||||
The matched route (path template in the format used by the respective server framework). See note below
|
||||
examples: ['/users/:userID?', '{controller}/{action}/{id?}']
|
||||
note: >
|
||||
MUST NOT be populated when this is not supported by the HTTP server framework as the route attribute should have low-cardinality and the URI path can NOT substitute it.
|
||||
|
||||
SHOULD include the [application root](/docs/http/http-spans.md#http-server-definitions) if there is one.
|
||||
- ref: server.address
|
||||
brief: >
|
||||
Name of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only
|
||||
include host identifier.
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup.
|
||||
|
||||
- ref: server.port
|
||||
brief: >
|
||||
Port of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host.
|
||||
- Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Port identifier of the `Host` header
|
||||
- ref: url.scheme
|
||||
requirement_level: required
|
||||
examples: ["http", "https"]
|
||||
- id: metric_attributes.http.server
|
||||
type: attribute_group
|
||||
brief: 'HTTP server attributes'
|
||||
extends: attributes.http.server
|
||||
attributes:
|
||||
- ref: server.address
|
||||
brief: >
|
||||
Name of the local HTTP server that received the request.
|
||||
requirement_level: opt_in
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only
|
||||
include host identifier.
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup.
|
||||
|
||||
- ref: server.port
|
||||
brief: >
|
||||
Port of the local HTTP server that received the request.
|
||||
requirement_level: opt_in
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host.
|
||||
- Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Port identifier of the `Host` header
|
||||
# todo (lmolkova) build tools don't populate grandparent attributes
|
||||
- ref: http.request.method
|
||||
- ref: http.response.status_code
|
||||
- ref: network.protocol.name
|
||||
- ref: network.protocol.version
|
||||
|
||||
- id: metric_attributes.http.client
|
||||
type: attribute_group
|
||||
brief: 'HTTP client attributes'
|
||||
extends: attributes.http.client
|
||||
attributes:
|
||||
# todo (lmolkova) build tools don't populate grandparent attributes
|
||||
- ref: http.request.method
|
||||
- ref: http.response.status_code
|
||||
- ref: network.protocol.name
|
||||
- ref: network.protocol.version
|
||||
- ref: server.socket.address
|
||||
|
||||
- id: metric.http.server.duration
|
||||
type: metric
|
||||
metric_name: http.server.duration
|
||||
brief: "Measures the duration of inbound HTTP requests."
|
||||
instrument: histogram
|
||||
unit: "s"
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.server.active_requests
|
||||
type: metric
|
||||
metric_name: http.server.active_requests
|
||||
brief: "Measures the number of concurrent HTTP requests that are currently in-flight."
|
||||
instrument: updowncounter
|
||||
unit: "{request}"
|
||||
attributes:
|
||||
- ref: http.request.method
|
||||
- ref: url.scheme
|
||||
requirement_level: required
|
||||
examples: ["http", "https"]
|
||||
- ref: server.address
|
||||
requirement_level: opt_in
|
||||
brief: >
|
||||
Name of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only
|
||||
include host identifier.
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup.
|
||||
|
||||
- ref: server.port
|
||||
requirement_level: opt_in
|
||||
brief: >
|
||||
Port of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host.
|
||||
- Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Port identifier of the `Host` header
|
||||
|
||||
- id: metric.http.server.request.size
|
||||
type: metric
|
||||
metric_name: http.server.request.size
|
||||
brief: "Measures the size of HTTP request messages (compressed)."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.server.response.size
|
||||
type: metric
|
||||
metric_name: http.server.response.size
|
||||
brief: "Measures the size of HTTP response messages (compressed)."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.client.duration
|
||||
type: metric
|
||||
metric_name: http.client.duration
|
||||
brief: "Measures the duration of outbound HTTP requests."
|
||||
instrument: histogram
|
||||
unit: "s"
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.http.client.request.size
|
||||
type: metric
|
||||
metric_name: http.client.request.size
|
||||
brief: "Measures the size of HTTP request messages (compressed)."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.http.client.response.size
|
||||
type: metric
|
||||
metric_name: http.client.response.size
|
||||
brief: "Measures the size of HTTP response messages (compressed)."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.process.runtime.jvm.system.cpu.load_1m
|
||||
type: metric
|
||||
metric_name: process.runtime.jvm.system.cpu.load_1m
|
||||
brief: "Average CPU load of the whole system for the last minute as reported by the JVM."
|
||||
note: >
|
||||
The value range is [0,n], where n is the number of CPU cores - or a negative number if the value is not available.
|
||||
This utilization is not defined as being for the specific interval since last measurement
|
||||
(unlike `system.cpu.utilization`).
|
||||
[Reference](https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html#getSystemLoadAverage()).
|
||||
instrument: gauge
|
||||
unit: "1"
|
||||
@@ -1,239 +0,0 @@
|
||||
# Verbatim excerpts of the HTTP attribute and metric groups from
|
||||
# open-telemetry/semantic-conventions v1.22.0 : model/http-common.yaml
|
||||
# open-telemetry/semantic-conventions v1.22.0 : model/metrics/http.yaml
|
||||
# open-telemetry/semantic-conventions v1.22.0 : model/metrics/jvm-metrics-experimental.yaml
|
||||
# Combined under one groups key so inheritance is exercised exactly as it is in
|
||||
# the upstream registry.
|
||||
groups:
|
||||
- id: attributes.http.common
|
||||
type: attribute_group
|
||||
brief: "Describes HTTP attributes."
|
||||
attributes:
|
||||
- ref: http.request.method
|
||||
requirement_level: required
|
||||
- ref: http.response.status_code
|
||||
requirement_level:
|
||||
conditionally_required: If and only if one was received/sent.
|
||||
- ref: error.type
|
||||
requirement_level:
|
||||
conditionally_required: If request has ended with an error.
|
||||
examples: ['timeout', 'name_resolution_error', '500']
|
||||
note: |
|
||||
If the request fails with an error before response status code was sent or received,
|
||||
`error.type` SHOULD be set to exception type or a component-specific low cardinality error code.
|
||||
|
||||
If response status code was sent or received and status indicates an error according to [HTTP span status definition](/docs/http/http-spans.md),
|
||||
`error.type` SHOULD be set to the status code number (represented as a string), an exception type (if thrown) or a component-specific error code.
|
||||
|
||||
The `error.type` value SHOULD be predictable and SHOULD have low cardinality.
|
||||
Instrumentations SHOULD document the list of errors they report.
|
||||
|
||||
The cardinality of `error.type` within one instrumentation library SHOULD be low, but
|
||||
telemetry consumers that aggregate data from multiple instrumentation libraries and applications
|
||||
should be prepared for `error.type` to have high cardinality at query time, when no
|
||||
additional filters are applied.
|
||||
|
||||
If the request has completed successfully, instrumentations SHOULD NOT set `error.type`.
|
||||
- ref: network.protocol.name
|
||||
examples: ['http', 'spdy']
|
||||
requirement_level:
|
||||
recommended: if not default (`http`).
|
||||
- ref: network.protocol.version
|
||||
examples: ['1.0', '1.1', '2', '3']
|
||||
|
||||
- id: attributes.http.client
|
||||
type: attribute_group
|
||||
brief: 'HTTP Client attributes'
|
||||
extends: attributes.http.common
|
||||
attributes:
|
||||
- ref: server.address
|
||||
requirement_level: required
|
||||
brief: >
|
||||
Host identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
If an HTTP client request is explicitly made to an IP address, e.g. `http://x.x.x.x:8080`, then
|
||||
`server.address` SHOULD be the IP address `x.x.x.x`. A DNS lookup SHOULD NOT be used.
|
||||
- ref: server.port
|
||||
requirement_level:
|
||||
conditionally_required: If not default (`80` for `http` scheme, `443` for `https`).
|
||||
brief: >
|
||||
Port identifier of the ["URI origin"](https://www.rfc-editor.org/rfc/rfc9110.html#name-uri-origin) HTTP request is sent to.
|
||||
note: >
|
||||
When [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource) is absolute URI, `server.port` MUST match
|
||||
URI port identifier, otherwise it MUST match `Host` header port identifier.
|
||||
|
||||
- id: attributes.http.server
|
||||
type: attribute_group
|
||||
brief: 'HTTP Server attributes'
|
||||
extends: attributes.http.common
|
||||
attributes:
|
||||
- ref: http.route
|
||||
requirement_level:
|
||||
conditionally_required: If and only if it's available
|
||||
- ref: server.address
|
||||
brief: >
|
||||
Name of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only
|
||||
include host identifier.
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup.
|
||||
|
||||
- ref: server.port
|
||||
brief: >
|
||||
Port of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host.
|
||||
- Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Port identifier of the `Host` header
|
||||
requirement_level:
|
||||
recommended: If not default (`80` for `http` scheme, `443` for `https`).
|
||||
- ref: url.scheme
|
||||
requirement_level: required
|
||||
examples: ["http", "https"]
|
||||
- id: metric_attributes.http.server
|
||||
type: attribute_group
|
||||
brief: 'HTTP server attributes'
|
||||
extends: attributes.http.server
|
||||
attributes:
|
||||
- ref: server.address
|
||||
requirement_level: opt_in
|
||||
- ref: server.port
|
||||
requirement_level: opt_in
|
||||
|
||||
- id: metric_attributes.http.client
|
||||
type: attribute_group
|
||||
brief: 'HTTP client attributes'
|
||||
extends: attributes.http.client
|
||||
attributes:
|
||||
- ref: url.scheme
|
||||
requirement_level: required
|
||||
|
||||
- id: metric.http.server.request.duration
|
||||
type: metric
|
||||
metric_name: http.server.request.duration
|
||||
brief: "Duration of HTTP server requests."
|
||||
instrument: histogram
|
||||
unit: "s"
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.server.active_requests
|
||||
type: metric
|
||||
metric_name: http.server.active_requests
|
||||
brief: "Number of active HTTP server requests."
|
||||
instrument: updowncounter
|
||||
unit: "{request}"
|
||||
attributes:
|
||||
- ref: http.request.method
|
||||
requirement_level: required
|
||||
- ref: url.scheme
|
||||
requirement_level: required
|
||||
examples: ["http", "https"]
|
||||
- ref: server.address
|
||||
requirement_level: opt_in
|
||||
brief: >
|
||||
Name of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- The [primary server name](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host. MUST only
|
||||
include host identifier.
|
||||
- Host identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Host identifier of the `Host` header
|
||||
|
||||
SHOULD NOT be set if only IP address is available and capturing name would require a reverse DNS lookup.
|
||||
|
||||
- ref: server.port
|
||||
requirement_level: opt_in
|
||||
brief: >
|
||||
Port of the local HTTP server that received the request.
|
||||
note: |
|
||||
Determined by using the first of the following that applies
|
||||
|
||||
- Port identifier of the [primary server host](/docs/http/http-spans.md#http-server-definitions) of the matched virtual host.
|
||||
- Port identifier of the [request target](https://www.rfc-editor.org/rfc/rfc9110.html#target.resource)
|
||||
if it's sent in absolute-form.
|
||||
- Port identifier of the `Host` header
|
||||
|
||||
- id: metric.http.server.request.body.size
|
||||
type: metric
|
||||
metric_name: http.server.request.body.size
|
||||
brief: "Size of HTTP server request bodies."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
note: >
|
||||
The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and
|
||||
is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
|
||||
header. For requests using transport encoding, this should be the compressed size.
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.server.response.body.size
|
||||
type: metric
|
||||
metric_name: http.server.response.body.size
|
||||
brief: "Size of HTTP server response bodies."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
note: >
|
||||
The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and
|
||||
is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
|
||||
header. For requests using transport encoding, this should be the compressed size.
|
||||
extends: metric_attributes.http.server
|
||||
|
||||
- id: metric.http.client.request.duration
|
||||
type: metric
|
||||
metric_name: http.client.request.duration
|
||||
brief: "Duration of HTTP client requests."
|
||||
instrument: histogram
|
||||
unit: "s"
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.http.client.request.body.size
|
||||
type: metric
|
||||
metric_name: http.client.request.body.size
|
||||
brief: "Size of HTTP client request bodies."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
note: >
|
||||
The size of the request payload body in bytes. This is the number of bytes transferred excluding headers and
|
||||
is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
|
||||
header. For requests using transport encoding, this should be the compressed size.
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.http.client.response.body.size
|
||||
type: metric
|
||||
metric_name: http.client.response.body.size
|
||||
brief: "Size of HTTP client response bodies."
|
||||
instrument: histogram
|
||||
unit: "By"
|
||||
note: >
|
||||
The size of the response payload body in bytes. This is the number of bytes transferred excluding headers and
|
||||
is often, but not always, present as the [Content-Length](https://www.rfc-editor.org/rfc/rfc9110.html#field.content-length)
|
||||
header. For requests using transport encoding, this should be the compressed size.
|
||||
extends: metric_attributes.http.client
|
||||
|
||||
- id: metric.jvm.system.cpu.load_1m
|
||||
type: metric
|
||||
metric_name: jvm.system.cpu.load_1m
|
||||
brief: "Average CPU load of the whole system for the last minute as reported by the JVM."
|
||||
note: >
|
||||
The value range is [0,n], where n is the number of CPU cores - or a negative number if the value is not available.
|
||||
This utilization is not defined as being for the specific interval since last measurement
|
||||
(unlike `system.cpu.utilization`).
|
||||
[Reference](https://docs.oracle.com/en/java/javase/17/docs/api/java.management/java/lang/management/OperatingSystemMXBean.html#getSystemLoadAverage()).
|
||||
instrument: gauge
|
||||
unit: "{run_queue_item}"
|
||||
@@ -1,13 +0,0 @@
|
||||
# Verbatim excerpt from open-telemetry/semantic-conventions v1.43.0
|
||||
# (commit 89aae438b3b3b0a8dd33003c9d70592baf7dbd0d): model/jvm/metrics.yaml.
|
||||
groups:
|
||||
- id: metric.jvm.class.loaded
|
||||
type: metric
|
||||
metric_name: jvm.class.loaded
|
||||
annotations:
|
||||
code_generation:
|
||||
metric_value_type: int
|
||||
brief: "Number of classes loaded since JVM start."
|
||||
instrument: counter
|
||||
unit: "{class}"
|
||||
stability: stable
|
||||
@@ -1,13 +0,0 @@
|
||||
# Verbatim excerpt from open-telemetry/semantic-conventions v1.44.0
|
||||
# (commit e10a930844c6951757a43b849d364f7d056ac32b): model/jvm/metrics.yaml.
|
||||
groups:
|
||||
- id: metric.jvm.cpu.count
|
||||
type: metric
|
||||
metric_name: jvm.cpu.count
|
||||
annotations:
|
||||
code_generation:
|
||||
metric_value_type: int
|
||||
brief: "Number of processors available to the Java virtual machine."
|
||||
instrument: updowncounter
|
||||
unit: "{cpu}"
|
||||
stability: stable
|
||||
@@ -29,13 +29,6 @@ import (
|
||||
// transform/2.0 features exercised by the policy and PromQL rewriter tests.
|
||||
func testTransformRegistry() map[string][]byte {
|
||||
return map[string][]byte{
|
||||
"1.0.0": []byte(`groups:
|
||||
- id: metric.test.counter
|
||||
type: metric
|
||||
metric_name: test.counter
|
||||
instrument: counter
|
||||
unit: By
|
||||
`),
|
||||
"http_1.20.0_to_1.23.1.yaml": []byte(`file_format: transform/2.0
|
||||
source:
|
||||
schema_url: https://opentelemetry.io/schemas/1.20.0
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// 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 semconv_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/prometheus/prometheus/model/labels"
|
||||
"github.com/prometheus/prometheus/storage/semconv"
|
||||
"github.com/prometheus/prometheus/util/teststorage"
|
||||
)
|
||||
|
||||
// The files under testdata/upstream combine a real v1.44.0 schema with verbatim
|
||||
// HTTP and JVM semantic-convention groups from v1.21.0, v1.22.0, v1.43.0, and
|
||||
// v1.44.0. Each fixture names its upstream source; the HTTP groups retain their
|
||||
// inheritance structure.
|
||||
// Hand-written fixtures are free to encode whatever shape the parser happens to
|
||||
// expect, which is how rename_metrics came to be read from a name_map key that
|
||||
// the file format does not have: every fixture agreed with the bug, so the tests
|
||||
// passed while metric renames silently did nothing on real data. These tests
|
||||
// exist so the real file format is what the parser is held to.
|
||||
//
|
||||
// Attribute-rename scoping uses a focused fixture elsewhere because the real
|
||||
// schema's HTTP attribute renames in this version apply only to spans.
|
||||
const (
|
||||
upstreamSchema = "./testdata/upstream/schema-1.44.0.yaml"
|
||||
upstreamSemconv1_21_0 = "./testdata/upstream/semconv-1.21.0.yaml"
|
||||
upstreamSemconv1_22_0 = "./testdata/upstream/semconv-1.22.0.yaml"
|
||||
upstreamSemconv1_43_0 = "./testdata/upstream/semconv-1.43.0.yaml"
|
||||
upstreamSemconv1_44_0 = "./testdata/upstream/semconv-1.44.0.yaml"
|
||||
)
|
||||
|
||||
// This edge is deliberately synthetic: its endpoints are unrelated stable
|
||||
// metrics whose definitions below are verbatim upstream data.
|
||||
const upstreamStableContradictionSchema = `file_format: 1.1.0
|
||||
schema_url: https://example.com/schemas/1.44.0
|
||||
versions:
|
||||
1.43.0:
|
||||
1.44.0:
|
||||
metrics:
|
||||
changes:
|
||||
- rename_metrics:
|
||||
jvm.class.loaded: jvm.cpu.count
|
||||
`
|
||||
|
||||
// upstreamRegistry assembles the real artefacts into a registry, keyed the way
|
||||
// AwareStorageWithRegistry expects: semver base names for semconv files, anything
|
||||
// else for the schema.
|
||||
func upstreamRegistry(t *testing.T) map[string][]byte {
|
||||
t.Helper()
|
||||
read := func(path string) []byte {
|
||||
b, err := os.ReadFile(filepath.Clean(path))
|
||||
require.NoError(t, err)
|
||||
return b
|
||||
}
|
||||
return map[string][]byte{
|
||||
"registry.yaml": read(upstreamSchema),
|
||||
"1.21.0": read(upstreamSemconv1_21_0),
|
||||
"1.22.0": read(upstreamSemconv1_22_0),
|
||||
"1.43.0": read(upstreamSemconv1_43_0),
|
||||
"1.44.0": read(upstreamSemconv1_44_0),
|
||||
}
|
||||
}
|
||||
|
||||
// TestUpstreamSchemaMetricRenames guards the file format itself. Reading
|
||||
// rename_metrics from a nested name_map key yielded zero metric renames from
|
||||
// every real schema while all fixtures still passed, so assert against the real
|
||||
// artefact that metric renames are found at all.
|
||||
func TestUpstreamSchemaMetricRenames(t *testing.T) {
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, upstreamRegistry(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
// http.server.duration was renamed to http.server.request.duration in
|
||||
// semconv 1.22.0, the canonical example of a metric rename. Write the series
|
||||
// under its pre-rename name and query it under the post-rename one.
|
||||
appendSeries(t, wrapped, "http.server.duration", 1, 7.0, "http.response.status_code", "200")
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "http.server.request.duration"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.22.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 1,
|
||||
"expected the pre-rename series to surface under the queried name; zero means rename_metrics did not parse, got %v", got)
|
||||
for k := range got {
|
||||
require.Contains(t, k, `__name__="http.server.request.duration"`)
|
||||
}
|
||||
|
||||
// Both versions declare the metric as s/histogram, so the metric rename is
|
||||
// corroborated. Later schema revisions may independently report ambiguous
|
||||
// attribute histories, but none of the warnings may concern metric lineage.
|
||||
for _, warning := range warningStrings(set.Warnings()) {
|
||||
require.Contains(t, warning, "attribute name",
|
||||
"a rename corroborated by the real semconv files must not raise a metric warning")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpstreamExperimentalMetricRenameMayChangeUnit(t *testing.T) {
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, upstreamRegistry(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, "process.runtime.jvm.system.cpu.load_1m", 1, 7.0)
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "jvm.system.cpu.load_1m"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.22.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/registry.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Len(t, got, 1, "the upstream experimental rename must retain its historical series: %v", got)
|
||||
for key := range got {
|
||||
require.Contains(t, key, `__name__="jvm.system.cpu.load_1m"`)
|
||||
}
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "following the explicit schema rename")
|
||||
}
|
||||
|
||||
func TestUpstreamStableMetricContradiction(t *testing.T) {
|
||||
registry := upstreamRegistry(t)
|
||||
registry["stable-contradiction.yaml"] = []byte(upstreamStableContradictionSchema)
|
||||
|
||||
underlying := teststorage.New(t)
|
||||
wrapped, err := semconv.AwareStorageWithRegistry(underlying, registry)
|
||||
require.NoError(t, err)
|
||||
|
||||
appendSeries(t, wrapped, "jvm.class.loaded", 1, 7.0)
|
||||
|
||||
q, err := wrapped.Querier(0, 10)
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() { _ = q.Close() })
|
||||
|
||||
set := q.Select(context.Background(), false, nil,
|
||||
labels.MustNewMatcher(labels.MatchEqual, model.MetricNameLabel, "jvm.cpu.count"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__semconv_url__", "registry/1.44.0"),
|
||||
labels.MustNewMatcher(labels.MatchEqual, "__schema_url__", "registry/stable-contradiction.yaml"),
|
||||
)
|
||||
got := collectSeries(t, set)
|
||||
require.Empty(t, got, "stable definitions with conflicting units and instruments must not merge: %v", got)
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), "treating them as different metrics")
|
||||
requireWarningsContain(t, warningStrings(set.Warnings()), `resolves it to "jvm.class.loaded"`)
|
||||
}
|
||||
Reference in new issue
Block a user