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