Skip to content

Add build tag to build without deprecated global name validation scheme #804

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ jobs:
use_gomod_cache:
type: boolean
default: true
build_tags:
type: string
default: ""
docker:
- image: cimg/go:<< parameters.go_version >>
environment:
Expand All @@ -24,7 +27,12 @@ jobs:
steps:
- go/load-cache:
key: v1-go<< parameters.go_version >>
- run: make test
- run: |
if [ -n "<< parameters.build_tags >>" ]; then
make test GOOPTS="-tags=<< parameters.build_tags >>"
else
make test
fi
- when:
condition: << parameters.use_gomod_cache >>
steps:
Expand Down Expand Up @@ -89,12 +97,15 @@ workflows:
jobs:
# Supported Go versions are synced with github.com/prometheus/client_golang.
- test:
name: go-<< matrix.go_version >>
name: go-<< matrix.go_version >><<# matrix.build_tags >>-<< matrix.build_tags >><</matrix.build_tags >>
matrix:
parameters:
go_version:
- "1.23"
- "1.24"
build_tags:
- ""
- "localvalidationscheme"
- test-assets:
name: assets-go-<< matrix.go_version >>
matrix:
Expand Down
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,17 @@ any stability guarantees for external usage.
* **route**: A routing wrapper around [httprouter](https://github.com/julienschmidt/httprouter) using `context.Context`
* **server**: Common servers
* **version**: Version information and metrics

## Metric/label name validation scheme

The libraries in this Go module share a notion of metric and label name validation scheme.
There are two different schemes to choose from:
* `model.LegacyValidation` => Metric and label names have to conform to the original Prometheus character requirements
* `model.UTF8Validation` => Metric and label names are only required to be valid UTF-8 strings

The active name validation scheme is normally implicitly controlled via the global variable `model.NameValidationScheme`.
It's used by functions such as `model.IsValidMetricName` and `model.LabelName.IsValid`.
_However_, if building with the _experimental_ build tag `localvalidationscheme`, the `model.NameValidationScheme` global is removed, and the API changes to accept the name validation scheme as an explicit parameter.
`model.NameValidationScheme` is deprecated, and at some point, the API currently controlled by the build tag `localvalidationscheme` becomes standard.
All users of this library will be expected to move to the new API, and there will be an announcement in the changelog when the global is removed.
For the time being, the `localvalidationscheme` build tag is experimental and the API enabled by it may change.
20 changes: 2 additions & 18 deletions expfmt/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
package expfmt

import (
"bufio"
"fmt"
"io"
"math"
Expand Down Expand Up @@ -70,21 +69,6 @@ func ResponseFormat(h http.Header) Format {
return FmtUnknown
}

// NewDecoder returns a new decoder based on the given input format.
// If the input format does not imply otherwise, a text format decoder is returned.
func NewDecoder(r io.Reader, format Format) Decoder {
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{r: bufio.NewReader(r)}
}
return &textDecoder{r: r}
}

// protoDecoder implements the Decoder interface for protocol buffers.
type protoDecoder struct {
r protodelim.Reader
}

// Decode implements the Decoder interface.
func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
opts := protodelim.UnmarshalOptions{
Expand All @@ -93,7 +77,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
if err := opts.UnmarshalFrom(d.r, v); err != nil {
return err
}
if !model.IsValidMetricName(model.LabelValue(v.GetName())) {
if !d.isValidMetricName(v.GetName()) {
return fmt.Errorf("invalid metric name %q", v.GetName())
}
for _, m := range v.GetMetric() {
Expand All @@ -107,7 +91,7 @@ func (d *protoDecoder) Decode(v *dto.MetricFamily) error {
if !model.LabelValue(l.GetValue()).IsValid() {
return fmt.Errorf("invalid label value %q", l.GetValue())
}
if !model.LabelName(l.GetName()).IsValid() {
if !d.isValidLabelName(l.GetName()) {
return fmt.Errorf("invalid label name %q", l.GetName())
}
}
Expand Down
48 changes: 48 additions & 0 deletions expfmt/decode_globalvalidationscheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2025 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.

//go:build !localvalidationscheme

package expfmt

import (
"bufio"
"io"

"google.golang.org/protobuf/encoding/protodelim"

"github.com/prometheus/common/model"
)

// protoDecoder implements the Decoder interface for protocol buffers.
type protoDecoder struct {
r protodelim.Reader
}

// NewDecoder returns a new decoder based on the given input format.
// If the input format does not imply otherwise, a text format decoder is returned.
func NewDecoder(r io.Reader, format Format) Decoder {
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{r: bufio.NewReader(r)}
}
return &textDecoder{r: r}
}

func (d *protoDecoder) isValidMetricName(name string) bool {
return model.IsValidMetricName(model.LabelValue(name))
}

func (d *protoDecoder) isValidLabelName(name string) bool {
return model.LabelName(name).IsValid()
}
36 changes: 36 additions & 0 deletions expfmt/decode_globalvalidationscheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright 2025 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.

//go:build !localvalidationscheme

package expfmt

import (
"io"
"testing"

"github.com/prometheus/common/model"
)

func newDecoder(t *testing.T, r io.Reader, format Format, scheme model.ValidationScheme) Decoder {
//nolint:staticcheck // NameValidationScheme is being phased out.
origScheme := model.NameValidationScheme
t.Cleanup(func() {
//nolint:staticcheck // NameValidationScheme is being phased out.
model.NameValidationScheme = origScheme
})
//nolint:staticcheck // NameValidationScheme is being phased out.
model.NameValidationScheme = scheme

return NewDecoder(r, format)
}
52 changes: 52 additions & 0 deletions expfmt/decode_localvalidationscheme.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2025 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.

//go:build localvalidationscheme

package expfmt

import (
"bufio"
"io"

"google.golang.org/protobuf/encoding/protodelim"

"github.com/prometheus/common/model"
)

// protoDecoder implements the Decoder interface for protocol buffers.
type protoDecoder struct {
r protodelim.Reader
validationScheme model.ValidationScheme
}

// NewDecoder returns a new decoder based on the given input format.
// If the input format does not imply otherwise, a text format decoder is returned.
func NewDecoder(r io.Reader, format Format, validationScheme model.ValidationScheme) Decoder {
switch format.FormatType() {
case TypeProtoDelim:
return &protoDecoder{
r: bufio.NewReader(r),
validationScheme: validationScheme,
}
}
return &textDecoder{r: r}
}

func (d *protoDecoder) isValidMetricName(name string) bool {
return model.IsValidMetricName(model.LabelValue(name), d.validationScheme)
}

func (d *protoDecoder) isValidLabelName(name string) bool {
return model.LabelName(name).IsValid(d.validationScheme)
}
27 changes: 27 additions & 0 deletions expfmt/decode_localvalidationscheme_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2025 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.

//go:build localvalidationscheme

package expfmt

import (
"io"
"testing"

"github.com/prometheus/common/model"
)

func newDecoder(_ *testing.T, r io.Reader, format Format, scheme model.ValidationScheme) Decoder {
return NewDecoder(r, format, scheme)
}
75 changes: 38 additions & 37 deletions expfmt/decode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"reflect"
"sort"
"strconv"
"strings"
"testing"

Expand Down Expand Up @@ -360,46 +361,46 @@ func TestProtoDecoder(t *testing.T) {
}

for i, scenario := range scenarios {
dec := &SampleDecoder{
Dec: &protoDecoder{r: strings.NewReader(scenario.in)},
Opts: &DecodeOptions{
Timestamp: testTime,
},
}

var all model.Vector
for {
model.NameValidationScheme = model.LegacyValidation //nolint:staticcheck
var smpls model.Vector
err := dec.Decode(&smpls)
if err != nil && errors.Is(err, io.EOF) {
break
t.Run(strconv.Itoa(i), func(t *testing.T) {
dec := &SampleDecoder{
Dec: newDecoder(t, strings.NewReader(scenario.in), FmtProtoDelim, model.LegacyValidation),
Opts: &DecodeOptions{
Timestamp: testTime,
},
}
if scenario.legacyNameFail {
require.Errorf(t, err, "Expected error when decoding without UTF-8 support enabled but got none")
model.NameValidationScheme = model.UTF8Validation //nolint:staticcheck
dec = &SampleDecoder{
Dec: &protoDecoder{r: strings.NewReader(scenario.in)},
Opts: &DecodeOptions{
Timestamp: testTime,
},

var all model.Vector
for {
var smpls model.Vector
err := dec.Decode(&smpls)
if err != nil && errors.Is(err, io.EOF) {
break
}
if scenario.legacyNameFail {
require.Errorf(t, err, "Expected error when decoding without UTF-8 support enabled but got none")
dec = &SampleDecoder{
Dec: newDecoder(t, strings.NewReader(scenario.in), FmtProtoDelim, model.UTF8Validation),
Opts: &DecodeOptions{
Timestamp: testTime,
},
}
err = dec.Decode(&smpls)
if errors.Is(err, io.EOF) {
break
}
require.NoErrorf(t, err, "Unexpected error when decoding with UTF-8 support: %v", err)
}
err = dec.Decode(&smpls)
if errors.Is(err, io.EOF) {
if scenario.fail {
require.Errorf(t, err, "Expected error but got none")
break
}
require.NoErrorf(t, err, "Unexpected error when decoding with UTF-8 support: %v", err)
require.NoError(t, err)
all = append(all, smpls...)
}
if scenario.fail {
require.Errorf(t, err, "Expected error but got none")
break
}
require.NoError(t, err)
all = append(all, smpls...)
}
sort.Sort(all)
sort.Sort(scenario.expected)
require.Truef(t, reflect.DeepEqual(all, scenario.expected), "%d. output does not match, want: %#v, got %#v", i, scenario.expected, all)
sort.Sort(all)
sort.Sort(scenario.expected)
require.Truef(t, reflect.DeepEqual(all, scenario.expected), "%d. output does not match, want: %#v, got %#v", i, scenario.expected, all)
})
}
}

Expand All @@ -408,7 +409,7 @@ func TestProtoMultiMessageDecoder(t *testing.T) {
require.NoErrorf(t, err, "Reading file failed: %v", err)

buf := bytes.NewReader(data)
decoder := NewDecoder(buf, FmtProtoDelim)
decoder := newDecoder(t, buf, FmtProtoDelim, model.UTF8Validation)
var metrics []*dto.MetricFamily
for {
var mf dto.MetricFamily
Expand Down Expand Up @@ -557,7 +558,7 @@ func TestTextDecoderWithBufioReader(t *testing.T) {

var decoded bool
r := bufio.NewReader(strings.NewReader(example))
dec := NewDecoder(r, FmtText)
dec := newDecoder(t, r, FmtText, model.UTF8Validation)
for {
var mf dto.MetricFamily
if err := dec.Decode(&mf); err != nil {
Expand Down
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/jpillora/backoff v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.20.4 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/prometheus/client_golang v1.22.1-0.20250714095417-1649bc88444a // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
golang.org/x/sys v0.33.0 // indirect
Expand Down
Loading
Loading