Promsafe introduced: Type Safe Labels. (Draft, Discussion is still required)
Signed-off-by: Eugene <eugene@amberpixels.io>
This commit is contained in:
parent
f53c5ca1a8
commit
879c4741b0
|
@ -0,0 +1,161 @@
|
||||||
|
// Copyright 2024 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 promsafe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LabelsProvider is an interface that allows to convert anything into prometheus.Labels
|
||||||
|
// It allows to provide your own FAST implementation of Struct->prometheus.Labels conversion
|
||||||
|
// without using reflection.
|
||||||
|
type LabelsProvider interface {
|
||||||
|
ToPrometheusLabels() prometheus.Labels
|
||||||
|
ToLabelNames() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// LabelsProviderMarker is a marker interface for enforcing type-safety of StructLabelProvider.
|
||||||
|
type LabelsProviderMarker interface {
|
||||||
|
labelsProviderMarker()
|
||||||
|
}
|
||||||
|
|
||||||
|
// StructLabelProvider should be embedded in any struct that serves as a label provider.
|
||||||
|
type StructLabelProvider struct{}
|
||||||
|
|
||||||
|
var _ LabelsProviderMarker = (*StructLabelProvider)(nil)
|
||||||
|
|
||||||
|
func (s StructLabelProvider) labelsProviderMarker() {
|
||||||
|
panic("LabelsProviderMarker interface method should never be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEmptyLabels creates a new empty labels instance of type T
|
||||||
|
// It's a bit tricky as we want to support both structs and pointers to structs
|
||||||
|
// e.g. &MyLabels{StructLabelProvider} or MyLabels{StructLabelProvider}
|
||||||
|
func NewEmptyLabels[T LabelsProviderMarker]() T {
|
||||||
|
var emptyLabels T
|
||||||
|
|
||||||
|
val := reflect.ValueOf(&emptyLabels).Elem()
|
||||||
|
if val.Kind() == reflect.Ptr {
|
||||||
|
ptrType := val.Type().Elem()
|
||||||
|
newValue := reflect.New(ptrType).Interface().(T)
|
||||||
|
return newValue
|
||||||
|
}
|
||||||
|
|
||||||
|
return emptyLabels
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// Helpers
|
||||||
|
//
|
||||||
|
|
||||||
|
// promsafeTag is the tag name used for promsafe labels inside structs.
|
||||||
|
// The tag is optional, as if not present, field is used with snake_cased FieldName.
|
||||||
|
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
|
||||||
|
var promsafeTag = "promsafe"
|
||||||
|
|
||||||
|
// SetPromsafeTag sets the tag name used for promsafe labels inside structs.
|
||||||
|
func SetPromsafeTag(tag string) {
|
||||||
|
promsafeTag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
// iterateStructFields iterates over struct fields, calling the given function for each field.
|
||||||
|
func iterateStructFields(structValue any, fn func(labelName string, fieldValue reflect.Value)) {
|
||||||
|
val := reflect.Indirect(reflect.ValueOf(structValue))
|
||||||
|
typ := val.Type()
|
||||||
|
|
||||||
|
for i := 0; i < typ.NumField(); i++ {
|
||||||
|
field := typ.Field(i)
|
||||||
|
if field.Anonymous {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle tag logic centrally
|
||||||
|
var labelName string
|
||||||
|
if ourTag := field.Tag.Get(promsafeTag); ourTag == "-" {
|
||||||
|
continue // Skip field
|
||||||
|
} else if ourTag != "" {
|
||||||
|
labelName = ourTag
|
||||||
|
} else {
|
||||||
|
labelName = toSnakeCase(field.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn(labelName, val.Field(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractLabelsWithValues extracts labels names+values from a given LabelsProviderMarker (parent instance of a StructLabelProvider)
|
||||||
|
func extractLabelsWithValues(labelProvider LabelsProviderMarker) prometheus.Labels {
|
||||||
|
if any(labelProvider) == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if clp, ok := labelProvider.(LabelsProvider); ok {
|
||||||
|
return clp.ToPrometheusLabels()
|
||||||
|
}
|
||||||
|
|
||||||
|
// extracting labels from a struct
|
||||||
|
labels := prometheus.Labels{}
|
||||||
|
iterateStructFields(labelProvider, func(labelName string, fieldValue reflect.Value) {
|
||||||
|
labels[labelName] = stringifyLabelValue(fieldValue)
|
||||||
|
})
|
||||||
|
return labels
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractLabelNames extracts labels names from a given LabelsProviderMarker (parent instance of aStructLabelProvider)
|
||||||
|
func extractLabelNames(labelProvider LabelsProviderMarker) []string {
|
||||||
|
if any(labelProvider) == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If custom implementation is done, just do it
|
||||||
|
if lp, ok := labelProvider.(LabelsProvider); ok {
|
||||||
|
return lp.ToLabelNames()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to slow implementation via reflect
|
||||||
|
// Important! We return label names in order of fields in the struct
|
||||||
|
labelNames := make([]string, 0)
|
||||||
|
iterateStructFields(labelProvider, func(labelName string, fieldValue reflect.Value) {
|
||||||
|
labelNames = append(labelNames, labelName)
|
||||||
|
})
|
||||||
|
|
||||||
|
return labelNames
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringifyLabelValue makes up a valid string value from a given field's value
|
||||||
|
// It's used ONLY in fallback reflect mode
|
||||||
|
// Field value might be a pointer, that's why we do reflect.Indirect()
|
||||||
|
// Note: in future we can handle default values here as well
|
||||||
|
func stringifyLabelValue(v reflect.Value) string {
|
||||||
|
// TODO: we probably want to handle custom type processing here
|
||||||
|
// e.g. sometimes booleans need to be "on"/"off" instead of "true"/"false"
|
||||||
|
return fmt.Sprintf("%v", reflect.Indirect(v).Interface())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert struct field names to snake_case for Prometheus label compliance.
|
||||||
|
func toSnakeCase(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
var result []rune
|
||||||
|
for i, r := range s {
|
||||||
|
if i > 0 && r >= 'A' && r <= 'Z' {
|
||||||
|
result = append(result, '_')
|
||||||
|
}
|
||||||
|
result = append(result, r)
|
||||||
|
}
|
||||||
|
return strings.ToLower(string(result))
|
||||||
|
}
|
|
@ -0,0 +1,263 @@
|
||||||
|
// Copyright 2024 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 promsafe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TestLabels struct {
|
||||||
|
StructLabelProvider
|
||||||
|
Field1 string
|
||||||
|
Field2 int
|
||||||
|
Field3 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLabelsWithTags struct {
|
||||||
|
StructLabelProvider
|
||||||
|
FieldA string `promsafe:"custom_a"`
|
||||||
|
FieldB int `promsafe:"-"`
|
||||||
|
FieldC bool // no promsafe tag, so default to snake_case of field_c
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLabelsWithPointers struct {
|
||||||
|
StructLabelProvider
|
||||||
|
Field1 *string
|
||||||
|
Field2 *int
|
||||||
|
Field3 *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLabelsFast struct {
|
||||||
|
StructLabelProvider
|
||||||
|
Field1 string
|
||||||
|
Field2 int
|
||||||
|
Field3 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestLabelsFast) ToPrometheusLabels() prometheus.Labels {
|
||||||
|
return prometheus.Labels{
|
||||||
|
"f1": t.Field1,
|
||||||
|
"f2": strconv.Itoa(t.Field2),
|
||||||
|
"f3": strconv.FormatBool(t.Field3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestLabelsFast) ToLabelNames() []string {
|
||||||
|
return []string{"f1", "f2", "f3"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_extractLabelsWithValues(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input LabelsProviderMarker
|
||||||
|
expected prometheus.Labels
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic struct without custom tags",
|
||||||
|
input: TestLabels{
|
||||||
|
Field1: "value1",
|
||||||
|
Field2: 123,
|
||||||
|
Field3: true,
|
||||||
|
},
|
||||||
|
expected: prometheus.Labels{
|
||||||
|
"field1": "value1",
|
||||||
|
"field2": "123",
|
||||||
|
"field3": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct with custom tags and exclusions",
|
||||||
|
input: TestLabelsWithTags{
|
||||||
|
FieldA: "customValue",
|
||||||
|
FieldB: 456,
|
||||||
|
FieldC: false,
|
||||||
|
},
|
||||||
|
expected: prometheus.Labels{
|
||||||
|
"custom_a": "customValue",
|
||||||
|
"field_c": "false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct with pointers",
|
||||||
|
input: TestLabelsWithPointers{
|
||||||
|
Field1: ptr("ptrValue"),
|
||||||
|
Field2: ptr(789),
|
||||||
|
Field3: ptr(true),
|
||||||
|
},
|
||||||
|
expected: prometheus.Labels{
|
||||||
|
"field1": "ptrValue",
|
||||||
|
"field2": "789",
|
||||||
|
"field3": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct fast (with declared methods)",
|
||||||
|
input: TestLabelsFast{
|
||||||
|
Field1: "hello",
|
||||||
|
Field2: 100,
|
||||||
|
Field3: true,
|
||||||
|
},
|
||||||
|
expected: prometheus.Labels{
|
||||||
|
"f1": "hello",
|
||||||
|
"f2": "100",
|
||||||
|
"f3": "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil will return empty result",
|
||||||
|
input: nil,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if !tt.shouldFail {
|
||||||
|
t.Errorf("unexpected panic: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call extractLabelsFromStruct
|
||||||
|
got := extractLabelsWithValues(tt.input)
|
||||||
|
|
||||||
|
// Compare results
|
||||||
|
if !reflect.DeepEqual(got, tt.expected) {
|
||||||
|
t.Errorf("extractLabelFromStruct(%v) = %v; want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_extractLabelNames(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input LabelsProviderMarker
|
||||||
|
expected []string
|
||||||
|
shouldFail bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "Basic struct without custom tags",
|
||||||
|
input: TestLabels{
|
||||||
|
Field1: "value1",
|
||||||
|
Field2: 123,
|
||||||
|
Field3: true,
|
||||||
|
},
|
||||||
|
expected: []string{"field1", "field2", "field3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct with custom tags and exclusions",
|
||||||
|
input: TestLabelsWithTags{
|
||||||
|
FieldA: "customValue",
|
||||||
|
FieldB: 456,
|
||||||
|
FieldC: false,
|
||||||
|
},
|
||||||
|
expected: []string{"custom_a", "field_c"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct with pointers",
|
||||||
|
input: TestLabelsWithPointers{
|
||||||
|
Field1: ptr("ptrValue"),
|
||||||
|
Field2: ptr(789),
|
||||||
|
Field3: ptr(true),
|
||||||
|
},
|
||||||
|
expected: []string{"field1", "field2", "field3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Struct fast (with declared methods)",
|
||||||
|
input: TestLabelsFast{
|
||||||
|
Field1: "hello",
|
||||||
|
Field2: 100,
|
||||||
|
Field3: true,
|
||||||
|
},
|
||||||
|
expected: []string{"f1", "f2", "f3"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Nil will return empty result",
|
||||||
|
input: nil,
|
||||||
|
expected: nil,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
if !tt.shouldFail {
|
||||||
|
t.Errorf("unexpected panic: %v", r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Call extractLabelsFromStruct
|
||||||
|
got := extractLabelNames(tt.input)
|
||||||
|
|
||||||
|
// Compare results
|
||||||
|
if !reflect.DeepEqual(got, tt.expected) {
|
||||||
|
t.Errorf("extractLabelFromStruct(%v) = %v; want %v", tt.input, got, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_NewEmptyLabels(t *testing.T) {
|
||||||
|
got1 := NewEmptyLabels[TestLabels]()
|
||||||
|
if !reflect.DeepEqual(got1, TestLabels{}) {
|
||||||
|
t.Errorf("NewEmptyLabels[%T] = %v; want %v", TestLabels{}, got1, TestLabels{})
|
||||||
|
}
|
||||||
|
got2 := NewEmptyLabels[TestLabelsWithTags]()
|
||||||
|
if !reflect.DeepEqual(got2, TestLabelsWithTags{}) {
|
||||||
|
t.Errorf("NewEmptyLabels[%T] = %v; want %v", TestLabelsWithTags{}, got1, TestLabelsWithTags{})
|
||||||
|
}
|
||||||
|
got3 := NewEmptyLabels[TestLabelsWithPointers]()
|
||||||
|
if !reflect.DeepEqual(got3, TestLabelsWithPointers{}) {
|
||||||
|
t.Errorf("NewEmptyLabels[%T] = %v; want %v", TestLabelsWithPointers{}, got1, TestLabelsWithPointers{})
|
||||||
|
}
|
||||||
|
got4 := NewEmptyLabels[*TestLabelsFast]()
|
||||||
|
if !reflect.DeepEqual(*got4, TestLabelsFast{}) {
|
||||||
|
t.Errorf("NewEmptyLabels[%T] = %v; want %v", TestLabelsFast{}, *got4, TestLabelsFast{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Test_SetPromsafeTag(t *testing.T) {
|
||||||
|
SetPromsafeTag("prom")
|
||||||
|
defer func() {
|
||||||
|
SetPromsafeTag("")
|
||||||
|
}()
|
||||||
|
if promsafeTag != "prom" {
|
||||||
|
t.Errorf("promsafeTag = %v; want %v", promsafeTag, "prom")
|
||||||
|
}
|
||||||
|
|
||||||
|
type CustomTestLabels struct {
|
||||||
|
StructLabelProvider
|
||||||
|
FieldX string `prom:"x"`
|
||||||
|
}
|
||||||
|
|
||||||
|
extractedLabelNames := extractLabelNames(CustomTestLabels{})
|
||||||
|
if !reflect.DeepEqual(extractedLabelNames, []string{"x"}) {
|
||||||
|
t.Errorf("Using custom promsafeTag: extractLabelNames(%v) = %v; want %v", CustomTestLabels{}, extractedLabelNames, []string{"x"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper functions to create pointers
|
||||||
|
func ptr[T any](v T) *T {
|
||||||
|
return &v
|
||||||
|
}
|
|
@ -0,0 +1,63 @@
|
||||||
|
// Copyright 2024 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 promauto_adapter provides compatibility adapter for migration of calls of promauto into promsafe
|
||||||
|
package promauto_adapter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCounterVec behaves as adapter-promauto.NewCounterVec but with type-safe labels
|
||||||
|
func NewCounterVec[T promsafe.LabelsProviderMarker](opts prometheus.CounterOpts) *promsafe.CounterVec[T] {
|
||||||
|
//_ = promauto.NewCounterVec // keeping for reference
|
||||||
|
|
||||||
|
c := promsafe.NewCounterVec[T](opts)
|
||||||
|
if prometheus.DefaultRegisterer != nil {
|
||||||
|
prometheus.DefaultRegisterer.MustRegister(c.Unsafe())
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory is a promauto-like factory that allows type-safe labels.
|
||||||
|
type Factory[T promsafe.LabelsProviderMarker] struct {
|
||||||
|
r prometheus.Registerer
|
||||||
|
}
|
||||||
|
|
||||||
|
// With behaves same as adapter-promauto.With but with type-safe labels
|
||||||
|
func With[T promsafe.LabelsProviderMarker](r prometheus.Registerer) Factory[T] {
|
||||||
|
return Factory[T]{r: r}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounterVec behaves like adapter-promauto.NewCounterVec but with type-safe labels
|
||||||
|
func (f Factory[T]) NewCounterVec(opts prometheus.CounterOpts) *promsafe.CounterVec[T] {
|
||||||
|
c := NewCounterVec[T](opts)
|
||||||
|
if f.r != nil {
|
||||||
|
f.r.MustRegister(c.Unsafe())
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounter wraps promauto.NewCounter.
|
||||||
|
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
|
||||||
|
func (f Factory[T]) NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
|
||||||
|
return promauto.With(f.r).NewCounter(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounterFunc wraps promauto.NewCounterFunc.
|
||||||
|
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
|
||||||
|
func (f Factory[T]) NewCounterFunc(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
|
||||||
|
return promauto.With(f.r).NewCounterFunc(opts, function)
|
||||||
|
}
|
|
@ -0,0 +1,58 @@
|
||||||
|
// Copyright 2024 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 promauto_adapter_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promsafe"
|
||||||
|
|
||||||
|
promauto "github.com/prometheus/client_golang/prometheus/promsafe/promauto_adapter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExampleNewCounterVec_promauto_adapted() {
|
||||||
|
// Examples on how to migrate from promauto to promsafe gently
|
||||||
|
//
|
||||||
|
// Before:
|
||||||
|
// import "github.com/prometheus/client_golang/prometheus/promauto"
|
||||||
|
// func main() {
|
||||||
|
// myReg := prometheus.NewRegistry()
|
||||||
|
// counterOpts := prometheus.CounterOpts{Name:"..."}
|
||||||
|
// promauto.With(myReg).NewCounterVec(counterOpts, []string{"event_type", "source"})
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// After:
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// promauto "github.com/prometheus/client_golang/prometheus/promsafe/promauto_adapter"
|
||||||
|
// )
|
||||||
|
// ...
|
||||||
|
|
||||||
|
myReg := prometheus.NewRegistry()
|
||||||
|
counterOpts := prometheus.CounterOpts{
|
||||||
|
Name: "items_counted_detailed_auto",
|
||||||
|
}
|
||||||
|
|
||||||
|
type MyLabels struct {
|
||||||
|
promsafe.StructLabelProvider
|
||||||
|
EventType string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
c := promauto.With[MyLabels](myReg).NewCounterVec(counterOpts)
|
||||||
|
|
||||||
|
c.With(MyLabels{
|
||||||
|
EventType: "reservation", Source: "source1",
|
||||||
|
}).Inc()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
// Copyright 2024 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 promsafe provides a layer of type-safety for label management in Prometheus metrics.
|
||||||
|
//
|
||||||
|
// Promsafe introduces type-safe labels, ensuring that the labels used with
|
||||||
|
// Prometheus metrics are explicitly defined and validated at compile-time. This
|
||||||
|
// eliminates common runtime errors caused by mislabeling, such as typos or
|
||||||
|
// incorrect label orders.
|
||||||
|
//
|
||||||
|
// The following example demonstrates how to create and use a CounterVec with
|
||||||
|
// type-safe labels (compared to how it's done in a regular way):
|
||||||
|
//
|
||||||
|
// package main
|
||||||
|
//
|
||||||
|
// import (
|
||||||
|
// "strconv"
|
||||||
|
//
|
||||||
|
// "github.com/prometheus/client_golang/prometheus"
|
||||||
|
// "github.com/prometheus/client_golang/prometheus/promsafe"
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // Original unsafe way (no type safety)
|
||||||
|
//
|
||||||
|
// func originalUnsafeWay() {
|
||||||
|
// counterVec := prometheus.NewCounterVec(
|
||||||
|
// prometheus.CounterOpts{
|
||||||
|
// Name: "http_requests_total",
|
||||||
|
// Help: "Total number of HTTP requests by status code and method.",
|
||||||
|
// },
|
||||||
|
// []string{"code", "method"}, // Labels defined as raw strings
|
||||||
|
// )
|
||||||
|
//
|
||||||
|
// // No compile-time checks; label order and types must be correct
|
||||||
|
// // You have to know which and how many labels are expected (in proper order)
|
||||||
|
// counterVec.WithLabelValues("200", "GET").Inc()
|
||||||
|
//
|
||||||
|
// // or you can use map, that is even more fragile
|
||||||
|
// counterVec.With(prometheus.Labels{"code": "200", "method": "GET"}).Inc()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Safe way (Quick implementation, reflect-based under-the-hood)
|
||||||
|
//
|
||||||
|
// type Labels1 struct {
|
||||||
|
// promsafe.StructLabelProvider
|
||||||
|
// Code int
|
||||||
|
// Method string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func safeReflectWay() {
|
||||||
|
// counterVec := promsafe.NewCounterVec[Labels1](prometheus.CounterOpts{
|
||||||
|
// Name: "http_requests_total_reflection",
|
||||||
|
// Help: "Total number of HTTP requests by status code and method (reflection-based).",
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// // Compile-time safe and readable; Will be converted into properly ordered list: "200", "GET"
|
||||||
|
// counterVec.With(Labels1{Method: "GET", Code: 200}).Inc()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Safe way with manual implementation (no reflection overhead, as fast as original)
|
||||||
|
//
|
||||||
|
// type Labels2 struct {
|
||||||
|
// promsafe.StructLabelProvider
|
||||||
|
// Code int
|
||||||
|
// Method string
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (c Labels2) ToPrometheusLabels() prometheus.Labels {
|
||||||
|
// return prometheus.Labels{
|
||||||
|
// "code": strconv.Itoa(c.Code), // Convert int to string
|
||||||
|
// "method": c.Method,
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func (c Labels2) ToLabelNames() []string {
|
||||||
|
// return []string{"code", "method"}
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// func safeManualWay() {
|
||||||
|
// counterVec := promsafe.NewCounterVec[Labels2](prometheus.CounterOpts{
|
||||||
|
// Name: "http_requests_total_custom",
|
||||||
|
// Help: "Total number of HTTP requests by status code and method (manual implementation).",
|
||||||
|
// })
|
||||||
|
// counterVec.With(Labels2{Code: 404, Method: "POST"}).Inc()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// Package promsafe also provides compatibility adapter for integration with Prometheus's
|
||||||
|
// `promauto` package, ensuring seamless adoption while preserving type-safety.
|
||||||
|
// Methods that cannot guarantee type safety, such as those using raw `[]string`
|
||||||
|
// label values, are explicitly deprecated and will raise runtime errors.
|
||||||
|
//
|
||||||
|
// A separate package allows conservative users to entirely ignore it. And
|
||||||
|
// whoever wants to use it will do so explicitly, with an opportunity to read
|
||||||
|
// this warning.
|
||||||
|
//
|
||||||
|
// Enjoy promsafe as it's safe!
|
||||||
|
package promsafe
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewCounterVec creates a new CounterVec with type-safe labels.
|
||||||
|
func NewCounterVec[T LabelsProviderMarker](opts prometheus.CounterOpts) *CounterVec[T] {
|
||||||
|
emptyLabels := NewEmptyLabels[T]()
|
||||||
|
inner := prometheus.NewCounterVec(opts, extractLabelNames(emptyLabels))
|
||||||
|
|
||||||
|
return &CounterVec[T]{inner: inner}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CounterVec is a wrapper around prometheus.CounterVec that allows type-safe labels.
|
||||||
|
type CounterVec[T LabelsProviderMarker] struct {
|
||||||
|
inner *prometheus.CounterVec
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricWithLabelValues covers prometheus.CounterVec.GetMetricWithLabelValues
|
||||||
|
// Deprecated: Use GetMetricWith() instead. We can't provide a []string safe implementation in promsafe
|
||||||
|
func (c *CounterVec[T]) GetMetricWithLabelValues(_ ...string) (prometheus.Counter, error) {
|
||||||
|
panic("There can't be a SAFE GetMetricWithLabelValues(). Use GetMetricWith() instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
|
||||||
|
func (c *CounterVec[T]) GetMetricWith(labels T) (prometheus.Counter, error) {
|
||||||
|
return c.inner.GetMetricWith(extractLabelsWithValues(labels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLabelValues covers like prometheus.CounterVec.WithLabelValues.
|
||||||
|
// Deprecated: Use With() instead. We can't provide a []string safe implementation in promsafe
|
||||||
|
func (c *CounterVec[T]) WithLabelValues(_ ...string) prometheus.Counter {
|
||||||
|
panic("There can't be a SAFE WithLabelValues(). Use With() instead")
|
||||||
|
}
|
||||||
|
|
||||||
|
// With behaves like prometheus.CounterVec.With but with type-safe labels.
|
||||||
|
func (c *CounterVec[T]) With(labels T) prometheus.Counter {
|
||||||
|
return c.inner.With(extractLabelsWithValues(labels))
|
||||||
|
}
|
||||||
|
|
||||||
|
// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels.
|
||||||
|
// It still returns a CounterVec, but it's inner prometheus.CounterVec is curried.
|
||||||
|
func (c *CounterVec[T]) CurryWith(labels T) (*CounterVec[T], error) {
|
||||||
|
curriedInner, err := c.inner.CurryWith(extractLabelsWithValues(labels))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
c.inner = curriedInner
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustCurryWith behaves like prometheus.CounterVec.MustCurryWith but with type-safe labels.
|
||||||
|
// It still returns a CounterVec, but it's inner prometheus.CounterVec is curried.
|
||||||
|
func (c *CounterVec[T]) MustCurryWith(labels T) *CounterVec[T] {
|
||||||
|
c.inner = c.inner.MustCurryWith(extractLabelsWithValues(labels))
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsafe returns the underlying prometheus.CounterVec
|
||||||
|
// it's used to call any other method of prometheus.CounterVec that doesn't require type-safe labels
|
||||||
|
func (c *CounterVec[T]) Unsafe() *prometheus.CounterVec {
|
||||||
|
return c.inner
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounter simply creates a new prometheus.Counter.
|
||||||
|
// As it doesn't have any labels, it's already type-safe.
|
||||||
|
// We keep this method just for consistency and interface fulfillment.
|
||||||
|
func NewCounter(opts prometheus.CounterOpts) prometheus.Counter {
|
||||||
|
return prometheus.NewCounter(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCounterFunc wraps a new prometheus.CounterFunc.
|
||||||
|
// As it doesn't have any labels, it's already type-safe.
|
||||||
|
// We keep this method just for consistency and interface fulfillment.
|
||||||
|
func NewCounterFunc(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
|
||||||
|
return prometheus.NewCounterFunc(opts, function)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: other methods (Gauge, Histogram, Summary, etc.)
|
|
@ -0,0 +1,162 @@
|
||||||
|
// Copyright 2024 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 promsafe_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// These are Examples that can be treated as basic smoke tests
|
||||||
|
|
||||||
|
func ExampleNewCounterVec_multiple_labels_manual() {
|
||||||
|
// Manually registering with multiple labels
|
||||||
|
|
||||||
|
type MyCounterLabels struct {
|
||||||
|
promsafe.StructLabelProvider
|
||||||
|
EventType string
|
||||||
|
Success bool
|
||||||
|
Position uint8 // yes, it's a number, but be careful with high-cardinality labels
|
||||||
|
|
||||||
|
ShouldNotBeUsed string `promsafe:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
c := promsafe.NewCounterVec[MyCounterLabels](prometheus.CounterOpts{
|
||||||
|
Name: "items_counted_detailed",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manually register the counter
|
||||||
|
if err := prometheus.Register(c.Unsafe()); err != nil {
|
||||||
|
log.Fatal("could not register: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// and now, because of generics we can call Inc() with filled struct of labels:
|
||||||
|
counter := c.With(MyCounterLabels{
|
||||||
|
EventType: "reservation", Success: true, Position: 1,
|
||||||
|
})
|
||||||
|
counter.Inc()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
}
|
||||||
|
|
||||||
|
// FastMyLabels is a struct that will have a custom method that converts to prometheus.Labels
|
||||||
|
type FastMyLabels struct {
|
||||||
|
promsafe.StructLabelProvider
|
||||||
|
EventType string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToPrometheusLabels does a superfast conversion to labels. So no reflection is involved.
|
||||||
|
func (f FastMyLabels) ToPrometheusLabels() prometheus.Labels {
|
||||||
|
return prometheus.Labels{"event_type": f.EventType, "source": f.Source}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToLabelNames does a superfast label names list. So no reflection is involved.
|
||||||
|
func (f FastMyLabels) ToLabelNames() []string {
|
||||||
|
return []string{"event_type", "source"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleNewCounterVec_fast_safe_labels_provider() {
|
||||||
|
// Note: fast labels provider has a drawback: they can't be declared as inline structs
|
||||||
|
// as we need methods
|
||||||
|
|
||||||
|
c := promsafe.NewCounterVec[FastMyLabels](prometheus.CounterOpts{
|
||||||
|
Name: "items_counted_detailed_fast",
|
||||||
|
})
|
||||||
|
|
||||||
|
// Manually register the counter
|
||||||
|
if err := prometheus.Register(c.Unsafe()); err != nil {
|
||||||
|
log.Fatal("could not register: ", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
counter := c.With(FastMyLabels{
|
||||||
|
EventType: "reservation", Source: "source1",
|
||||||
|
})
|
||||||
|
counter.Inc()
|
||||||
|
|
||||||
|
// Output:
|
||||||
|
}
|
||||||
|
|
||||||
|
// ====================
|
||||||
|
// Benchmark Tests
|
||||||
|
// ====================
|
||||||
|
|
||||||
|
type TestLabels struct {
|
||||||
|
promsafe.StructLabelProvider
|
||||||
|
Label1 string
|
||||||
|
Label2 int
|
||||||
|
Label3 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestLabelsFast struct {
|
||||||
|
promsafe.StructLabelProvider
|
||||||
|
Label1 string
|
||||||
|
Label2 int
|
||||||
|
Label3 bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestLabelsFast) ToPrometheusLabels() prometheus.Labels {
|
||||||
|
return prometheus.Labels{
|
||||||
|
"label1": t.Label1,
|
||||||
|
"label2": strconv.Itoa(t.Label2),
|
||||||
|
"label3": strconv.FormatBool(t.Label3),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t TestLabelsFast) ToLabelNames() []string {
|
||||||
|
return []string{"label1", "label2", "label3"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkCompareCreatingMetric(b *testing.B) {
|
||||||
|
// Note: on stage of creation metrics, Unique metric names are not required,
|
||||||
|
// but let it be for consistency
|
||||||
|
|
||||||
|
b.Run("Prometheus NewCounterVec", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
uniqueMetricName := fmt.Sprintf("test_counter_prometheus_%d_%d", b.N, i)
|
||||||
|
|
||||||
|
_ = prometheus.NewCounterVec(prometheus.CounterOpts{
|
||||||
|
Name: uniqueMetricName,
|
||||||
|
Help: "A test counter created just using prometheus.NewCounterVec",
|
||||||
|
}, []string{"label1", "label2", "label3"})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Promsafe (reflect) NewCounterVec", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
uniqueMetricName := fmt.Sprintf("test_counter_promsafe_%d_%d", b.N, i)
|
||||||
|
|
||||||
|
_ = promsafe.NewCounterVec[TestLabels](prometheus.CounterOpts{
|
||||||
|
Name: uniqueMetricName,
|
||||||
|
Help: "A test counter created using promauto.NewCounterVec",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("Promsafe (fast) NewCounterVec", func(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
uniqueMetricName := fmt.Sprintf("test_counter_promsafe_fast_%d_%d", b.N, i)
|
||||||
|
|
||||||
|
_ = promsafe.NewCounterVec[TestLabelsFast](prometheus.CounterOpts{
|
||||||
|
Name: uniqueMetricName,
|
||||||
|
Help: "A test counter created using promauto.NewCounterVec",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue