Skip to content

Commit d7c6010

Browse files
committed
Promsafe feature introduced
1 parent dbf72fc commit d7c6010

File tree

2 files changed

+413
-0
lines changed

2 files changed

+413
-0
lines changed

prometheus/promsafe/safe.go

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,294 @@
1+
// Package promsafe provides safe labeling - strongly typed labels in prometheus metrics.
2+
// Enjoy promsafe as you wish!
3+
package promsafe
4+
5+
import (
6+
"fmt"
7+
"reflect"
8+
"strings"
9+
10+
"github.com/prometheus/client_golang/prometheus"
11+
"github.com/prometheus/client_golang/prometheus/promauto"
12+
)
13+
14+
//
15+
// promsafe configuration: promauto-compatibility, etc
16+
//
17+
18+
// factory stands for a global promauto.Factory to be used (if any)
19+
var factory *promauto.Factory
20+
21+
// SetupGlobalPromauto sets a global promauto.Factory to be used for all promsafe metrics.
22+
// This means that each promsafe.New* call will use this promauto.Factory.
23+
func SetupGlobalPromauto(factoryArg ...promauto.Factory) {
24+
if len(factoryArg) == 0 {
25+
f := promauto.With(prometheus.DefaultRegisterer)
26+
factory = &f
27+
} else {
28+
f := factoryArg[0]
29+
factory = &f
30+
}
31+
}
32+
33+
// promsafeTag is the tag name used for promsafe labels inside structs.
34+
// The tag is optional, as if not present, field is used with snake_cased FieldName.
35+
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
36+
var promsafeTag = "promsafe"
37+
38+
// SetPromsafeTag sets the tag name used for promsafe labels inside structs.
39+
func SetPromsafeTag(tag string) {
40+
promsafeTag = tag
41+
}
42+
43+
// labelProviderMarker is a marker interface for enforcing type-safety.
44+
// With its help we can force our label-related functions to only accept SingleLabelProvider or StructLabelProvider.
45+
type labelProviderMarker interface {
46+
marker()
47+
}
48+
49+
// SingleLabelProvider is a type used for declaring a single label.
50+
// When used as labelProviderMarker it provides just a label name.
51+
// It's meant to be used with single-label metrics only!
52+
// Use StructLabelProvider for multi-label metrics.
53+
type SingleLabelProvider string
54+
55+
var _ labelProviderMarker = SingleLabelProvider("")
56+
57+
func (s SingleLabelProvider) marker() {
58+
panic("marker interface method should never be called")
59+
}
60+
61+
// StructLabelProvider should be embedded in any struct that serves as a label provider.
62+
type StructLabelProvider struct{}
63+
64+
var _ labelProviderMarker = (*StructLabelProvider)(nil)
65+
66+
func (s StructLabelProvider) marker() {
67+
panic("marker interface method should never be called")
68+
}
69+
70+
// handler is a helper struct that helps us to handle type-safe labels
71+
// It holds a label name in case if it's the only label (when SingleLabelProvider is used).
72+
type handler[T labelProviderMarker] struct {
73+
theOnlyLabelName string
74+
}
75+
76+
func newHandler[T labelProviderMarker](labelProvider T) handler[T] {
77+
var h handler[T]
78+
if s, ok := any(labelProvider).(SingleLabelProvider); ok {
79+
h.theOnlyLabelName = string(s)
80+
}
81+
return h
82+
}
83+
84+
// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider)
85+
func (h handler[T]) extractLabels(labelProvider T) []string {
86+
if any(labelProvider) == nil {
87+
return nil
88+
}
89+
if s, ok := any(labelProvider).(SingleLabelProvider); ok {
90+
return []string{string(s)}
91+
}
92+
93+
// Here, then, it can be only a struct, that is a parent of StructLabelProvider
94+
labels := extractLabelFromStruct(labelProvider)
95+
labelNames := make([]string, 0, len(labels))
96+
for k := range labels {
97+
labelNames = append(labelNames, k)
98+
}
99+
return labelNames
100+
}
101+
102+
// extractLabelsWithValues extracts labels names+values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider)
103+
func (h handler[T]) extractLabelsWithValues(labelProvider T) prometheus.Labels {
104+
if any(labelProvider) == nil {
105+
return nil
106+
}
107+
108+
// TODO: let's handle defaults as well, why not?
109+
110+
if s, ok := any(labelProvider).(SingleLabelProvider); ok {
111+
return prometheus.Labels{h.theOnlyLabelName: string(s)}
112+
}
113+
114+
// Here, then, it can be only a struct, that is a parent of StructLabelProvider
115+
return extractLabelFromStruct(labelProvider)
116+
}
117+
118+
// extractLabelValues extracts label string values from a given labelProviderMarker (SingleLabelProvider or StructLabelProvider)
119+
func (h handler[T]) extractLabelValues(labelProvider T) []string {
120+
m := h.extractLabels(labelProvider)
121+
122+
labelValues := make([]string, 0, len(m))
123+
for _, v := range m {
124+
labelValues = append(labelValues, v)
125+
}
126+
return labelValues
127+
}
128+
129+
// NewCounterVecT creates a new CounterVecT with type-safe labels.
130+
func NewCounterVecT[T labelProviderMarker](opts prometheus.CounterOpts, labels T) *CounterVecT[T] {
131+
h := newHandler(labels)
132+
133+
var inner *prometheus.CounterVec
134+
135+
if factory != nil {
136+
inner = factory.NewCounterVec(opts, h.extractLabels(labels))
137+
} else {
138+
inner = prometheus.NewCounterVec(opts, h.extractLabels(labels))
139+
}
140+
141+
return &CounterVecT[T]{
142+
handler: h,
143+
inner: inner,
144+
}
145+
}
146+
147+
// CounterVecT is a wrapper around prometheus.CounterVecT that allows type-safe labels.
148+
type CounterVecT[T labelProviderMarker] struct {
149+
handler[T]
150+
inner *prometheus.CounterVec
151+
}
152+
153+
// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels.
154+
func (c *CounterVecT[T]) GetMetricWithLabelValues(labels T) (prometheus.Counter, error) {
155+
return c.inner.GetMetricWithLabelValues(c.handler.extractLabelValues(labels)...)
156+
}
157+
158+
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
159+
func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) {
160+
return c.inner.GetMetricWith(c.handler.extractLabelsWithValues(labels))
161+
}
162+
163+
// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels.
164+
func (c *CounterVecT[T]) WithLabelValues(labels T) prometheus.Counter {
165+
return c.inner.WithLabelValues(c.handler.extractLabelValues(labels)...)
166+
}
167+
168+
// With behaves like prometheus.CounterVec.With but with type-safe labels.
169+
func (c *CounterVecT[T]) With(labels T) prometheus.Counter {
170+
return c.inner.With(c.handler.extractLabelsWithValues(labels))
171+
}
172+
173+
// CurryWith behaves like prometheus.CounterVec.CurryWith but with type-safe labels.
174+
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
175+
func (c *CounterVecT[T]) CurryWith(labels T) (*CounterVecT[T], error) {
176+
curriedInner, err := c.inner.CurryWith(c.handler.extractLabelsWithValues(labels))
177+
if err != nil {
178+
return nil, err
179+
}
180+
c.inner = curriedInner
181+
return c, nil
182+
}
183+
184+
// MustCurryWith behaves like prometheus.CounterVec.MustCurryWith but with type-safe labels.
185+
// It still returns a CounterVecT, but it's inner prometheus.CounterVec is curried.
186+
func (c *CounterVecT[T]) MustCurryWith(labels T) *CounterVecT[T] {
187+
c.inner = c.inner.MustCurryWith(c.handler.extractLabelsWithValues(labels))
188+
return c
189+
}
190+
191+
// Unsafe returns the underlying prometheus.CounterVec
192+
// it's used to call any other method of prometheus.CounterVec that doesn't require type-safe labels
193+
func (c *CounterVecT[T]) Unsafe() *prometheus.CounterVec {
194+
return c.inner
195+
}
196+
197+
// NewCounterT simply creates a new prometheus.Counter.
198+
// As it doesn't have any labels, it's already type-safe.
199+
// We keep this method just for consistency and interface fulfillment.
200+
func NewCounterT(opts prometheus.CounterOpts) prometheus.Counter {
201+
return prometheus.NewCounter(opts)
202+
}
203+
204+
// NewCounterFuncT simply creates a new prometheus.CounterFunc.
205+
// As it doesn't have any labels, it's already type-safe.
206+
// We keep this method just for consistency and interface fulfillment.
207+
func NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
208+
return prometheus.NewCounterFunc(opts, function)
209+
}
210+
211+
//
212+
// Promauto compatibility
213+
//
214+
215+
// Factory is a promauto-like factory that allows type-safe labels.
216+
// We have to duplicate promauto.Factory logic here, because promauto.Factory's registry is private.
217+
type Factory[T labelProviderMarker] struct {
218+
r prometheus.Registerer
219+
}
220+
221+
// WithAuto is a helper function that allows to use promauto.With with promsafe.With
222+
func WithAuto(r prometheus.Registerer) Factory[labelProviderMarker] {
223+
return Factory[labelProviderMarker]{r: r}
224+
}
225+
226+
// NewCounterVecT works like promauto.NewCounterVec but with type-safe labels
227+
func (f Factory[T]) NewCounterVecT(opts prometheus.CounterOpts, labels T) *CounterVecT[T] {
228+
c := NewCounterVecT(opts, labels)
229+
if f.r != nil {
230+
f.r.MustRegister(c.inner)
231+
}
232+
return c
233+
}
234+
235+
// NewCounterT wraps promauto.NewCounter.
236+
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
237+
func (f Factory[T]) NewCounterT(opts prometheus.CounterOpts) prometheus.Counter {
238+
return promauto.With(f.r).NewCounter(opts)
239+
}
240+
241+
// NewCounterFuncT wraps promauto.NewCounterFunc.
242+
// As it doesn't require any labels, it's already type-safe, and we keep it for consistency.
243+
func (f Factory[T]) NewCounterFuncT(opts prometheus.CounterOpts, function func() float64) prometheus.CounterFunc {
244+
return promauto.With(f.r).NewCounterFunc(opts, function)
245+
}
246+
247+
//
248+
// Helpers
249+
//
250+
251+
// extractLabelFromStruct extracts labels names+values from a given StructLabelProvider
252+
func extractLabelFromStruct(structWithLabels any) prometheus.Labels {
253+
labels := prometheus.Labels{}
254+
255+
val := reflect.Indirect(reflect.ValueOf(structWithLabels))
256+
typ := val.Type()
257+
258+
for i := 0; i < typ.NumField(); i++ {
259+
field := typ.Field(i)
260+
if field.Anonymous {
261+
continue
262+
}
263+
264+
var labelName string
265+
if ourTag := field.Tag.Get(promsafeTag); ourTag != "" {
266+
if ourTag == "-" { // tag="-" means "skip this field"
267+
continue
268+
}
269+
labelName = ourTag
270+
} else {
271+
labelName = toSnakeCase(field.Name)
272+
}
273+
274+
// Note: we don't handle defaults values for now
275+
// so it can have "nil" values, if you had *string fields, etc
276+
fieldVal := fmt.Sprintf("%v", val.Field(i).Interface())
277+
278+
labels[labelName] = fieldVal
279+
}
280+
return labels
281+
}
282+
283+
// Convert struct field names to snake_case for Prometheus label compliance.
284+
func toSnakeCase(s string) string {
285+
s = strings.TrimSpace(s)
286+
var result []rune
287+
for i, r := range s {
288+
if i > 0 && r >= 'A' && r <= 'Z' {
289+
result = append(result, '_')
290+
}
291+
result = append(result, r)
292+
}
293+
return strings.ToLower(string(result))
294+
}

0 commit comments

Comments
 (0)