github.com/m3db/m3@v1.5.1-0.20231129193456-75a402aa583b/src/x/instrument/methods.go (about) 1 // Copyright (c) 2016 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package instrument 22 23 import ( 24 "fmt" 25 "time" 26 27 "github.com/uber-go/tally" 28 29 "github.com/m3db/m3/src/x/unsafe" 30 ) 31 32 var ( 33 // no-op stopwatch must contain a valid recorder to prevent panics. 34 nullStopWatchStart = tally.NewStopwatch(time.Time{}, noopStopwatchRecorder{}) 35 ) 36 37 // TimerType is a type of timer, standard or histogram timer. 38 type TimerType uint 39 40 const ( 41 // StandardTimerType is a standard timer type back by a regular timer. 42 StandardTimerType TimerType = iota 43 // HistogramTimerType is a histogram timer backed by a histogram. 44 HistogramTimerType 45 ) 46 47 const _samplingPrecision = 1 << 24 48 49 // TimerOptions is a set of timer options when creating a timer. 50 type TimerOptions struct { 51 Type TimerType 52 StandardSampleRate float64 53 HistogramBuckets tally.Buckets 54 } 55 56 // NewTimer creates a new timer based on the timer options. 57 func (o TimerOptions) NewTimer(scope tally.Scope, name string) tally.Timer { 58 return NewTimer(scope, name, o) 59 } 60 61 // SparseHistogramTimerHistogramBuckets returns a small spare set of 62 // histogram timer histogram buckets, from 1ms up to 8m. 63 func SparseHistogramTimerHistogramBuckets() tally.Buckets { 64 return tally.DurationBuckets{ 65 time.Millisecond, 66 5 * time.Millisecond, 67 10 * time.Millisecond, 68 25 * time.Millisecond, 69 50 * time.Millisecond, 70 75 * time.Millisecond, 71 100 * time.Millisecond, 72 250 * time.Millisecond, 73 500 * time.Millisecond, 74 750 * time.Millisecond, 75 time.Second, 76 2*time.Second + 500*time.Millisecond, 77 5 * time.Second, 78 7*time.Second + 500*time.Millisecond, 79 10 * time.Second, 80 25 * time.Second, 81 50 * time.Second, 82 75 * time.Second, 83 100 * time.Second, 84 250 * time.Second, 85 500 * time.Second, 86 } 87 } 88 89 // DefaultHistogramTimerHistogramBuckets returns a set of default 90 // histogram timer histogram buckets, from 2ms up to 1hr. 91 func DefaultHistogramTimerHistogramBuckets() tally.Buckets { 92 return tally.DurationBuckets{ 93 2 * time.Millisecond, 94 4 * time.Millisecond, 95 6 * time.Millisecond, 96 8 * time.Millisecond, 97 10 * time.Millisecond, 98 20 * time.Millisecond, 99 40 * time.Millisecond, 100 60 * time.Millisecond, 101 80 * time.Millisecond, 102 100 * time.Millisecond, 103 200 * time.Millisecond, 104 400 * time.Millisecond, 105 600 * time.Millisecond, 106 800 * time.Millisecond, 107 time.Second, 108 time.Second + 500*time.Millisecond, 109 2 * time.Second, 110 2*time.Second + 500*time.Millisecond, 111 3 * time.Second, 112 3*time.Second + 500*time.Millisecond, 113 4 * time.Second, 114 4*time.Second + 500*time.Millisecond, 115 5 * time.Second, 116 5*time.Second + 500*time.Millisecond, 117 6 * time.Second, 118 6*time.Second + 500*time.Millisecond, 119 7 * time.Second, 120 7*time.Second + 500*time.Millisecond, 121 8 * time.Second, 122 8*time.Second + 500*time.Millisecond, 123 9 * time.Second, 124 9*time.Second + 500*time.Millisecond, 125 10 * time.Second, 126 15 * time.Second, 127 20 * time.Second, 128 25 * time.Second, 129 30 * time.Second, 130 35 * time.Second, 131 40 * time.Second, 132 45 * time.Second, 133 50 * time.Second, 134 55 * time.Second, 135 60 * time.Second, 136 150 * time.Second, 137 300 * time.Second, 138 450 * time.Second, 139 600 * time.Second, 140 900 * time.Second, 141 1200 * time.Second, 142 1500 * time.Second, 143 1800 * time.Second, 144 2100 * time.Second, 145 2400 * time.Second, 146 2700 * time.Second, 147 3000 * time.Second, 148 3300 * time.Second, 149 3600 * time.Second, 150 } 151 } 152 153 // DefaultSummaryQuantileObjectives is a set of default summary 154 // quantile objectives and allowed error thresholds. 155 func DefaultSummaryQuantileObjectives() map[float64]float64 { 156 return map[float64]float64{ 157 0.5: 0.01, 158 0.75: 0.001, 159 0.95: 0.001, 160 0.99: 0.001, 161 0.999: 0.0001, 162 } 163 } 164 165 // NewStandardTimerOptions returns a set of standard timer options for 166 // standard timer types. 167 func NewStandardTimerOptions() TimerOptions { 168 return TimerOptions{Type: StandardTimerType} 169 } 170 171 // HistogramTimerOptions is a set of histogram timer options. 172 type HistogramTimerOptions struct { 173 HistogramBuckets tally.Buckets 174 } 175 176 // NewHistogramTimerOptions returns a set of histogram timer options 177 // and if no histogram buckets are set it will use the default 178 // histogram buckets defined. 179 func NewHistogramTimerOptions(opts HistogramTimerOptions) TimerOptions { 180 result := TimerOptions{Type: HistogramTimerType} 181 if opts.HistogramBuckets != nil && opts.HistogramBuckets.Len() > 0 { 182 result.HistogramBuckets = opts.HistogramBuckets 183 } else { 184 result.HistogramBuckets = DefaultHistogramTimerHistogramBuckets() 185 } 186 return result 187 } 188 189 var _ tally.Timer = (*timer)(nil) 190 191 // timer is a timer that can be backed by a timer or a histogram 192 // depending on TimerOptions. 193 type timer struct { 194 TimerOptions 195 timer tally.Timer 196 histogram tally.Histogram 197 } 198 199 // NewTimer returns a new timer that is backed by a timer or a histogram 200 // based on the timer options. 201 func NewTimer(scope tally.Scope, name string, opts TimerOptions) tally.Timer { 202 t := &timer{TimerOptions: opts} 203 switch t.Type { 204 case HistogramTimerType: 205 t.histogram = scope.Histogram(name, opts.HistogramBuckets) 206 default: 207 t.timer = scope.Timer(name) 208 if rate := opts.StandardSampleRate; validRate(rate) { 209 t.timer = newSampledTimer(t.timer, rate) 210 } 211 } 212 return t 213 } 214 215 func (t *timer) Record(v time.Duration) { 216 switch t.Type { 217 case HistogramTimerType: 218 t.histogram.RecordDuration(v) 219 default: 220 t.timer.Record(v) 221 } 222 } 223 224 func (t *timer) Start() tally.Stopwatch { 225 return tally.NewStopwatch(time.Now(), t) 226 } 227 228 func (t *timer) RecordStopwatch(stopwatchStart time.Time) { 229 t.Record(time.Since(stopwatchStart)) 230 } 231 232 // sampledTimer is a sampled timer that implements the tally timer interface. 233 // NB(xichen): the sampling logic should eventually be implemented in tally. 234 type sampledTimer struct { 235 tally.Timer 236 237 rate uint32 238 } 239 240 // NewSampledTimer creates a new sampled timer. 241 func NewSampledTimer(base tally.Timer, rate float64) (tally.Timer, error) { 242 if !validRate(rate) { 243 return nil, fmt.Errorf("sampling rate %f must be between 0.0 and 1.0", rate) 244 } 245 return newSampledTimer(base, rate), nil 246 } 247 248 func validRate(rate float64) bool { 249 return rate > 0.0 && rate <= 1.0 250 } 251 252 func newSampledTimer(base tally.Timer, rate float64) tally.Timer { 253 if rate == 1.0 { 254 // Avoid the overhead of working out if should sample each time. 255 return base 256 } 257 258 r := uint32(rate * _samplingPrecision) 259 if rate >= 1.0 || r > _samplingPrecision { 260 r = _samplingPrecision // clamp to 100% 261 } 262 263 return &sampledTimer{ 264 Timer: base, 265 rate: _samplingPrecision - r, 266 } 267 } 268 269 // MustCreateSampledTimer creates a new sampled timer, and panics if an error 270 // is encountered. 271 func MustCreateSampledTimer(base tally.Timer, rate float64) tally.Timer { 272 t, err := NewSampledTimer(base, rate) 273 if err != nil { 274 panic(err) 275 } 276 return t 277 } 278 279 func (t *sampledTimer) shouldSample() bool { 280 return unsafe.Fastrandn(_samplingPrecision) >= t.rate 281 } 282 283 func (t *sampledTimer) Start() tally.Stopwatch { 284 if !t.shouldSample() { 285 return nullStopWatchStart 286 } 287 return t.Timer.Start() 288 } 289 290 func (t *sampledTimer) Stop(startTime tally.Stopwatch) { 291 if startTime == nullStopWatchStart { 292 // If startTime is nullStopWatchStart, do nothing. 293 return 294 } 295 startTime.Stop() 296 } 297 298 func (t *sampledTimer) Record(d time.Duration) { 299 if !t.shouldSample() { 300 return 301 } 302 t.Timer.Record(d) 303 } 304 305 // MethodMetrics is a bundle of common metrics with a uniform naming scheme. 306 type MethodMetrics struct { 307 Errors tally.Counter 308 Success tally.Counter 309 ErrorsLatency tally.Timer 310 SuccessLatency tally.Timer 311 } 312 313 // ReportSuccess reports a success. 314 func (m *MethodMetrics) ReportSuccess(d time.Duration) { 315 m.Success.Inc(1) 316 m.SuccessLatency.Record(d) 317 } 318 319 // ReportError reports an error. 320 func (m *MethodMetrics) ReportError(d time.Duration) { 321 m.Errors.Inc(1) 322 m.ErrorsLatency.Record(d) 323 } 324 325 // ReportSuccessOrError increments Error/Success counter dependending on the error. 326 func (m *MethodMetrics) ReportSuccessOrError(e error, d time.Duration) { 327 if e != nil { 328 m.ReportError(d) 329 } else { 330 m.ReportSuccess(d) 331 } 332 } 333 334 // NewMethodMetrics returns a new Method metrics for the given method name. 335 func NewMethodMetrics(scope tally.Scope, methodName string, opts TimerOptions) MethodMetrics { 336 return MethodMetrics{ 337 Errors: scope.Counter(methodName + ".errors"), 338 Success: scope.Counter(methodName + ".success"), 339 ErrorsLatency: NewTimer(scope, methodName+".errors-latency", opts), 340 SuccessLatency: NewTimer(scope, methodName+".success-latency", opts), 341 } 342 } 343 344 // BatchMethodMetrics is a bundle of common metrics for methods with batch semantics. 345 type BatchMethodMetrics struct { 346 RetryableErrors tally.Counter 347 NonRetryableErrors tally.Counter 348 Errors tally.Counter 349 Success tally.Counter 350 Latency tally.Timer 351 } 352 353 // NewBatchMethodMetrics creates new batch method metrics. 354 func NewBatchMethodMetrics( 355 scope tally.Scope, 356 methodName string, 357 opts TimerOptions, 358 ) BatchMethodMetrics { 359 return BatchMethodMetrics{ 360 RetryableErrors: scope.Counter(methodName + ".retryable-errors"), 361 NonRetryableErrors: scope.Counter(methodName + ".non-retryable-errors"), 362 Errors: scope.Counter(methodName + ".errors"), 363 Success: scope.Counter(methodName + ".success"), 364 Latency: NewTimer(scope, methodName+".latency", opts), 365 } 366 } 367 368 // ReportSuccess reports successess. 369 func (m *BatchMethodMetrics) ReportSuccess(n int) { 370 m.Success.Inc(int64(n)) 371 } 372 373 // ReportRetryableErrors reports retryable errors. 374 func (m *BatchMethodMetrics) ReportRetryableErrors(n int) { 375 m.RetryableErrors.Inc(int64(n)) 376 m.Errors.Inc(int64(n)) 377 } 378 379 // ReportNonRetryableErrors reports non-retryable errors. 380 func (m *BatchMethodMetrics) ReportNonRetryableErrors(n int) { 381 m.NonRetryableErrors.Inc(int64(n)) 382 m.Errors.Inc(int64(n)) 383 } 384 385 // ReportLatency reports latency. 386 func (m *BatchMethodMetrics) ReportLatency(d time.Duration) { 387 m.Latency.Record(d) 388 } 389 390 type noopStopwatchRecorder struct{} 391 392 func (noopStopwatchRecorder) RecordStopwatch(_ time.Time) {}