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:
Bartek Plotka committed 2026-10-27 21:31:49 +00:00
1 parent 6509cd7a3c
commit 8e609892db
36 files changed
+163 -13042

No files matched your search

-318
View File
@@ -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
View File
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+18 -781
View File
@@ -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
+1 -1
View File
@@ -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
+5 -39
View File
@@ -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
}
-30
View File
@@ -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
-30
View File
@@ -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
-30
View File
@@ -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
-22
View File
@@ -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
+33 -98
View File
@@ -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) {
+1 -1
View File
@@ -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__
-879
View File
@@ -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
-171
View File
@@ -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))
}
-123
View File
@@ -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")
})
}
}
-221
View File
@@ -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
File diff suppressed because it is too large. Load diff
-83
View File
@@ -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))
}
-131
View File
@@ -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")
}
-40
View File
@@ -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
-7
View File
@@ -1,7 +0,0 @@
file_format: "2.0.0"
schema_url: https://example.com/schemas/2.0.0
versions:
2.0.0:
metrics:
changes: []
-17
View File
@@ -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
-20
View File
@@ -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
-10
View File
@@ -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
-766
View File
@@ -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:
-293
View File
@@ -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"
-239
View File
@@ -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}"
-13
View File
@@ -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
-13
View File
@@ -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
-7
View File
@@ -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
-168
View File
@@ -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"`)
}