Skip to content
This repository has been archived by the owner on Jul 31, 2023. It is now read-only.

Add metric API #975

Merged
merged 2 commits into from
Nov 20, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions metric/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package metric contains a data model and exporter support for metrics.
// The data model is derived from the metric protocol buffers:
// https://github.com/census-instrumentation/opencensus-proto/blob/master/src/opencensus/proto/metrics/v1/metrics.proto
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

User doesn't have to know about the proto, please remove this.

package metric // import "go.opencensus.io/metric"
55 changes: 55 additions & 0 deletions metric/metric.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metric

import (
"time"

"go.opencensus.io/resource"
)

// LabelValue represents the value of a label. A missing value (nil) is distinct
// from an empty string value.
type LabelValue *string
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a unlikely type. Why not simply type LabelValue { Value string }?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The intention here is to be able to represent no value (nil) and empty string value distinctly. The protobuf does this, so we need to as well to be compatible. Using a type like you suggest would not allow us to represent these two distinctly.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Protobuf types are not an idiomatic due to the limitations and code-generation nature of the problem. In handcrafted libraries, we are doing better than proto.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We proposed to replace with this with struct { Value string, HasValue bool } to differentiate between "missing" and an empty string value.


// NewLabelValue creates a new non-nil LabelValue that represents the given string.
Copy link
Contributor

@rakyll rakyll Nov 19, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This becomes if the type changes.

LabelValue{ Value: "val" } would just work.

func NewLabelValue(val string) LabelValue {
return &val
}

// Descriptor holds metadata about a metric.
type Descriptor struct {
Name string // full name of the metric
Description string // human-readable description
Unit Unit // units for the measure
Type Type // type of measure
LabelKeys []string // label keys
}

// Metric represents a quantity measured against a resource with different
// label value combinations.
type Metric struct {
Descriptor *Descriptor // metric descriptor
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Descriptor Descriptor

No need for a pointer.

Resource *resource.Resource // resource against which this was measured
TimeSeries []*TimeSeries // one time series for each combination of label values
}

// TimeSeries is a sequence of points associated with a combination of label
// values.
type TimeSeries struct {
LabelValues []LabelValue // label values, same order as keys in the metric descriptor
Points []Point // points sequence
StartTime time.Time // time we started recording this time series
}
237 changes: 237 additions & 0 deletions metric/point.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metric

import (
"time"

"go.opencensus.io/exemplar"
)

// Point is a single data point of a time series.
type Point struct {
// Time is the point in time that this point represents in a time series.
Time time.Time
// ValueType is the type of value held in this point.
ValueType ValueType
// Value is the value of this point. Prefer using ReadValue to switching on
// the value type, since new value types might be added.
Value interface{}
}

//go:generate stringer -type ValueType

// ValueType is the type of value held in a point.
type ValueType int

// Value types. New value types may be added.
const (
ValueTypeDouble ValueType = iota
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no doubles in Go, if the name is dictated by the spec, it should be flex-ed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was on the fence about this. I agree it looks very strange. Will change it.

ValueTypeInt64
ValueTypeDistribution
ValueTypeSummary
)

// NewDoublePoint creates a new Point holding a float64 value.
func NewDoublePoint(t time.Time, val float64) Point {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewFloat64Point

return Point{
ValueType: ValueTypeDouble,
Value: val,
Time: t,
}
}

// NewInt64Point creates a new Point holding an int64 value.
func NewInt64Point(t time.Time, val int64) Point {
return Point{
ValueType: ValueTypeInt64,
Value: val,
Time: t,
}
}

// NewDistributionPoint creates a new Point holding a Distribution value.
func NewDistributionPoint(t time.Time, val *Distribution) Point {
return Point{
ValueType: ValueTypeDistribution,
Value: val,
Time: t,
}
}

// NewSummaryPoint creates a new Point holding a Summary value.
func NewSummaryPoint(t time.Time, val *Summary) Point {
return Point{
ValueType: ValueTypeSummary,
Value: val,
Time: t,
}
}

// ValueVisitor allows reading the value of a point.
type ValueVisitor interface {
VisitDoubleValue(float64)
VisitInt64Value(int64)
VisitDistributionValue(*Distribution)
VisitSummaryValue(*Summary)
}

// ReadValue accepts a ValueVisitor and calls the appropriate method with the
// value of this point.
// Consumers of Point should use this in preference to switching on the type
// of the value directly, since new value types may be added.
func (p Point) ReadValue(vv ValueVisitor) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this something that exporters would use? If yes, maybe we shouldn't have it in this package.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is. This whole package is mostly for use by exporters.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any end to end example usage for the new APIs here? It is not clear what the package is for. If it is mainly for the exporters, we shouldn't use a top-level package like "metric" where we would like to put the developer-facing APIs to.

switch v := p.Value.(type) {
case int64:
vv.VisitInt64Value(v)
case float64:
vv.VisitDoubleValue(v)
case *Distribution:
vv.VisitDistributionValue(v)
case *Summary:
vv.VisitSummaryValue(v)
default:
panic("unexpected value type")
}
}

// Distribution contains summary statistics for a population of values. It
// optionally contains a histogram representing the distribution of those
// values across a set of buckets.
type Distribution struct {
// Count is the number of values in the population. Must be non-negative. This value
// must equal the sum of the values in bucket_counts if a histogram is
// provided.
Count int64
// Sum is the sum of the values in the population. If count is zero then this field
// must be zero.
Sum float64
// SumOfSquaredDeviation is the sum of squared deviations from the mean of the values in the
// population. For values x_i this is:
//
// Sum[i=1..n]((x_i - mean)^2)
//
// Knuth, "The Art of Computer Programming", Vol. 2, page 323, 3rd edition
// describes Welford's method for accumulating this sum in one pass.
//
// If count is zero then this field must be zero.
SumOfSquaredDeviation float64
// BucketOptions describes the bounds of the histogram buckets in this
// distribution.
//
// A Distribution may optionally contain a histogram of the values in the
// population.
//
// If nil, there is no associated histogram.
BucketOptions *BucketOptions
// Bucket If the distribution does not have a histogram, then omit this field.
// If there is a histogram, then the sum of the values in the Bucket counts
// must equal the value in the count field of the distribution.
Buckets []Bucket
}

// BucketOptions describes the bounds of the histogram buckets in this
// distribution.
type BucketOptions struct {
// ExplicitBoundaries specifies a set of buckets with arbitrary upper-bounds.
// This defines len(bounds) + 1 (= N) buckets. The boundaries for bucket
// index i are:
//
// [0, bucket_bounds[i]) for i == 0
// [bucket_bounds[i-1], bucket_bounds[i]) for 0 < i < N-1
// [bucket_bounds[i-1], +infinity) for i == N-1
ExplicitBoundaries []float64
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just Boundaries?

}

type Bucket struct {
// Count is the number of values in each bucket of the histogram, as described in
// bucket_bounds.
Count int64
// Exemplar associated with this bucket (if any).
Exemplar *exemplar.Exemplar
}

type Summary struct {
// Count is the cumulative count (if available).
Count int64
// Sum is the cumulative sum of values (if available).
Sum float64
// HasCountAndSum is true if Count and Sum are available.
HasCountAndSum bool
// Snapshot represents percentiles calculated over an arbitrary time window.
// The values in this struct can be reset at arbitrary unknown times, with
// the requirement that all of them are reset at the same time.
Snapshot Snapshot
}

// Snapshot represents percentiles over an arbitrary time.
// The values in this struct can be reset at arbitrary unknown times, with
// the requirement that all of them are reset at the same time.
type Snapshot struct {
// Count is the number of values in the snapshot. Optional since some systems don't
// expose this. Set to 0 if not available.
Count int64
// Sum is the sum of values in the snapshot. Optional since some systems don't
// expose this. If count is 0 then this field must be zero.
Sum float64
// Percentiles is a map from percentile (range (0-100.0]) to the value of
// the percentile.
Percentiles map[float64]float64
}

//go:generate stringer -type Type

// Type is the overall type of metric, including its value type and whether it
// represents a cumulative total (since the start time) or if it represents a
// gauge value.
type Type int

// Metric types.
const (
TypeGaugeInt64 Type = iota
TypeGaugeDouble
TypeGaugeDistribution
TypeCumulativeInt64
TypeCumulativeDouble
TypeCumulativeDistribution
TypeSummary
)

// IsGuage returns true if the metric type represents a gauge-type value.
func (t Type) IsGuage() bool {
switch t {
case TypeGaugeInt64, TypeGaugeDouble, TypeGaugeDistribution:
return true
default:
return false
}
}

// ValueType returns the type of value of the points of metrics of the receiver
// type.
func (t Type) ValueType() ValueType {
switch t {
case TypeGaugeDouble, TypeCumulativeDouble:
return ValueTypeDouble
case TypeGaugeDistribution, TypeCumulativeDistribution:
return ValueTypeDistribution
case TypeGaugeInt64, TypeCumulativeInt64:
return ValueTypeInt64
case TypeSummary:
return ValueTypeSummary
default:
panic("unexpected metric.Type value")
}
}
84 changes: 84 additions & 0 deletions metric/producer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright 2018, OpenCensus Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package metric

import (
"sync"
)

// Producer is a source of metrics.
type Producer interface {
// Read should return the current values of all metrics supported by this
// metric provider.
// The returned metrics should be unique for each combination of name and
// resource.
Read() []*Metric
}

// Registry maintains a set of metric producers for exporting. Most users will
// rely on the DefaultRegistry.
type Registry struct {
mu sync.RWMutex
sources map[*uintptr]Producer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sources map[Producer]struct{}?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Producer may not be suitable as a map key. This way, we don't have to care.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what case? Also why a pointer to uintptr?

ind uint64
}

var _ Producer = (*Registry)(nil)

// NewRegistry creates a new Registry.
func NewRegistry() *Registry {
m := &Registry{
sources: make(map[*uintptr]Producer),
ind: 0,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused remainder from before the uintptr trick :)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup!

}
return m
}

// Read returns all the metrics from all the metric produces in this registry.
func (m *Registry) Read() []*Metric {
m.mu.RLock()
defer m.mu.RUnlock()
ms := make([]*Metric, 0, len(m.sources))
for _, s := range m.sources {
ms = append(ms, s.Read()...)
}
return ms
}

// AddProducer adds a producer to this registry.
func (m *Registry) AddProducer(source Producer) (remove func()) {
m.mu.Lock()
defer m.mu.Unlock()
if source == m {
panic("attempt to add registry to itself")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this statically possible?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*Registry implements Producer

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is it might be broken to make Registry implementing Read() []*Metric.

Can't we make it implement ReadAll() []*Metric to differentiate it from the producers? Otherwise, we should introduce a MultiProducer a la io.MultiReader instead of this type.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will rename

}
tok := new(uintptr)
m.sources[tok] = source
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.sources, tok)
}
}

var defaultReg = NewRegistry()

// DefaultRegistry returns the default, global metric registry for the current
// process.
// Most applications will rely on this registry but libraries should not assume
// the default registry is used.
func DefaultRegistry() *Registry {
return defaultReg
}
Loading