Skip to content

Commit c52d495

Browse files
committed
promsafe: improvements. ability to add fast implementation of toLabelNames
Signed-off-by: Eugene <[email protected]>
1 parent 5b52b01 commit c52d495

File tree

2 files changed

+59
-36
lines changed

2 files changed

+59
-36
lines changed

prometheus/promsafe/safe.go

Lines changed: 52 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,13 @@ type CustomLabelsProvider interface {
5050
ToPrometheusLabels() prometheus.Labels
5151
}
5252

53+
// CustomLabelNamesProvider is an interface that allows to convert anything to an order list of label names
54+
// Result of it is considered to be when registering new metric. As we need proper ORDERED list of label names
55+
// It allows to provide your own FAST implementation of Struct->[]string conversion without any reflection
56+
type CustomLabelNamesProvider interface {
57+
ToLabelNames() []string
58+
}
59+
5360
// promsafeTag is the tag name used for promsafe labels inside structs.
5461
// The tag is optional, as if not present, field is used with snake_cased FieldName.
5562
// It's useful to use a tag when you want to override the default naming or exclude a field from the metric.
@@ -108,19 +115,21 @@ type CounterVecT[T labelsProviderMarker] struct {
108115
inner *prometheus.CounterVec
109116
}
110117

111-
// GetMetricWithLabelValues behaves like prometheus.CounterVec.GetMetricWithLabelValues but with type-safe labels.
112-
func (c *CounterVecT[T]) GetMetricWithLabelValues(labels T) (prometheus.Counter, error) {
113-
return c.inner.GetMetricWithLabelValues(extractLabelValues(labels)...)
118+
// GetMetricWithLabelValues covers prometheus.CounterVec.GetMetricWithLabelValues
119+
// Deprecated: Use GetMetricWith() instead. We can't provide a []string safe implementation in promsafe
120+
func (c *CounterVecT[T]) GetMetricWithLabelValues(lvs ...string) (prometheus.Counter, error) {
121+
panic("There can't be a SAFE GetMetricWithLabelValues(). Use GetMetricWith() instead")
114122
}
115123

116124
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
117125
func (c *CounterVecT[T]) GetMetricWith(labels T) (prometheus.Counter, error) {
118126
return c.inner.GetMetricWith(extractLabelsWithValues(labels))
119127
}
120128

121-
// WithLabelValues behaves like prometheus.CounterVec.WithLabelValues but with type-safe labels.
122-
func (c *CounterVecT[T]) WithLabelValues(labels T) prometheus.Counter {
123-
return c.inner.WithLabelValues(extractLabelValues(labels)...)
129+
// WithLabelValues covers like prometheus.CounterVec.WithLabelValues.
130+
// Deprecated: Use With() instead. We can't provide a []string safe implementation in promsafe
131+
func (c *CounterVecT[T]) WithLabelValues(lvs ...string) prometheus.Counter {
132+
panic("There can't be a SAFE WithLabelValues(). Use With() instead")
124133
}
125134

126135
// With behaves like prometheus.CounterVec.With but with type-safe labels.
@@ -194,6 +203,7 @@ func (c *CounterVecT1) GetMetricWithLabelValues(labelValue string) (prometheus.C
194203
}
195204

196205
// GetMetricWith behaves like prometheus.CounterVec.GetMetricWith but with type-safe labels.
206+
// We keep this function for consistency. Actually is useless and it's the same as GetMetricWithLabelValues
197207
func (c *CounterVecT1) GetMetricWith(labelValue string) (prometheus.Counter, error) {
198208
return c.inner.GetMetricWith(prometheus.Labels{c.labelName: labelValue})
199209
}
@@ -204,6 +214,7 @@ func (c *CounterVecT1) WithLabelValues(labelValue string) prometheus.Counter {
204214
}
205215

206216
// With behaves like prometheus.CounterVec.With but with type-safe labels.
217+
// We keep this function for consistency. Actually is useless and it's the same as WithLabelValues
207218
func (c *CounterVecT1) With(labelValue string) prometheus.Counter {
208219
return c.inner.With(prometheus.Labels{c.labelName: labelValue})
209220
}
@@ -284,42 +295,45 @@ func extractLabelsWithValues(labelProvider labelsProviderMarker) prometheus.Labe
284295
return clp.ToPrometheusLabels()
285296
}
286297

287-
// TODO: let's handle defaults as well, why not?
288-
289298
// Here, then, it can be only a struct, that is a parent of StructLabelProvider
290299
return extractLabelFromStruct(labelProvider)
291300
}
292301

293-
// extractLabelValues extracts label string values from a given labelsProviderMarker (parent instance of aStructLabelProvider)
294-
func extractLabelValues(labelProvider labelsProviderMarker) []string {
295-
m := extractLabelsWithValues(labelProvider)
296-
297-
labelValues := make([]string, 0, len(m))
298-
for _, v := range m {
299-
labelValues = append(labelValues, v)
300-
}
301-
return labelValues
302-
}
303-
304302
// extractLabelNames extracts labels names from a given labelsProviderMarker (parent instance of aStructLabelProvider)
305-
// Deprecated: refactor is required. Order of result slice is not guaranteed.
306303
func extractLabelNames(labelProvider labelsProviderMarker) []string {
307304
if any(labelProvider) == nil {
308305
return nil
309306
}
310307

311-
var labels prometheus.Labels
312-
if clp, ok := labelProvider.(CustomLabelsProvider); ok {
313-
labels = clp.ToPrometheusLabels()
314-
} else {
315-
labels = extractLabelFromStruct(labelProvider)
308+
// If custom implementation is done, just do it
309+
if clp, ok := labelProvider.(CustomLabelNamesProvider); ok {
310+
return clp.ToLabelNames()
316311
}
317312

318-
// Here, then, it can be only a struct, that is a parent of StructLabelProvider
319-
labelNames := make([]string, 0, len(labels))
320-
for k := range labels {
321-
labelNames = append(labelNames, k)
313+
// Fallback to slow implementation via reflect
314+
// We'll return label values in order of fields in the struct
315+
val := reflect.Indirect(reflect.ValueOf(labelProvider))
316+
typ := val.Type()
317+
318+
labelNames := make([]string, 0, typ.NumField())
319+
for i := 0; i < typ.NumField(); i++ {
320+
field := typ.Field(i)
321+
if field.Anonymous {
322+
continue
323+
}
324+
325+
var fieldName string
326+
if ourTag := field.Tag.Get(promsafeTag); ourTag == "-" {
327+
continue
328+
} else if ourTag != "" {
329+
fieldName = ourTag
330+
} else {
331+
fieldName = toSnakeCase(field.Name)
332+
}
333+
334+
labelNames = append(labelNames, fieldName)
322335
}
336+
323337
return labelNames
324338
}
325339

@@ -346,15 +360,19 @@ func extractLabelFromStruct(structWithLabels any) prometheus.Labels {
346360
labelName = toSnakeCase(field.Name)
347361
}
348362

349-
// Note: we don't handle defaults values for now
350-
// so it can have "nil" values, if you had *string fields, etc
351-
fieldVal := fmt.Sprintf("%v", val.Field(i).Interface())
352-
353-
labels[labelName] = fieldVal
363+
labels[labelName] = stringifyLabelValue(val.Field(i))
354364
}
355365
return labels
356366
}
357367

368+
// stringifyLabelValue makes up a valid string value from a given field's value
369+
// It's used ONLY in fallback reflect mode
370+
// Field value might be a pointer, that's why we do reflect.Indirect()
371+
// Note: in future we can handle default values here as well
372+
func stringifyLabelValue(v reflect.Value) string {
373+
return fmt.Sprintf("%v", reflect.Indirect(v).Interface())
374+
}
375+
358376
// Convert struct field names to snake_case for Prometheus label compliance.
359377
func toSnakeCase(s string) string {
360378
s = strings.TrimSpace(s)

prometheus/promsafe/safe_test.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func ExampleNewCounterVecT_multiple_labels_manual() {
4343

4444
// Manually register the counter
4545
if err := prometheus.Register(c.Unsafe()); err != nil {
46-
log.Fatal("could not register1: ", err.Error())
46+
log.Fatal("could not register: ", err.Error())
4747
}
4848

4949
// and now, because of generics we can call Inc() with filled struct of labels:
@@ -151,11 +151,16 @@ type FastMyLabels struct {
151151
Source string
152152
}
153153

154-
// ToPrometheusLabels does a fast conversion to labels. No reflection involved.
154+
// ToPrometheusLabels does a superfast conversion to labels. So no reflection is involved.
155155
func (f FastMyLabels) ToPrometheusLabels() prometheus.Labels {
156156
return prometheus.Labels{"event_type": f.EventType, "source": f.Source}
157157
}
158158

159+
// ToLabelNames does a superfast label names list. So no reflection is involved.
160+
func (f FastMyLabels) ToLabelNames() []string {
161+
return []string{"event_type", "source"}
162+
}
163+
159164
func ExampleNewCounterVecT_fast_safe_labels_provider() {
160165
// It's possible to use pointer to labels struct
161166
myReg := prometheus.NewRegistry()

0 commit comments

Comments
 (0)