| // 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() |
| } |
| } |
| } |