github.com/lyft/flytestdlib@v0.3.12-0.20210213045714-8cdd111ecda1/promutils/scope.go (about) 1 package promutils 2 3 import ( 4 "strings" 5 "time" 6 7 "k8s.io/apimachinery/pkg/util/rand" 8 9 "github.com/prometheus/client_golang/prometheus" 10 ) 11 12 const defaultScopeDelimiterStr = ":" 13 const defaultMetricDelimiterStr = "_" 14 15 var ( 16 defaultObjectives = map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001} 17 defaultBuckets = []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10} 18 ) 19 20 func panicIfError(err error) { 21 if err != nil { 22 panic("Failed to register metrics. Error: " + err.Error()) 23 } 24 } 25 26 // A Simple StopWatch that works with prometheus summary 27 // It will scale the output to match the expected time scale (milliseconds, seconds etc) 28 // NOTE: Do not create a StopWatch object by hand, use a Scope to get a new instance of the StopWatch object 29 type StopWatch struct { 30 prometheus.Observer 31 outputScale time.Duration 32 } 33 34 // Start creates a new Instance of the StopWatch called a Timer that is closeable/stoppable. 35 // Common pattern to time a scope would be 36 // { 37 // timer := stopWatch.Start() 38 // defer timer.Stop() 39 // .... 40 // } 41 func (s StopWatch) Start() Timer { 42 return Timer{ 43 start: time.Now(), 44 outputScale: s.outputScale, 45 timer: s.Observer, 46 } 47 } 48 49 // Observes specified duration between the start and end time 50 func (s StopWatch) Observe(start, end time.Time) { 51 observed := end.Sub(start).Nanoseconds() 52 outputScaleDuration := s.outputScale.Nanoseconds() 53 if outputScaleDuration == 0 { 54 s.Observer.Observe(0) 55 return 56 } 57 scaled := float64(observed / outputScaleDuration) 58 s.Observer.Observe(scaled) 59 } 60 61 // Observes/records the time to execute the given function synchronously 62 func (s StopWatch) Time(f func()) { 63 t := s.Start() 64 f() 65 t.Stop() 66 } 67 68 // A Simple StopWatch that works with prometheus summary 69 // It will scale the output to match the expected time scale (milliseconds, seconds etc) 70 // NOTE: Do not create a StopWatch object by hand, use a Scope to get a new instance of the StopWatch object 71 type StopWatchVec struct { 72 *prometheus.SummaryVec 73 outputScale time.Duration 74 } 75 76 // Gets a concrete StopWatch instance that can be used to start a timer and record observations. 77 func (s StopWatchVec) WithLabelValues(values ...string) StopWatch { 78 return StopWatch{ 79 Observer: s.SummaryVec.WithLabelValues(values...), 80 outputScale: s.outputScale, 81 } 82 } 83 84 func (s StopWatchVec) GetMetricWith(labels prometheus.Labels) (StopWatch, error) { 85 sVec, err := s.SummaryVec.GetMetricWith(labels) 86 if err != nil { 87 return StopWatch{}, err 88 } 89 return StopWatch{ 90 Observer: sVec, 91 outputScale: s.outputScale, 92 }, nil 93 } 94 95 // This is a stoppable instance of a StopWatch or a Timer 96 // A Timer can only be stopped. On stopping it will output the elapsed duration to prometheus 97 type Timer struct { 98 start time.Time 99 outputScale time.Duration 100 timer prometheus.Observer 101 } 102 103 // This method observes the elapsed duration since the creation of the timer. The timer is created using a StopWatch 104 func (s Timer) Stop() float64 { 105 observed := time.Since(s.start).Nanoseconds() 106 outputScaleDuration := s.outputScale.Nanoseconds() 107 if outputScaleDuration == 0 { 108 s.timer.Observe(0) 109 return 0 110 } 111 scaled := float64(observed / outputScaleDuration) 112 s.timer.Observe(scaled) 113 return scaled 114 } 115 116 // A SummaryOptions represents a set of options that can be supplied when creating a new prometheus summary metric 117 type SummaryOptions struct { 118 // An Objectives defines the quantile rank estimates with their respective absolute errors. 119 // Refer to https://godoc.org/github.com/prometheus/client_golang/prometheus#SummaryOpts for details 120 Objectives map[float64]float64 121 } 122 123 // A Scope represents a prefix in Prometheus. It is nestable, thus every metric that is published does not need to 124 // provide a prefix, but just the name of the metric. As long as the Scope is used to create a new instance of the metric 125 // The prefix (or scope) is automatically set. 126 type Scope interface { 127 // Creates new prometheus.Gauge metric with the prefix as the CurrentScope 128 // Name is a string that follows prometheus conventions (mostly [_a-z]) 129 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 130 NewGauge(name, description string) (prometheus.Gauge, error) 131 MustNewGauge(name, description string) prometheus.Gauge 132 133 // Creates new prometheus.GaugeVec metric with the prefix as the CurrentScope 134 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 135 NewGaugeVec(name, description string, labelNames ...string) (*prometheus.GaugeVec, error) 136 MustNewGaugeVec(name, description string, labelNames ...string) *prometheus.GaugeVec 137 138 // Creates new prometheus.Summary metric with the prefix as the CurrentScope 139 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 140 NewSummary(name, description string) (prometheus.Summary, error) 141 MustNewSummary(name, description string) prometheus.Summary 142 143 // Creates new prometheus.Summary metric with custom options, such as a custom set of objectives (i.e., target quantiles). 144 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 145 NewSummaryWithOptions(name, description string, options SummaryOptions) (prometheus.Summary, error) 146 MustNewSummaryWithOptions(name, description string, options SummaryOptions) prometheus.Summary 147 148 // Creates new prometheus.SummaryVec metric with the prefix as the CurrentScope 149 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 150 NewSummaryVec(name, description string, labelNames ...string) (*prometheus.SummaryVec, error) 151 MustNewSummaryVec(name, description string, labelNames ...string) *prometheus.SummaryVec 152 153 // Creates new prometheus.Histogram metric with the prefix as the CurrentScope 154 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 155 NewHistogram(name, description string) (prometheus.Histogram, error) 156 MustNewHistogram(name, description string) prometheus.Histogram 157 158 // Creates new prometheus.HistogramVec metric with the prefix as the CurrentScope 159 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 160 NewHistogramVec(name, description string, labelNames ...string) (*prometheus.HistogramVec, error) 161 MustNewHistogramVec(name, description string, labelNames ...string) *prometheus.HistogramVec 162 163 // Creates new prometheus.Counter metric with the prefix as the CurrentScope 164 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 165 // Important to note, counters are not like typical counters. These are ever increasing and cumulative. 166 // So if you want to observe counters within buckets use Summary/Histogram 167 NewCounter(name, description string) (prometheus.Counter, error) 168 MustNewCounter(name, description string) prometheus.Counter 169 170 // Creates new prometheus.GaugeVec metric with the prefix as the CurrentScope 171 // Refer to https://prometheus.io/docs/concepts/metric_types/ for more information 172 NewCounterVec(name, description string, labelNames ...string) (*prometheus.CounterVec, error) 173 MustNewCounterVec(name, description string, labelNames ...string) *prometheus.CounterVec 174 175 // This is a custom wrapper to create a StopWatch object in the current Scope. 176 // Duration is to specify the scale of the Timer. For example if you are measuring times in milliseconds 177 // pass scale=times.Millisecond 178 // https://golang.org/pkg/time/#Duration 179 // The metric name is auto-suffixed with the right scale. Refer to DurationToString to understand 180 NewStopWatch(name, description string, scale time.Duration) (StopWatch, error) 181 MustNewStopWatch(name, description string, scale time.Duration) StopWatch 182 183 // This is a custom wrapper to create a StopWatch object in the current Scope. 184 // Duration is to specify the scale of the Timer. For example if you are measuring times in milliseconds 185 // pass scale=times.Millisecond 186 // https://golang.org/pkg/time/#Duration 187 // The metric name is auto-suffixed with the right scale. Refer to DurationToString to understand 188 NewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) (*StopWatchVec, error) 189 MustNewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) *StopWatchVec 190 191 // In case nesting is desired for metrics, create a new subScope. This is generally useful in creating 192 // Scoped and SubScoped metrics 193 NewSubScope(name string) Scope 194 195 // Returns the current ScopeName. Use for creating your own metrics 196 CurrentScope() string 197 198 // Method that provides a scoped metric name. Can be used, if you want to directly create your own metric 199 NewScopedMetricName(name string) string 200 } 201 202 type metricsScope struct { 203 scope string 204 } 205 206 func (m metricsScope) NewGauge(name, description string) (prometheus.Gauge, error) { 207 g := prometheus.NewGauge( 208 prometheus.GaugeOpts{ 209 Name: m.NewScopedMetricName(name), 210 Help: description, 211 }, 212 ) 213 return g, prometheus.Register(g) 214 } 215 216 func (m metricsScope) MustNewGauge(name, description string) prometheus.Gauge { 217 g, err := m.NewGauge(name, description) 218 panicIfError(err) 219 return g 220 } 221 222 func (m metricsScope) NewGaugeVec(name, description string, labelNames ...string) (*prometheus.GaugeVec, error) { 223 g := prometheus.NewGaugeVec( 224 prometheus.GaugeOpts{ 225 Name: m.NewScopedMetricName(name), 226 Help: description, 227 }, 228 labelNames, 229 ) 230 return g, prometheus.Register(g) 231 } 232 233 func (m metricsScope) MustNewGaugeVec(name, description string, labelNames ...string) *prometheus.GaugeVec { 234 g, err := m.NewGaugeVec(name, description, labelNames...) 235 panicIfError(err) 236 return g 237 } 238 239 func (m metricsScope) NewSummary(name, description string) (prometheus.Summary, error) { 240 return m.NewSummaryWithOptions(name, description, SummaryOptions{Objectives: defaultObjectives}) 241 } 242 243 func (m metricsScope) MustNewSummary(name, description string) prometheus.Summary { 244 s, err := m.NewSummary(name, description) 245 panicIfError(err) 246 return s 247 } 248 249 func (m metricsScope) NewSummaryWithOptions(name, description string, options SummaryOptions) (prometheus.Summary, error) { 250 s := prometheus.NewSummary( 251 prometheus.SummaryOpts{ 252 Name: m.NewScopedMetricName(name), 253 Help: description, 254 Objectives: options.Objectives, 255 }, 256 ) 257 258 return s, prometheus.Register(s) 259 } 260 261 func (m metricsScope) MustNewSummaryWithOptions(name, description string, options SummaryOptions) prometheus.Summary { 262 s, err := m.NewSummaryWithOptions(name, description, options) 263 panicIfError(err) 264 return s 265 } 266 267 func (m metricsScope) NewSummaryVec(name, description string, labelNames ...string) (*prometheus.SummaryVec, error) { 268 s := prometheus.NewSummaryVec( 269 prometheus.SummaryOpts{ 270 Name: m.NewScopedMetricName(name), 271 Help: description, 272 Objectives: defaultObjectives, 273 }, 274 labelNames, 275 ) 276 277 return s, prometheus.Register(s) 278 } 279 func (m metricsScope) MustNewSummaryVec(name, description string, labelNames ...string) *prometheus.SummaryVec { 280 s, err := m.NewSummaryVec(name, description, labelNames...) 281 panicIfError(err) 282 return s 283 } 284 285 func (m metricsScope) NewHistogram(name, description string) (prometheus.Histogram, error) { 286 h := prometheus.NewHistogram( 287 prometheus.HistogramOpts{ 288 Name: m.NewScopedMetricName(name), 289 Help: description, 290 Buckets: defaultBuckets, 291 }, 292 ) 293 return h, prometheus.Register(h) 294 } 295 296 func (m metricsScope) MustNewHistogram(name, description string) prometheus.Histogram { 297 h, err := m.NewHistogram(name, description) 298 panicIfError(err) 299 return h 300 } 301 302 func (m metricsScope) NewHistogramVec(name, description string, labelNames ...string) (*prometheus.HistogramVec, error) { 303 h := prometheus.NewHistogramVec( 304 prometheus.HistogramOpts{ 305 Name: m.NewScopedMetricName(name), 306 Help: description, 307 Buckets: defaultBuckets, 308 }, 309 labelNames, 310 ) 311 return h, prometheus.Register(h) 312 } 313 314 func (m metricsScope) MustNewHistogramVec(name, description string, labelNames ...string) *prometheus.HistogramVec { 315 h, err := m.NewHistogramVec(name, description, labelNames...) 316 panicIfError(err) 317 return h 318 } 319 320 func (m metricsScope) NewCounter(name, description string) (prometheus.Counter, error) { 321 c := prometheus.NewCounter( 322 prometheus.CounterOpts{ 323 Name: m.NewScopedMetricName(name), 324 Help: description, 325 }, 326 ) 327 return c, prometheus.Register(c) 328 } 329 330 func (m metricsScope) MustNewCounter(name, description string) prometheus.Counter { 331 c, err := m.NewCounter(name, description) 332 panicIfError(err) 333 return c 334 } 335 336 func (m metricsScope) NewCounterVec(name, description string, labelNames ...string) (*prometheus.CounterVec, error) { 337 c := prometheus.NewCounterVec( 338 prometheus.CounterOpts{ 339 Name: m.NewScopedMetricName(name), 340 Help: description, 341 }, 342 labelNames, 343 ) 344 return c, prometheus.Register(c) 345 } 346 347 func (m metricsScope) MustNewCounterVec(name, description string, labelNames ...string) *prometheus.CounterVec { 348 c, err := m.NewCounterVec(name, description, labelNames...) 349 panicIfError(err) 350 return c 351 } 352 353 func (m metricsScope) NewStopWatch(name, description string, scale time.Duration) (StopWatch, error) { 354 if !strings.HasSuffix(name, defaultMetricDelimiterStr) { 355 name += defaultMetricDelimiterStr 356 } 357 name += DurationToString(scale) 358 s, err := m.NewSummary(name, description) 359 if err != nil { 360 return StopWatch{}, err 361 } 362 363 return StopWatch{ 364 Observer: s, 365 outputScale: scale, 366 }, nil 367 } 368 369 func (m metricsScope) MustNewStopWatch(name, description string, scale time.Duration) StopWatch { 370 s, err := m.NewStopWatch(name, description, scale) 371 panicIfError(err) 372 return s 373 } 374 375 func (m metricsScope) NewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) (*StopWatchVec, error) { 376 if !strings.HasSuffix(name, defaultMetricDelimiterStr) { 377 name += defaultMetricDelimiterStr 378 } 379 name += DurationToString(scale) 380 s, err := m.NewSummaryVec(name, description, labelNames...) 381 if err != nil { 382 return &StopWatchVec{}, err 383 } 384 385 return &StopWatchVec{ 386 SummaryVec: s, 387 outputScale: scale, 388 }, nil 389 } 390 391 func (m metricsScope) MustNewStopWatchVec(name, description string, scale time.Duration, labelNames ...string) *StopWatchVec { 392 s, err := m.NewStopWatchVec(name, description, scale, labelNames...) 393 panicIfError(err) 394 return s 395 } 396 397 func (m metricsScope) CurrentScope() string { 398 return m.scope 399 } 400 401 // Creates a metric name under the scope. Scope will always have a defaultScopeDelimiterRune as the last character 402 func (m metricsScope) NewScopedMetricName(name string) string { 403 if name == "" { 404 panic("metric name cannot be an empty string") 405 } 406 407 return m.scope + name 408 } 409 410 func (m metricsScope) NewSubScope(subscopeName string) Scope { 411 if subscopeName == "" { 412 panic("scope name cannot be an empty string") 413 } 414 415 // If the last character of the new subscope is already a defaultScopeDelimiterRune, do not add anything 416 if !strings.HasSuffix(subscopeName, defaultScopeDelimiterStr) { 417 subscopeName += defaultScopeDelimiterStr 418 } 419 420 // Always add a new defaultScopeDelimiterRune to every scope name 421 return NewScope(m.scope + subscopeName) 422 } 423 424 // Creates a new scope in the format `name + defaultScopeDelimiterRune` 425 // If the last character is already a defaultScopeDelimiterRune, then it does not add it to the scope name 426 func NewScope(name string) Scope { 427 if name == "" { 428 panic("base scope for a metric cannot be an empty string") 429 } 430 431 // If the last character of the new subscope is already a defaultScopeDelimiterRune, do not add anything 432 if !strings.HasSuffix(name, defaultScopeDelimiterStr) { 433 name += defaultScopeDelimiterStr 434 } 435 436 return metricsScope{ 437 scope: name, 438 } 439 } 440 441 // Returns a randomly-named scope for use in tests. 442 // Prometheus requires that metric names begin with a single word, which is generated from the alphabetic testScopeNameCharset. 443 func NewTestScope() Scope { 444 return NewScope("test" + rand.String(6)) 445 } 446 447 // DurationToString converts the duration to a string suffix that indicates the scale of the timer. 448 func DurationToString(duration time.Duration) string { 449 if duration >= time.Hour { 450 return "h" 451 } 452 if duration >= time.Minute { 453 return "m" 454 } 455 if duration >= time.Second { 456 return "s" 457 } 458 if duration >= time.Millisecond { 459 return "ms" 460 } 461 if duration >= time.Microsecond { 462 return "us" 463 } 464 return "ns" 465 }