blob: 0b62f5a5baf98a04141c2ab8792fe63933e95966 [file] [log] [blame]
// Copyright 2019 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 metric aggregates stats into metrics that can be exported.
package metric
import (
"context"
"sort"
"golang.org/x/tools/internal/lsp/telemetry/stats"
"golang.org/x/tools/internal/lsp/telemetry/tag"
"golang.org/x/tools/internal/lsp/telemetry/worker"
)
// Handle uniquely identifies a constructed metric.
// It can be used to detect which observed data objects belong
// to that metric.
type Handle struct {
name string
}
// Data represents a single point in the time series of a metric.
// This provides the common interface to all metrics no matter their data
// format.
// To get the actual values for the metric you must type assert to a concrete
// metric type.
type Data interface {
// Handle returns the metric handle this data is for.
Handle() Handle
// Groups reports the rows that currently exist for this metric.
Groups() []tag.List
}
// Scalar represents the construction information for a scalar metric.
type Scalar struct {
// Name is the unique name of this metric.
Name string
// Description can be used by observers to describe the metric to users.
Description string
// Keys is the set of tags that collectively describe rows of the metric.
Keys []interface{}
}
// HistogramInt64 represents the construction information for an int64 histogram metric.
type HistogramInt64 struct {
// Name is the unique name of this metric.
Name string
// Description can be used by observers to describe the metric to users.
Description string
// Keys is the set of tags that collectively describe rows of the metric.
Keys []interface{}
// Buckets holds the inclusive upper bound of each bucket in the histogram.
Buckets []int64
}
// HistogramFloat64 represents the construction information for an float64 histogram metric.
type HistogramFloat64 struct {
// Name is the unique name of this metric.
Name string
// Description can be used by observers to describe the metric to users.
Description string
// Keys is the set of tags that collectively describe rows of the metric.
Keys []interface{}
// Buckets holds the inclusive upper bound of each bucket in the histogram.
Buckets []float64
}
// Observer is the type for functions that want to observe metric values
// as they arrive.
// Each data point delivered to an observer is immutable and can be stored if
// needed.
type Observer func(Data)
// CountInt64 creates a new metric based on the Scalar information that counts
// the number of times the supplied int64 measure is set.
// Metrics of this type will use Int64Data.
func (info Scalar) CountInt64(measure *stats.Int64Measure) Handle {
data := &Int64Data{Info: &info}
measure.Subscribe(data.countInt64)
return Handle{info.Name}
}
// SumInt64 creates a new metric based on the Scalar information that sums all
// the values recorded on the int64 measure.
// Metrics of this type will use Int64Data.
func (info Scalar) SumInt64(measure *stats.Int64Measure) Handle {
data := &Int64Data{Info: &info}
measure.Subscribe(data.sum)
_ = data
return Handle{info.Name}
}
// LatestInt64 creates a new metric based on the Scalar information that tracks
// the most recent value recorded on the int64 measure.
// Metrics of this type will use Int64Data.
func (info Scalar) LatestInt64(measure *stats.Int64Measure) Handle {
data := &Int64Data{Info: &info, IsGauge: true}
measure.Subscribe(data.latest)
return Handle{info.Name}
}
// CountFloat64 creates a new metric based on the Scalar information that counts
// the number of times the supplied float64 measure is set.
// Metrics of this type will use Int64Data.
func (info Scalar) CountFloat64(measure *stats.Float64Measure) Handle {
data := &Int64Data{Info: &info}
measure.Subscribe(data.countFloat64)
return Handle{info.Name}
}
// SumFloat64 creates a new metric based on the Scalar information that sums all
// the values recorded on the float64 measure.
// Metrics of this type will use Float64Data.
func (info Scalar) SumFloat64(measure *stats.Float64Measure) Handle {
data := &Float64Data{Info: &info}
measure.Subscribe(data.sum)
return Handle{info.Name}
}
// LatestFloat64 creates a new metric based on the Scalar information that tracks
// the most recent value recorded on the float64 measure.
// Metrics of this type will use Float64Data.
func (info Scalar) LatestFloat64(measure *stats.Float64Measure) Handle {
data := &Float64Data{Info: &info, IsGauge: true}
measure.Subscribe(data.latest)
return Handle{info.Name}
}
// Record creates a new metric based on the HistogramInt64 information that
// tracks the bucketized counts of values recorded on the int64 measure.
// Metrics of this type will use HistogramInt64Data.
func (info HistogramInt64) Record(measure *stats.Int64Measure) Handle {
data := &HistogramInt64Data{Info: &info}
measure.Subscribe(data.record)
return Handle{info.Name}
}
// Record creates a new metric based on the HistogramFloat64 information that
// tracks the bucketized counts of values recorded on the float64 measure.
// Metrics of this type will use HistogramFloat64Data.
func (info HistogramFloat64) Record(measure *stats.Float64Measure) Handle {
data := &HistogramFloat64Data{Info: &info}
measure.Subscribe(data.record)
return Handle{info.Name}
}
// Int64Data is a concrete implementation of Data for int64 scalar metrics.
type Int64Data struct {
// Info holds the original consruction information.
Info *Scalar
// IsGauge is true for metrics that track values, rather than increasing over time.
IsGauge bool
// Rows holds the per group values for the metric.
Rows []int64
groups []tag.List
}
// Float64Data is a concrete implementation of Data for float64 scalar metrics.
type Float64Data struct {
// Info holds the original consruction information.
Info *Scalar
// IsGauge is true for metrics that track values, rather than increasing over time.
IsGauge bool
// Rows holds the per group values for the metric.
Rows []float64
groups []tag.List
}
// HistogramInt64Data is a concrete implementation of Data for int64 histogram metrics.
type HistogramInt64Data struct {
// Info holds the original consruction information.
Info *HistogramInt64
// Rows holds the per group values for the metric.
Rows []*HistogramInt64Row
groups []tag.List
}
// HistogramInt64Row holds the values for a single row of a HistogramInt64Data.
type HistogramInt64Row struct {
// Values is the counts per bucket.
Values []int64
// Count is the total count.
Count int64
// Sum is the sum of all the values recorded.
Sum int64
// Min is the smallest recorded value.
Min int64
// Max is the largest recorded value.
Max int64
}
// HistogramFloat64Data is a concrete implementation of Data for float64 histogram metrics.
type HistogramFloat64Data struct {
// Info holds the original consruction information.
Info *HistogramFloat64
// Rows holds the per group values for the metric.
Rows []*HistogramFloat64Row
groups []tag.List
}
// HistogramFloat64Row holds the values for a single row of a HistogramFloat64Data.
type HistogramFloat64Row struct {
// Values is the counts per bucket.
Values []int64
// Count is the total count.
Count int64
// Sum is the sum of all the values recorded.
Sum float64
// Min is the smallest recorded value.
Min float64
// Max is the largest recorded value.
Max float64
}
// Name returns the name of the metric this is a handle for.
func (h Handle) Name() string { return h.name }
var observers []Observer
// RegisterObservers adds a new metric observer to the system.
// There is no way to unregister an observer.
func RegisterObservers(e ...Observer) {
worker.Do(func() {
observers = append(e, observers...)
})
}
// export must only be called from inside a worker
func export(m Data) {
for _, e := range observers {
e(m)
}
}
func getGroup(ctx context.Context, g *[]tag.List, keys []interface{}) (int, bool) {
group := tag.Get(ctx, keys...)
old := *g
index := sort.Search(len(old), func(i int) bool {
return !old[i].Less(group)
})
if index < len(old) && group.Equal(old[index]) {
// not a new group
return index, false
}
*g = make([]tag.List, len(old)+1)
copy(*g, old[:index])
copy((*g)[index+1:], old[index:])
(*g)[index] = group
return index, true
}
func (data *Int64Data) Handle() Handle { return Handle{data.Info.Name} }
func (data *Int64Data) Groups() []tag.List { return data.groups }
func (data *Int64Data) modify(ctx context.Context, f func(v int64) int64) {
worker.Do(func() {
index, insert := getGroup(ctx, &data.groups, data.Info.Keys)
old := data.Rows
if insert {
data.Rows = make([]int64, len(old)+1)
copy(data.Rows, old[:index])
copy(data.Rows[index+1:], old[index:])
} else {
data.Rows = make([]int64, len(old))
copy(data.Rows, old)
}
data.Rows[index] = f(data.Rows[index])
frozen := *data
export(&frozen)
})
}
func (data *Int64Data) countInt64(ctx context.Context, measure *stats.Int64Measure, value int64) {
data.modify(ctx, func(v int64) int64 { return v + 1 })
}
func (data *Int64Data) countFloat64(ctx context.Context, measure *stats.Float64Measure, value float64) {
data.modify(ctx, func(v int64) int64 { return v + 1 })
}
func (data *Int64Data) sum(ctx context.Context, measure *stats.Int64Measure, value int64) {
data.modify(ctx, func(v int64) int64 { return v + value })
}
func (data *Int64Data) latest(ctx context.Context, measure *stats.Int64Measure, value int64) {
data.modify(ctx, func(v int64) int64 { return value })
}
func (data *Float64Data) Handle() Handle { return Handle{data.Info.Name} }
func (data *Float64Data) Groups() []tag.List { return data.groups }
func (data *Float64Data) modify(ctx context.Context, f func(v float64) float64) {
worker.Do(func() {
index, insert := getGroup(ctx, &data.groups, data.Info.Keys)
old := data.Rows
if insert {
data.Rows = make([]float64, len(old)+1)
copy(data.Rows, old[:index])
copy(data.Rows[index+1:], old[index:])
} else {
data.Rows = make([]float64, len(old))
copy(data.Rows, old)
}
data.Rows[index] = f(data.Rows[index])
frozen := *data
export(&frozen)
})
}
func (data *Float64Data) sum(ctx context.Context, measure *stats.Float64Measure, value float64) {
data.modify(ctx, func(v float64) float64 { return v + value })
}
func (data *Float64Data) latest(ctx context.Context, measure *stats.Float64Measure, value float64) {
data.modify(ctx, func(v float64) float64 { return value })
}
func (data *HistogramInt64Data) Handle() Handle { return Handle{data.Info.Name} }
func (data *HistogramInt64Data) Groups() []tag.List { return data.groups }
func (data *HistogramInt64Data) modify(ctx context.Context, f func(v *HistogramInt64Row)) {
worker.Do(func() {
index, insert := getGroup(ctx, &data.groups, data.Info.Keys)
old := data.Rows
var v HistogramInt64Row
if insert {
data.Rows = make([]*HistogramInt64Row, len(old)+1)
copy(data.Rows, old[:index])
copy(data.Rows[index+1:], old[index:])
} else {
data.Rows = make([]*HistogramInt64Row, len(old))
copy(data.Rows, old)
v = *data.Rows[index]
}
oldValues := v.Values
v.Values = make([]int64, len(data.Info.Buckets))
copy(v.Values, oldValues)
f(&v)
data.Rows[index] = &v
frozen := *data
export(&frozen)
})
}
func (data *HistogramInt64Data) record(ctx context.Context, measure *stats.Int64Measure, value int64) {
data.modify(ctx, func(v *HistogramInt64Row) {
v.Sum += value
if v.Min > value || v.Count == 0 {
v.Min = value
}
if v.Max < value || v.Count == 0 {
v.Max = value
}
v.Count++
for i, b := range data.Info.Buckets {
if value <= b {
v.Values[i]++
}
}
})
}
func (data *HistogramFloat64Data) Handle() Handle { return Handle{data.Info.Name} }
func (data *HistogramFloat64Data) Groups() []tag.List { return data.groups }
func (data *HistogramFloat64Data) modify(ctx context.Context, f func(v *HistogramFloat64Row)) {
worker.Do(func() {
index, insert := getGroup(ctx, &data.groups, data.Info.Keys)
old := data.Rows
var v HistogramFloat64Row
if insert {
data.Rows = make([]*HistogramFloat64Row, len(old)+1)
copy(data.Rows, old[:index])
copy(data.Rows[index+1:], old[index:])
} else {
data.Rows = make([]*HistogramFloat64Row, len(old))
copy(data.Rows, old)
v = *data.Rows[index]
}
oldValues := v.Values
v.Values = make([]int64, len(data.Info.Buckets))
copy(v.Values, oldValues)
f(&v)
data.Rows[index] = &v
frozen := *data
export(&frozen)
})
}
func (data *HistogramFloat64Data) record(ctx context.Context, measure *stats.Float64Measure, value float64) {
data.modify(ctx, func(v *HistogramFloat64Row) {
v.Sum += value
if v.Min > value || v.Count == 0 {
v.Min = value
}
if v.Max < value || v.Count == 0 {
v.Max = value
}
v.Count++
for i, b := range data.Info.Buckets {
if value <= b {
v.Values[i]++
}
}
})
}