Promsafe introduced: Type Safe Labels. (Draft, Discussion is still required)

Signed-off-by: Eugene <eugene@amberpixels.io>
This commit is contained in:
Eugene 2024-11-29 11:57:57 +02:00
parent f53c5ca1a8
commit 879c4741b0
No known key found for this signature in database
GPG Key ID: 51AC89611A689305
6 changed files with 893 additions and 0 deletions

View File

@ -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))
}

View File

@ -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
}

View File

@ -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)
}

View File

@ -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:
}

186
prometheus/promsafe/safe.go Normal file
View File

@ -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.)

View File

@ -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",
})
}
})
}