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