From 5d421be19109b71e21cc3b244013f3733599a4b1 Mon Sep 17 00:00:00 2001 From: Eugene Date: Wed, 28 Aug 2024 19:11:07 +0300 Subject: [PATCH 1/7] Stongly typed labels: `promsafe` feature introduced Signed-off-by: Eugene --- prometheus/promsafe/safe.go | 307 +++++++++++++++++++++++++++++++ prometheus/promsafe/safe_test.go | 131 +++++++++++++ 2 files changed, 438 insertions(+) create mode 100644 prometheus/promsafe/safe.go create mode 100644 prometheus/promsafe/safe_test.go diff --git a/prometheus/promsafe/safe.go b/prometheus/promsafe/safe.go new file mode 100644 index 0000000..019210b --- /dev/null +++ b/prometheus/promsafe/safe.go @@ -0,0 +1,307 @@ +// 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 safe labeling - strongly typed labels in prometheus metrics. +// Enjoy promsafe as you wish! +package promsafe + +import ( + "fmt" + "reflect" + "strings" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +// +// promsafe configuration: promauto-compatibility, etc +// + +// factory stands for a global promauto.Factory to be used (if any) +var factory *promauto.Factory + +// SetupGlobalPromauto sets a global promauto.Factory to be used for all promsafe metrics. +// This means that each promsafe.New* call will use this promauto.Factory. +func SetupGlobalPromauto(factoryArg ...promauto.Factory) { + if len(factoryArg) == 0 { + f := promauto.With(prometheus.DefaultRegisterer) + factory = &f + } else { + f := factoryArg[0] + factory = &f + } +} + +// 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 +} + +// labelProviderMarker is a marker interface for enforcing type-safety. +// With its help we can force our label-related functions to only accept SingleLabelProvider or StructLabelProvider. +type labelProviderMarker interface { + marker() +} + +// SingleLabelProvider is a type used for declaring a single label. +// When used as labelProviderMarker it provides just a label name. +// It's meant to be used with single-label metrics only! +// Use StructLabelProvider for multi-label metrics. +type SingleLabelProvider string + +var _ labelProviderMarker = SingleLabelProvider("") + +func (s SingleLabelProvider) marker() { + panic("marker interface method should never be called") +} + +// StructLabelProvider should be embedded in any struct that serves as a label provider. +type StructLabelProvider struct{} + +var _ labelProviderMarker = (*StructLabelProvider)(nil) + +func (s StructLabelProvider) marker() { + panic("marker interface method should never be called") +} + +// handler is a helper struct that helps us to handle type-safe labels +// It holds a label name in case if it's the only label (when SingleLabelProvider is used). +type handler[T labelProviderMarker] struct { + theOnlyLabelName string +} + +func newHandler[T labelProviderMarker](labelProvider T) handler[T] { + var h handler[T] + if s, ok := any(labelProvider).(SingleLabelProvider); ok { + h.theOnlyLabelName = string(s) + } + return h +} + +// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) +func (h handler[T]) extractLabels(labelProvider T) []string { + if any(labelProvider) == nil { + return nil + } + if s, ok := any(labelProvider).(SingleLabelProvider); ok { + return []string{string(s)} + } + + // Here, then, it can be only a struct, that is a parent of StructLabelProvider + labels := extractLabelFromStruct(labelProvider) + labelNames := make([]string, 0, len(labels)) + for k := range labels { + labelNames = append(labelNames, k) + } + return labelNames +} + +// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) +func (h handler[T]) extractLabelsWithValues(labelProvider T) prometheus.Labels { + if any(labelProvider) == nil { + return nil + } + + // TODO: let's handle defaults as well, why not? + + if s, ok := any(labelProvider).(SingleLabelProvider); ok { + return prometheus.Labels{h.theOnlyLabelName: string(s)} + } + + // Here, then, it can be only a struct, that is a parent of StructLabelProvider + return extractLabelFromStruct(labelProvider) +} + +// extractLabelValues extracts label string values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) +func (h handler[T]) extractLabelValues(labelProvider T) []string { + m := h.extractLabelsWithValues(labelProvider) + + labelValues := make([]string, 0, len(m)) + for _, v := range m { + labelValues = append(labelValues, v) + } + return labelValues +} + +// NewCounterVecT creates a new CounterVecT with type-safe labels. +func NewCounterVecT[T labelProviderMarker](opts prometheus.CounterOpts, labels T) *CounterVecT[T] { + h := newHandler(labels) + + var inner *prometheus.CounterVec + + if factory != nil { + inner = factory.NewCounterVec(opts, h.extractLabels(labels)) + } else { + inner = prometheus.NewCounterVec(opts, h.extractLabels(labels)) + } + + return &CounterVecT[T]{ + handler: h, + inner: inner, + } +} + +// CounterVecT is a wrapper around prometheus.CounterVecT that allows type-safe labels. +type CounterVecT[T labelProviderMarker] struct { + handler[T] + inner *prometheus.CounterVec +} + +// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels. +func (c *CounterVecT[T]) GetMetricWithLabelValues(labels T) (prometheus.Counter, error) { + return c.inner.GetMetricWithLabelValues(c.handler.extractLabelValues(labels)...) +} + +// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels. +func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) { + return c.inner.GetMetricWith(c.handler.extractLabelsWithValues(labels)) +} + +// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels. +func (c *CounterVecT[T]) WithLabelValues(labels T) prometheus.Counter { + return c.inner.WithLabelValues(c.handler.extractLabelValues(labels)...) +} + +// With behaves like prometheus.CounterVec.With but with type-safe labels. +func (c *CounterVecT[T]) With(labels T) prometheus.Counter { + return c.inner.With(c.handler.extractLabelsWithValues(labels)) +} + +// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels. +// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried. +func (c *CounterVecT[T]) CurryWith(labels T) (*CounterVecT[T], error) { + curriedInner, err := c.inner.CurryWith(c.handler.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 CounterVecT, but it's inner prometheus.CounterVec is curried. +func (c *CounterVecT[T]) MustCurryWith(labels T) *CounterVecT[T] { + c.inner = c.inner.MustCurryWith(c.handler.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 *CounterVecT[T]) Unsafe() *prometheus.CounterVec { + return c.inner +} + +// NewCounterT 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 NewCounterT(opts prometheus.CounterOpts) prometheus.Counter { + return prometheus.NewCounter(opts) +} + +// NewCounterFuncT simply creates 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 NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc { + return prometheus.NewCounterFunc(opts, function) +} + +// +// Promauto compatibility +// + +// Factory is a promauto-like factory that allows type-safe labels. +// We have to duplicate promauto.Factory logic here, because promauto.Factory's registry is private. +type Factory[T labelProviderMarker] struct { + r prometheus.Registerer +} + +// WithAuto is a helper function that allows to use promauto.With with promsafe.With +func WithAuto(r prometheus.Registerer) Factory[labelProviderMarker] { + return Factory[labelProviderMarker]{r: r} +} + +// NewCounterVecT works like promauto.NewCounterVec but with type-safe labels +func (f Factory[T]) NewCounterVecT(opts prometheus.CounterOpts, labels T) *CounterVecT[T] { + c := NewCounterVecT(opts, labels) + if f.r != nil { + f.r.MustRegister(c.inner) + } + return c +} + +// NewCounterT 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]) NewCounterT(opts prometheus.CounterOpts) prometheus.Counter { + return promauto.With(f.r).NewCounter(opts) +} + +// NewCounterFuncT 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]) NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc { + return promauto.With(f.r).NewCounterFunc(opts, function) +} + +// +// Helpers +// + +// extractLabelFromStruct extracts labels names+values from a given StructLabelProvider +func extractLabelFromStruct(structWithLabels any) prometheus.Labels { + labels := prometheus.Labels{} + + val := reflect.Indirect(reflect.ValueOf(structWithLabels)) + typ := val.Type() + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if field.Anonymous { + continue + } + + var labelName string + if ourTag := field.Tag.Get(promsafeTag); ourTag != "" { + if ourTag == "-" { // tag="-" means "skip this field" + continue + } + labelName = ourTag + } else { + labelName = toSnakeCase(field.Name) + } + + // Note: we don't handle defaults values for now + // so it can have "nil" values, if you had *string fields, etc + fieldVal := fmt.Sprintf("%v", val.Field(i).Interface()) + + labels[labelName] = fieldVal + } + return labels +} + +// 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)) +} diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go new file mode 100644 index 0000000..d0f65c2 --- /dev/null +++ b/prometheus/promsafe/safe_test.go @@ -0,0 +1,131 @@ +// 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 ( + "log" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promsafe" +) + +func ExampleNewCounterVecT_single_label_manual() { + // Manually registering with a single label + + c := promsafe.NewCounterVecT(prometheus.CounterOpts{ + Name: "items_counted_by_status", + }, promsafe.SingleLabelProvider("status")) + + // Manually register the counter + if err := prometheus.Register(c.Unsafe()); err != nil { + log.Fatal("could not register: ", err.Error()) + } + + c.With("active").Inc() + + // Output: +} + +func ExampleNewCounterVecT_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.NewCounterVecT(prometheus.CounterOpts{ + Name: "items_counted_detailed", + }, &MyCounterLabels{}) + + // 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: +} + +func ExampleNewCounterVecT_promauto_migrated() { + // Examples on how to migrate from promauto to promsafe + // When promauto was using a custom factory with custom registry + + myReg := prometheus.NewRegistry() + + counterOpts := prometheus.CounterOpts{ + Name: "items_counted_detailed_auto", + } + + // Old unsafe code + // promauto.With(myReg).NewCounterVec(counterOpts, []string{"event_type", "source"}) + // becomes: + + type TicketReservationAttemptsLabels struct { + promsafe.StructLabelProvider + EventType string + Source string + } + c := promsafe.WithAuto(myReg).NewCounterVecT(counterOpts, &TicketReservationAttemptsLabels{}) + + c.With(&TicketReservationAttemptsLabels{ + EventType: "reservation", Source: "source1", + }).Inc() + + // Output: +} + +func ExampleNewCounterVecT_promauto_global_migrated() { + // Examples on how to migrate from promauto to promsafe + // when promauto public API was used (with default registry) + + // Setup so every NewCounter* call will use default registry + // like promauto does + // Note: it actually accepts other registry to become a default one + promsafe.SetupGlobalPromauto() + + counterOpts := prometheus.CounterOpts{ + Name: "items_counted_detailed_auto_global", + } + + // Old code: + //c := promauto.NewCounterVec(counterOpts, []string{"status", "source"}) + //c.With(prometheus.Labels{ + // "status": "active", + // "source": "source1", + //}).Inc() + // becomes: + + type TicketReservationAttemptsLabels struct { + promsafe.StructLabelProvider + Status string + Source string + } + c := promsafe.NewCounterVecT(counterOpts, &TicketReservationAttemptsLabels{}) + + c.With(&TicketReservationAttemptsLabels{ + Status: "active", Source: "source1", + }).Inc() + + // Output: +} From aa7e203147efa69269ccb7a94309e74bd6be3c01 Mon Sep 17 00:00:00 2001 From: Eugene Date: Sat, 31 Aug 2024 11:06:19 +0300 Subject: [PATCH 2/7] cleaner example Signed-off-by: Eugene --- prometheus/promsafe/safe_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index d0f65c2..02a68fc 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -51,7 +51,7 @@ func ExampleNewCounterVecT_multiple_labels_manual() { c := promsafe.NewCounterVecT(prometheus.CounterOpts{ Name: "items_counted_detailed", - }, &MyCounterLabels{}) + }, new(MyCounterLabels)) // Manually register the counter if err := prometheus.Register(c.Unsafe()); err != nil { @@ -86,7 +86,7 @@ func ExampleNewCounterVecT_promauto_migrated() { EventType string Source string } - c := promsafe.WithAuto(myReg).NewCounterVecT(counterOpts, &TicketReservationAttemptsLabels{}) + c := promsafe.WithAuto(myReg).NewCounterVecT(counterOpts, new(TicketReservationAttemptsLabels)) c.With(&TicketReservationAttemptsLabels{ EventType: "reservation", Source: "source1", @@ -121,7 +121,7 @@ func ExampleNewCounterVecT_promauto_global_migrated() { Status string Source string } - c := promsafe.NewCounterVecT(counterOpts, &TicketReservationAttemptsLabels{}) + c := promsafe.NewCounterVecT(counterOpts, new(TicketReservationAttemptsLabels)) c.With(&TicketReservationAttemptsLabels{ Status: "active", Source: "source1", From 80149ab4d44185cf5a0dcf0acbf5122451943c5e Mon Sep 17 00:00:00 2001 From: Eugene Date: Mon, 2 Sep 2024 13:32:58 +0300 Subject: [PATCH 3/7] clearer & simpler API. move out single_label_provider as a separate case Signed-off-by: Eugene --- prometheus/promsafe/safe.go | 256 +++++++++++++++++++------------ prometheus/promsafe/safe_test.go | 60 ++++---- 2 files changed, 193 insertions(+), 123 deletions(-) diff --git a/prometheus/promsafe/safe.go b/prometheus/promsafe/safe.go index 019210b..fae3bb8 100644 --- a/prometheus/promsafe/safe.go +++ b/prometheus/promsafe/safe.go @@ -53,140 +53,78 @@ func SetPromsafeTag(tag string) { promsafeTag = tag } -// labelProviderMarker is a marker interface for enforcing type-safety. -// With its help we can force our label-related functions to only accept SingleLabelProvider or StructLabelProvider. -type labelProviderMarker interface { - marker() -} - -// SingleLabelProvider is a type used for declaring a single label. -// When used as labelProviderMarker it provides just a label name. -// It's meant to be used with single-label metrics only! -// Use StructLabelProvider for multi-label metrics. -type SingleLabelProvider string - -var _ labelProviderMarker = SingleLabelProvider("") - -func (s SingleLabelProvider) marker() { - panic("marker interface method should never be called") +// 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 _ labelProviderMarker = (*StructLabelProvider)(nil) +var _ labelsProviderMarker = (*StructLabelProvider)(nil) -func (s StructLabelProvider) marker() { - panic("marker interface method should never be called") +func (s StructLabelProvider) labelsProviderMarker() { + panic("labelsProviderMarker interface method should never be called") } -// handler is a helper struct that helps us to handle type-safe labels -// It holds a label name in case if it's the only label (when SingleLabelProvider is used). -type handler[T labelProviderMarker] struct { - theOnlyLabelName string -} +// 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 -func newHandler[T labelProviderMarker](labelProvider T) handler[T] { - var h handler[T] - if s, ok := any(labelProvider).(SingleLabelProvider); ok { - h.theOnlyLabelName = string(s) - } - return h -} - -// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) -func (h handler[T]) extractLabels(labelProvider T) []string { - if any(labelProvider) == nil { - return nil - } - if s, ok := any(labelProvider).(SingleLabelProvider); ok { - return []string{string(s)} + // Let's Support both Structs or Pointer to Structs given as T + val := reflect.ValueOf(&emptyLabels).Elem() + if val.Kind() == reflect.Ptr { + val.Set(reflect.New(val.Type().Elem())) } - // Here, then, it can be only a struct, that is a parent of StructLabelProvider - labels := extractLabelFromStruct(labelProvider) - labelNames := make([]string, 0, len(labels)) - for k := range labels { - labelNames = append(labelNames, k) - } - return labelNames -} - -// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) -func (h handler[T]) extractLabelsWithValues(labelProvider T) prometheus.Labels { - if any(labelProvider) == nil { - return nil - } - - // TODO: let's handle defaults as well, why not? - - if s, ok := any(labelProvider).(SingleLabelProvider); ok { - return prometheus.Labels{h.theOnlyLabelName: string(s)} - } - - // Here, then, it can be only a struct, that is a parent of StructLabelProvider - return extractLabelFromStruct(labelProvider) -} - -// extractLabelValues extracts label string values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider) -func (h handler[T]) extractLabelValues(labelProvider T) []string { - m := h.extractLabelsWithValues(labelProvider) - - labelValues := make([]string, 0, len(m)) - for _, v := range m { - labelValues = append(labelValues, v) - } - return labelValues + return emptyLabels } // NewCounterVecT creates a new CounterVecT with type-safe labels. -func NewCounterVecT[T labelProviderMarker](opts prometheus.CounterOpts, labels T) *CounterVecT[T] { - h := newHandler(labels) +func NewCounterVecT[T labelsProviderMarker](opts prometheus.CounterOpts) *CounterVecT[T] { + emptyLabels := newEmptyLabels[T]() var inner *prometheus.CounterVec - if factory != nil { - inner = factory.NewCounterVec(opts, h.extractLabels(labels)) + inner = factory.NewCounterVec(opts, extractLabelNames(emptyLabels)) } else { - inner = prometheus.NewCounterVec(opts, h.extractLabels(labels)) + inner = prometheus.NewCounterVec(opts, extractLabelNames(emptyLabels)) } - return &CounterVecT[T]{ - handler: h, - inner: inner, - } + return &CounterVecT[T]{inner: inner} } -// CounterVecT is a wrapper around prometheus.CounterVecT that allows type-safe labels. -type CounterVecT[T labelProviderMarker] struct { - handler[T] +// CounterVecT is a wrapper around prometheus.CounterVec that allows type-safe labels. +type CounterVecT[T labelsProviderMarker] struct { inner *prometheus.CounterVec } // GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels. func (c *CounterVecT[T]) GetMetricWithLabelValues(labels T) (prometheus.Counter, error) { - return c.inner.GetMetricWithLabelValues(c.handler.extractLabelValues(labels)...) + return c.inner.GetMetricWithLabelValues(extractLabelValues(labels)...) } // GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels. func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) { - return c.inner.GetMetricWith(c.handler.extractLabelsWithValues(labels)) + return c.inner.GetMetricWith(extractLabelsWithValues(labels)) } // WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels. func (c *CounterVecT[T]) WithLabelValues(labels T) prometheus.Counter { - return c.inner.WithLabelValues(c.handler.extractLabelValues(labels)...) + return c.inner.WithLabelValues(extractLabelValues(labels)...) } // With behaves like prometheus.CounterVec.With but with type-safe labels. func (c *CounterVecT[T]) With(labels T) prometheus.Counter { - return c.inner.With(c.handler.extractLabelsWithValues(labels)) + return c.inner.With(extractLabelsWithValues(labels)) } // CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels. // It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried. func (c *CounterVecT[T]) CurryWith(labels T) (*CounterVecT[T], error) { - curriedInner, err := c.inner.CurryWith(c.handler.extractLabelsWithValues(labels)) + curriedInner, err := c.inner.CurryWith(extractLabelsWithValues(labels)) if err != nil { return nil, err } @@ -197,7 +135,7 @@ func (c *CounterVecT[T]) CurryWith(labels T) (*CounterVecT[T], error) { // MustCurryWith behaves like prometheus.CounterVec.MustCurryWith but with type-safe labels. // It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried. func (c *CounterVecT[T]) MustCurryWith(labels T) *CounterVecT[T] { - c.inner = c.inner.MustCurryWith(c.handler.extractLabelsWithValues(labels)) + c.inner = c.inner.MustCurryWith(extractLabelsWithValues(labels)) return c } @@ -221,24 +159,110 @@ func NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prome return prometheus.NewCounterFunc(opts, function) } +// +// Shorthand for Metrics with a single label +// + +// singleLabelProviderMarker is a marker interface for enforcing type-safety of SingleLabelProvider. +type singleLabelProviderMarker interface { + singleLabelProviderMarker() +} + +// SingleLabelProvider is a type used for declaring a single label only. +// When declaring a metric it's values used as a label name +// When calling With() it's values used as a label value +type SingleLabelProvider string + +var _ singleLabelProviderMarker = SingleLabelProvider("") + +func (s SingleLabelProvider) singleLabelProviderMarker() { + panic("singleLabelProviderMarker interface method should never be called") +} + +// NewCounterVecT1 creates a new CounterVecT with the only single label +func NewCounterVecT1(opts prometheus.CounterOpts, singleLabelProvider singleLabelProviderMarker) *CounterVecT1 { + // labelName is the string itself + // and singleLabelProviderMarker here can ONLY be SingleLabelProvider + labelName := string(singleLabelProvider.(SingleLabelProvider)) + + var inner *prometheus.CounterVec + if factory != nil { + inner = factory.NewCounterVec(opts, []string{labelName}) + } else { + inner = prometheus.NewCounterVec(opts, []string{labelName}) + } + + return &CounterVecT1{inner: inner, labelName: labelName} +} + +// CounterVecT1 is a wrapper around prometheus.CounterVec that allows a single type-safe label. +type CounterVecT1 struct { + labelName string + inner *prometheus.CounterVec +} + +// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels. +func (c *CounterVecT1) GetMetricWithLabelValues(labelValue string) (prometheus.Counter, error) { + return c.inner.GetMetricWithLabelValues(labelValue) +} + +// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels. +func (c *CounterVecT1) GetMetricWith(labelValue string) (prometheus.Counter, error) { + return c.inner.GetMetricWith(prometheus.Labels{c.labelName: labelValue}) +} + +// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels. +func (c *CounterVecT1) WithLabelValues(labelValue string) prometheus.Counter { + return c.inner.WithLabelValues(labelValue) +} + +// With behaves like prometheus.CounterVec.With but with type-safe labels. +func (c *CounterVecT1) With(labelValue string) prometheus.Counter { + return c.inner.With(prometheus.Labels{c.labelName: labelValue}) +} + +// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels. +// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried. +func (c *CounterVecT1) CurryWith(labelValue string) (*CounterVecT1, error) { + curriedInner, err := c.inner.CurryWith(prometheus.Labels{c.labelName: labelValue}) + 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 CounterVecT, but it's inner prometheus.CounterVec is curried. +func (c *CounterVecT1) MustCurryWith(labelValue string) *CounterVecT1 { + c.inner = c.inner.MustCurryWith(prometheus.Labels{c.labelName: labelValue}) + 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 *CounterVecT1) Unsafe() *prometheus.CounterVec { + return c.inner +} + // // Promauto compatibility // // Factory is a promauto-like factory that allows type-safe labels. // We have to duplicate promauto.Factory logic here, because promauto.Factory's registry is private. -type Factory[T labelProviderMarker] struct { +type Factory[T labelsProviderMarker] struct { r prometheus.Registerer } // WithAuto is a helper function that allows to use promauto.With with promsafe.With -func WithAuto(r prometheus.Registerer) Factory[labelProviderMarker] { - return Factory[labelProviderMarker]{r: r} +func WithAuto[T labelsProviderMarker](r prometheus.Registerer) Factory[T] { + return Factory[T]{r: r} } // NewCounterVecT works like promauto.NewCounterVec but with type-safe labels -func (f Factory[T]) NewCounterVecT(opts prometheus.CounterOpts, labels T) *CounterVecT[T] { - c := NewCounterVecT(opts, labels) +func (f Factory[T]) NewCounterVecT(opts prometheus.CounterOpts) *CounterVecT[T] { + c := NewCounterVecT[T](opts) if f.r != nil { f.r.MustRegister(c.inner) } @@ -257,10 +281,50 @@ func (f Factory[T]) NewCounterFuncT(opts prometheus.CounterOpts, function func() return promauto.With(f.r).NewCounterFunc(opts, function) } +// TODO: we can't use Factory with NewCounterT1. If we need, then we need a new type-less Factory + // // Helpers // +// 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 + } + + // TODO: let's handle defaults as well, why not? + + // Here, then, it can be only a struct, that is a parent of StructLabelProvider + return extractLabelFromStruct(labelProvider) +} + +// extractLabelValues extracts label string values from a given labelsProviderMarker (parent instance of aStructLabelProvider) +func extractLabelValues(labelProvider labelsProviderMarker) []string { + m := extractLabelsWithValues(labelProvider) + + labelValues := make([]string, 0, len(m)) + for _, v := range m { + labelValues = append(labelValues, v) + } + return labelValues +} + +// extractLabelNames extracts labels names from a given labelsProviderMarker (parent instance of aStructLabelProvider) +func extractLabelNames(labelProvider labelsProviderMarker) []string { + if any(labelProvider) == nil { + return nil + } + + // Here, then, it can be only a struct, that is a parent of StructLabelProvider + labels := extractLabelFromStruct(labelProvider) + labelNames := make([]string, 0, len(labels)) + for k := range labels { + labelNames = append(labelNames, k) + } + return labelNames +} + // extractLabelFromStruct extracts labels names+values from a given StructLabelProvider func extractLabelFromStruct(structWithLabels any) prometheus.Labels { labels := prometheus.Labels{} diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index 02a68fc..3d78fef 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -17,26 +17,10 @@ import ( "log" "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promsafe" ) -func ExampleNewCounterVecT_single_label_manual() { - // Manually registering with a single label - - c := promsafe.NewCounterVecT(prometheus.CounterOpts{ - Name: "items_counted_by_status", - }, promsafe.SingleLabelProvider("status")) - - // Manually register the counter - if err := prometheus.Register(c.Unsafe()); err != nil { - log.Fatal("could not register: ", err.Error()) - } - - c.With("active").Inc() - - // Output: -} - func ExampleNewCounterVecT_multiple_labels_manual() { // Manually registering with multiple labels @@ -49,17 +33,17 @@ func ExampleNewCounterVecT_multiple_labels_manual() { ShouldNotBeUsed string `promsafe:"-"` } - c := promsafe.NewCounterVecT(prometheus.CounterOpts{ + c := promsafe.NewCounterVecT[MyCounterLabels](prometheus.CounterOpts{ Name: "items_counted_detailed", - }, new(MyCounterLabels)) + }) // Manually register the counter if err := prometheus.Register(c.Unsafe()); err != nil { - log.Fatal("could not register: ", err.Error()) + log.Fatal("could not register1: ", err.Error()) } // and now, because of generics we can call Inc() with filled struct of labels: - counter := c.With(&MyCounterLabels{ + counter := c.With(MyCounterLabels{ EventType: "reservation", Success: true, Position: 1, }) counter.Inc() @@ -81,14 +65,14 @@ func ExampleNewCounterVecT_promauto_migrated() { // promauto.With(myReg).NewCounterVec(counterOpts, []string{"event_type", "source"}) // becomes: - type TicketReservationAttemptsLabels struct { + type MyLabels struct { promsafe.StructLabelProvider EventType string Source string } - c := promsafe.WithAuto(myReg).NewCounterVecT(counterOpts, new(TicketReservationAttemptsLabels)) + c := promsafe.WithAuto[MyLabels](myReg).NewCounterVecT(counterOpts) - c.With(&TicketReservationAttemptsLabels{ + c.With(MyLabels{ EventType: "reservation", Source: "source1", }).Inc() @@ -103,6 +87,10 @@ func ExampleNewCounterVecT_promauto_global_migrated() { // like promauto does // Note: it actually accepts other registry to become a default one promsafe.SetupGlobalPromauto() + defer func() { + // cleanup for other examples + promsafe.SetupGlobalPromauto(promauto.With(nil)) + }() counterOpts := prometheus.CounterOpts{ Name: "items_counted_detailed_auto_global", @@ -116,16 +104,34 @@ func ExampleNewCounterVecT_promauto_global_migrated() { //}).Inc() // becomes: - type TicketReservationAttemptsLabels struct { + type MyLabels struct { promsafe.StructLabelProvider Status string Source string } - c := promsafe.NewCounterVecT(counterOpts, new(TicketReservationAttemptsLabels)) + c := promsafe.NewCounterVecT[*MyLabels](counterOpts) - c.With(&TicketReservationAttemptsLabels{ + c.With(&MyLabels{ Status: "active", Source: "source1", }).Inc() // Output: } + +func ExampleNewCounterVecT_single_label_manual() { + // Manually registering with a single label + // Example of usage of shorthand: no structs no generics, but one string only + + c := promsafe.NewCounterVecT1(prometheus.CounterOpts{ + Name: "items_counted_by_status", + }, promsafe.SingleLabelProvider("status")) + + // Manually register the counter + if err := prometheus.Register(c.Unsafe()); err != nil { + log.Fatal("could not register: ", err.Error()) + } + + c.With("active").Inc() + + // Output: +} From 6724abaa0b51ebc67afba21e3b30ce5056368ddf Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 20 Sep 2024 00:19:09 +0300 Subject: [PATCH 4/7] simplify SingleLabeled metrics: just accept a string when registering a metric Signed-off-by: Eugene --- prometheus/promsafe/safe.go | 22 +--------------------- prometheus/promsafe/safe_test.go | 2 +- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/prometheus/promsafe/safe.go b/prometheus/promsafe/safe.go index fae3bb8..5caac20 100644 --- a/prometheus/promsafe/safe.go +++ b/prometheus/promsafe/safe.go @@ -163,28 +163,8 @@ func NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prome // Shorthand for Metrics with a single label // -// singleLabelProviderMarker is a marker interface for enforcing type-safety of SingleLabelProvider. -type singleLabelProviderMarker interface { - singleLabelProviderMarker() -} - -// SingleLabelProvider is a type used for declaring a single label only. -// When declaring a metric it's values used as a label name -// When calling With() it's values used as a label value -type SingleLabelProvider string - -var _ singleLabelProviderMarker = SingleLabelProvider("") - -func (s SingleLabelProvider) singleLabelProviderMarker() { - panic("singleLabelProviderMarker interface method should never be called") -} - // NewCounterVecT1 creates a new CounterVecT with the only single label -func NewCounterVecT1(opts prometheus.CounterOpts, singleLabelProvider singleLabelProviderMarker) *CounterVecT1 { - // labelName is the string itself - // and singleLabelProviderMarker here can ONLY be SingleLabelProvider - labelName := string(singleLabelProvider.(SingleLabelProvider)) - +func NewCounterVecT1(opts prometheus.CounterOpts, labelName string) *CounterVecT1 { var inner *prometheus.CounterVec if factory != nil { inner = factory.NewCounterVec(opts, []string{labelName}) diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index 3d78fef..304055c 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -124,7 +124,7 @@ func ExampleNewCounterVecT_single_label_manual() { c := promsafe.NewCounterVecT1(prometheus.CounterOpts{ Name: "items_counted_by_status", - }, promsafe.SingleLabelProvider("status")) + }, "status") // Manually register the counter if err := prometheus.Register(c.Unsafe()); err != nil { From 83a2a655ae93d945069002f92690b3765470b7b4 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 20 Sep 2024 00:32:13 +0300 Subject: [PATCH 5/7] add example showing that using pointer to labels struct is fine Signed-off-by: Eugene --- prometheus/promsafe/safe_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index 304055c..56b3330 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -118,6 +118,28 @@ func ExampleNewCounterVecT_promauto_global_migrated() { // Output: } +func ExampleNewCounterVecT_pointer_to_labels_promauto() { + // It's possible to use pointer to labels struct + myReg := prometheus.NewRegistry() + + counterOpts := prometheus.CounterOpts{ + Name: "items_counted_detailed_ptr", + } + + type MyLabels struct { + promsafe.StructLabelProvider + EventType string + Source string + } + c := promsafe.WithAuto[*MyLabels](myReg).NewCounterVecT(counterOpts) + + c.With(&MyLabels{ + EventType: "reservation", Source: "source1", + }).Inc() + + // Output: +} + func ExampleNewCounterVecT_single_label_manual() { // Manually registering with a single label // Example of usage of shorthand: no structs no generics, but one string only From 5b52b012e1c5c3a983e329a7fae7a0c4b92612b2 Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 20 Sep 2024 11:13:27 +0300 Subject: [PATCH 6/7] promauto: make it available to avoid reflect when building labels Signed-off-by: Eugene --- prometheus/promsafe/safe.go | 20 ++++++++++++++++++- prometheus/promsafe/safe_test.go | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/prometheus/promsafe/safe.go b/prometheus/promsafe/safe.go index 5caac20..ca930b3 100644 --- a/prometheus/promsafe/safe.go +++ b/prometheus/promsafe/safe.go @@ -43,6 +43,13 @@ func SetupGlobalPromauto(factoryArg ...promauto.Factory) { } } +// CustomLabelsProvider is an interface that allows to convert anything to a prometheus.Labels +// It allows to provide your own FAST implementation of Struct->prometheus.Labels conversion +// without using reflection. +type CustomLabelsProvider interface { + ToPrometheusLabels() prometheus.Labels +} + // 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. @@ -273,6 +280,10 @@ func extractLabelsWithValues(labelProvider labelsProviderMarker) prometheus.Labe return nil } + if clp, ok := labelProvider.(CustomLabelsProvider); ok { + return clp.ToPrometheusLabels() + } + // TODO: let's handle defaults as well, why not? // Here, then, it can be only a struct, that is a parent of StructLabelProvider @@ -291,13 +302,20 @@ func extractLabelValues(labelProvider labelsProviderMarker) []string { } // extractLabelNames extracts labels names from a given labelsProviderMarker (parent instance of aStructLabelProvider) +// Deprecated: refactor is required. Order of result slice is not guaranteed. func extractLabelNames(labelProvider labelsProviderMarker) []string { if any(labelProvider) == nil { return nil } + var labels prometheus.Labels + if clp, ok := labelProvider.(CustomLabelsProvider); ok { + labels = clp.ToPrometheusLabels() + } else { + labels = extractLabelFromStruct(labelProvider) + } + // Here, then, it can be only a struct, that is a parent of StructLabelProvider - labels := extractLabelFromStruct(labelProvider) labelNames := make([]string, 0, len(labels)) for k := range labels { labelNames = append(labelNames, k) diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index 56b3330..9aaecea 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -21,6 +21,10 @@ import ( "github.com/prometheus/client_golang/prometheus/promsafe" ) +// Important: This is not a test file. These are only examples! +// These can be considered smoke tests, but nothing more. +// TODO: Write real tests + func ExampleNewCounterVecT_multiple_labels_manual() { // Manually registering with multiple labels @@ -140,6 +144,35 @@ func ExampleNewCounterVecT_pointer_to_labels_promauto() { // 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 fast conversion to labels. No reflection involved. +func (f FastMyLabels) ToPrometheusLabels() prometheus.Labels { + return prometheus.Labels{"event_type": f.EventType, "source": f.Source} +} + +func ExampleNewCounterVecT_fast_safe_labels_provider() { + // It's possible to use pointer to labels struct + myReg := prometheus.NewRegistry() + + counterOpts := prometheus.CounterOpts{ + Name: "items_counted_fast", + } + + c := promsafe.WithAuto[FastMyLabels](myReg).NewCounterVecT(counterOpts) + + c.With(FastMyLabels{ + EventType: "reservation", Source: "source1", + }).Inc() + + // Output: +} + func ExampleNewCounterVecT_single_label_manual() { // Manually registering with a single label // Example of usage of shorthand: no structs no generics, but one string only From c52d495061fc2a6ba0c0a7443a0da33de2b94fcf Mon Sep 17 00:00:00 2001 From: Eugene Date: Fri, 20 Sep 2024 11:41:11 +0300 Subject: [PATCH 7/7] promsafe: improvements. ability to add fast implementation of toLabelNames Signed-off-by: Eugene --- prometheus/promsafe/safe.go | 86 +++++++++++++++++++------------- prometheus/promsafe/safe_test.go | 9 +++- 2 files changed, 59 insertions(+), 36 deletions(-) diff --git a/prometheus/promsafe/safe.go b/prometheus/promsafe/safe.go index ca930b3..c1fee8a 100644 --- a/prometheus/promsafe/safe.go +++ b/prometheus/promsafe/safe.go @@ -50,6 +50,13 @@ type CustomLabelsProvider interface { ToPrometheusLabels() prometheus.Labels } +// CustomLabelNamesProvider is an interface that allows to convert anything to an order list of label names +// Result of it is considered to be when registering new metric. As we need proper ORDERED list of label names +// It allows to provide your own FAST implementation of Struct->[]string conversion without any reflection +type CustomLabelNamesProvider interface { + ToLabelNames() []string +} + // 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. @@ -108,9 +115,10 @@ type CounterVecT[T labelsProviderMarker] struct { inner *prometheus.CounterVec } -// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels. -func (c *CounterVecT[T]) GetMetricWithLabelValues(labels T) (prometheus.Counter, error) { - return c.inner.GetMetricWithLabelValues(extractLabelValues(labels)...) +// GetMetricWithLabelValues covers prometheus.CounterVec.GetMetricWithLabelValues +// Deprecated: Use GetMetricWith() instead. We can't provide a []string safe implementation in promsafe +func (c *CounterVecT[T]) GetMetricWithLabelValues(lvs ...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. @@ -118,9 +126,10 @@ func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) { return c.inner.GetMetricWith(extractLabelsWithValues(labels)) } -// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels. -func (c *CounterVecT[T]) WithLabelValues(labels T) prometheus.Counter { - return c.inner.WithLabelValues(extractLabelValues(labels)...) +// WithLabelValues covers like prometheus.CounterVec.WithLabelValues. +// Deprecated: Use With() instead. We can't provide a []string safe implementation in promsafe +func (c *CounterVecT[T]) WithLabelValues(lvs ...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. @@ -194,6 +203,7 @@ func (c *CounterVecT1) GetMetricWithLabelValues(labelValue string) (prometheus.C } // GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels. +// We keep this function for consistency. Actually is useless and it's the same as GetMetricWithLabelValues func (c *CounterVecT1) GetMetricWith(labelValue string) (prometheus.Counter, error) { return c.inner.GetMetricWith(prometheus.Labels{c.labelName: labelValue}) } @@ -204,6 +214,7 @@ func (c *CounterVecT1) WithLabelValues(labelValue string) prometheus.Counter { } // With behaves like prometheus.CounterVec.With but with type-safe labels. +// We keep this function for consistency. Actually is useless and it's the same as WithLabelValues func (c *CounterVecT1) With(labelValue string) prometheus.Counter { return c.inner.With(prometheus.Labels{c.labelName: labelValue}) } @@ -284,42 +295,45 @@ func extractLabelsWithValues(labelProvider labelsProviderMarker) prometheus.Labe return clp.ToPrometheusLabels() } - // TODO: let's handle defaults as well, why not? - // Here, then, it can be only a struct, that is a parent of StructLabelProvider return extractLabelFromStruct(labelProvider) } -// extractLabelValues extracts label string values from a given labelsProviderMarker (parent instance of aStructLabelProvider) -func extractLabelValues(labelProvider labelsProviderMarker) []string { - m := extractLabelsWithValues(labelProvider) - - labelValues := make([]string, 0, len(m)) - for _, v := range m { - labelValues = append(labelValues, v) - } - return labelValues -} - // extractLabelNames extracts labels names from a given labelsProviderMarker (parent instance of aStructLabelProvider) -// Deprecated: refactor is required. Order of result slice is not guaranteed. func extractLabelNames(labelProvider labelsProviderMarker) []string { if any(labelProvider) == nil { return nil } - var labels prometheus.Labels - if clp, ok := labelProvider.(CustomLabelsProvider); ok { - labels = clp.ToPrometheusLabels() - } else { - labels = extractLabelFromStruct(labelProvider) + // If custom implementation is done, just do it + if clp, ok := labelProvider.(CustomLabelNamesProvider); ok { + return clp.ToLabelNames() } - // Here, then, it can be only a struct, that is a parent of StructLabelProvider - labelNames := make([]string, 0, len(labels)) - for k := range labels { - labelNames = append(labelNames, k) + // Fallback to slow implementation via reflect + // We'll return label values in order of fields in the struct + val := reflect.Indirect(reflect.ValueOf(labelProvider)) + typ := val.Type() + + labelNames := make([]string, 0, typ.NumField()) + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + if field.Anonymous { + continue + } + + var fieldName string + if ourTag := field.Tag.Get(promsafeTag); ourTag == "-" { + continue + } else if ourTag != "" { + fieldName = ourTag + } else { + fieldName = toSnakeCase(field.Name) + } + + labelNames = append(labelNames, fieldName) } + return labelNames } @@ -346,15 +360,19 @@ func extractLabelFromStruct(structWithLabels any) prometheus.Labels { labelName = toSnakeCase(field.Name) } - // Note: we don't handle defaults values for now - // so it can have "nil" values, if you had *string fields, etc - fieldVal := fmt.Sprintf("%v", val.Field(i).Interface()) - - labels[labelName] = fieldVal + labels[labelName] = stringifyLabelValue(val.Field(i)) } return labels } +// 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 { + 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) diff --git a/prometheus/promsafe/safe_test.go b/prometheus/promsafe/safe_test.go index 9aaecea..977a031 100644 --- a/prometheus/promsafe/safe_test.go +++ b/prometheus/promsafe/safe_test.go @@ -43,7 +43,7 @@ func ExampleNewCounterVecT_multiple_labels_manual() { // Manually register the counter if err := prometheus.Register(c.Unsafe()); err != nil { - log.Fatal("could not register1: ", err.Error()) + log.Fatal("could not register: ", err.Error()) } // and now, because of generics we can call Inc() with filled struct of labels: @@ -151,11 +151,16 @@ type FastMyLabels struct { Source string } -// ToPrometheusLabels does a fast conversion to labels. No reflection involved. +// 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 ExampleNewCounterVecT_fast_safe_labels_provider() { // It's possible to use pointer to labels struct myReg := prometheus.NewRegistry()