promsafe: improvements. ability to add fast implementation of toLabelNames

Signed-off-by: Eugene <eugene@amberpixels.io>
This commit is contained in:
Eugene 2024-09-20 11:41:11 +03:00
parent 5b52b012e1
commit c52d495061
No known key found for this signature in database
GPG Key ID: 51AC89611A689305
2 changed files with 59 additions and 36 deletions

View File

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

View File

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