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