Skip to content

Commit 7828200

Browse files
committed
Promsafe introduced: Type Safe Labels. (Draft, Discussion is still required)
1 parent f53c5ca commit 7828200

File tree

6 files changed

+893
-0
lines changed

6 files changed

+893
-0
lines changed
+161
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
// Copyright 2024 The Prometheus Authors
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package promsafe
15+
16+
import (
17+
"fmt"
18+
"reflect"
19+
"strings"
20+
21+
"github.com/prometheus/client_golang/prometheus"
22+
)
23+
24+
// LabelsProvider is an interface that allows to convert anything into prometheus.Labels
25+
// It allows to provide your own FAST implementation of Struct->prometheus.Labels conversion
26+
// without using reflection.
27+
type LabelsProvider interface {
28+
ToPrometheusLabels() prometheus.Labels
29+
ToLabelNames() []string
30+
}
31+
32+
// LabelsProviderMarker is a marker interface for enforcing type-safety of StructLabelProvider.
33+
type LabelsProviderMarker interface {
34+
labelsProviderMarker()
35+
}
36+
37+
// StructLabelProvider should be embedded in any struct that serves as a label provider.
38+
type StructLabelProvider struct{}
39+
40+
var _ LabelsProviderMarker = (*StructLabelProvider)(nil)
41+
42+
func (s StructLabelProvider) labelsProviderMarker() {
43+
panic("LabelsProviderMarker interface method should never be called")
44+
}
45+
46+
// NewEmptyLabels creates a new empty labels instance of type T
47+
// It's a bit tricky as we want to support both structs and pointers to structs
48+
// e.g. &MyLabels{StructLabelProvider} or MyLabels{StructLabelProvider}
49+
func NewEmptyLabels[T LabelsProviderMarker]() T {
50+
var emptyLabels T
51+
52+
val := reflect.ValueOf(&emptyLabels).Elem()
53+
if val.Kind() == reflect.Ptr {
54+
ptrType := val.Type().Elem()
55+
newValue := reflect.New(ptrType).Interface().(T)
56+
return newValue
57+
}
58+
59+
return emptyLabels
60+
}
61+
62+
//
63+
// Helpers
64+
//
65+
66+
// promsafeTag is the tag name used for promsafe labels inside structs.
67+
// The tag is optional, as if not present, field is used with snake_cased FieldName.
68+
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
69+
var promsafeTag = "promsafe"
70+
71+
// SetPromsafeTag sets the tag name used for promsafe labels inside structs.
72+
func SetPromsafeTag(tag string) {
73+
promsafeTag = tag
74+
}
75+
76+
// iterateStructFields iterates over struct fields, calling the given function for each field.
77+
func iterateStructFields(structValue any, fn func(labelName string, fieldValue reflect.Value)) {
78+
val := reflect.Indirect(reflect.ValueOf(structValue))
79+
typ := val.Type()
80+
81+
for i := 0; i < typ.NumField(); i++ {
82+
field := typ.Field(i)
83+
if field.Anonymous {
84+
continue
85+
}
86+
87+
// Handle tag logic centrally
88+
var labelName string
89+
if ourTag := field.Tag.Get(promsafeTag); ourTag == "-" {
90+
continue // Skip field
91+
} else if ourTag != "" {
92+
labelName = ourTag
93+
} else {
94+
labelName = toSnakeCase(field.Name)
95+
}
96+
97+
fn(labelName, val.Field(i))
98+
}
99+
}
100+
101+
// extractLabelsWithValues extracts labels names+values from a given LabelsProviderMarker (parent instance of a StructLabelProvider)
102+
func extractLabelsWithValues(labelProvider LabelsProviderMarker) prometheus.Labels {
103+
if any(labelProvider) == nil {
104+
return nil
105+
}
106+
107+
if clp, ok := labelProvider.(LabelsProvider); ok {
108+
return clp.ToPrometheusLabels()
109+
}
110+
111+
// extracting labels from a struct
112+
labels := prometheus.Labels{}
113+
iterateStructFields(labelProvider, func(labelName string, fieldValue reflect.Value) {
114+
labels[labelName] = stringifyLabelValue(fieldValue)
115+
})
116+
return labels
117+
}
118+
119+
// extractLabelNames extracts labels names from a given LabelsProviderMarker (parent instance of aStructLabelProvider)
120+
func extractLabelNames(labelProvider LabelsProviderMarker) []string {
121+
if any(labelProvider) == nil {
122+
return nil
123+
}
124+
125+
// If custom implementation is done, just do it
126+
if lp, ok := labelProvider.(LabelsProvider); ok {
127+
return lp.ToLabelNames()
128+
}
129+
130+
// Fallback to slow implementation via reflect
131+
// Important! We return label names in order of fields in the struct
132+
labelNames := make([]string, 0)
133+
iterateStructFields(labelProvider, func(labelName string, fieldValue reflect.Value) {
134+
labelNames = append(labelNames, labelName)
135+
})
136+
137+
return labelNames
138+
}
139+
140+
// stringifyLabelValue makes up a valid string value from a given field's value
141+
// It's used ONLY in fallback reflect mode
142+
// Field value might be a pointer, that's why we do reflect.Indirect()
143+
// Note: in future we can handle default values here as well
144+
func stringifyLabelValue(v reflect.Value) string {
145+
// TODO: we probably want to handle custom type processing here
146+
// e.g. sometimes booleans need to be "on"/"off" instead of "true"/"false"
147+
return fmt.Sprintf("%v", reflect.Indirect(v).Interface())
148+
}
149+
150+
// Convert struct field names to snake_case for Prometheus label compliance.
151+
func toSnakeCase(s string) string {
152+
s = strings.TrimSpace(s)
153+
var result []rune
154+
for i, r := range s {
155+
if i > 0 && r >= 'A' && r <= 'Z' {
156+
result = append(result, '_')
157+
}
158+
result = append(result, r)
159+
}
160+
return strings.ToLower(string(result))
161+
}

0 commit comments

Comments
 (0)