blob: 3147ecb9f7f727303be5464e8c2491b1fa7bc27d [file] [log] [blame]
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package telemetry
import (
"context"
"errors"
"fmt"
"sort"
"sync"
"time"
"golang.org/x/telemetry/counter"
)
// latencyKey is used for looking up latency counters.
type latencyKey struct {
operation, bucket string
isError bool
}
var (
latencyBuckets = []struct {
end time.Duration
name string
}{
{10 * time.Millisecond, "<10ms"},
{50 * time.Millisecond, "<50ms"},
{100 * time.Millisecond, "<100ms"},
{200 * time.Millisecond, "<200ms"},
{500 * time.Millisecond, "<500ms"},
{1 * time.Second, "<1s"},
{5 * time.Second, "<5s"},
{24 * time.Hour, "<24h"},
}
latencyCounterMu sync.Mutex
latencyCounters = make(map[latencyKey]*counter.Counter) // lazily populated
)
// ForEachLatencyCounter runs the provided function for each current latency
// counter measuring the given operation.
//
// Exported for testing.
func ForEachLatencyCounter(operation string, isError bool, f func(*counter.Counter)) {
latencyCounterMu.Lock()
defer latencyCounterMu.Unlock()
for k, v := range latencyCounters {
if k.operation == operation && k.isError == isError {
f(v)
}
}
}
// getLatencyCounter returns the counter used to record latency of the given
// operation in the given bucket.
func getLatencyCounter(operation, bucket string, isError bool) *counter.Counter {
latencyCounterMu.Lock()
defer latencyCounterMu.Unlock()
key := latencyKey{operation, bucket, isError}
c, ok := latencyCounters[key]
if !ok {
var name string
if isError {
name = fmt.Sprintf("gopls/%s/error-latency:%s", operation, bucket)
} else {
name = fmt.Sprintf("gopls/%s/latency:%s", operation, bucket)
}
c = counter.New(name)
latencyCounters[key] = c
}
return c
}
// StartLatencyTimer starts a timer for the gopls operation with the given
// name, and returns a func to stop the timer and record the latency sample.
//
// If the context provided to the resulting func is done, no observation is
// recorded.
func StartLatencyTimer(operation string) func(context.Context, error) {
start := time.Now()
return func(ctx context.Context, err error) {
if errors.Is(ctx.Err(), context.Canceled) {
// Ignore timing where the operation is cancelled, it may be influenced
// by client behavior.
return
}
latency := time.Since(start)
bucketIdx := sort.Search(len(latencyBuckets), func(i int) bool {
bucket := latencyBuckets[i]
return latency < bucket.end
})
if bucketIdx < len(latencyBuckets) { // ignore latency longer than a day :)
bucketName := latencyBuckets[bucketIdx].name
getLatencyCounter(operation, bucketName, err != nil).Inc()
}
}
}