Skip to content

Commit

Permalink
Golang metrics prototype (#100)
Browse files Browse the repository at this point in the history
* initial metrics work

* rename cumulative to counter

* rename bidirectional to nonmonotonic

* rename unidirectional to monotonic

* rename nonnegative to signed

this changes the default semantics a bit - before the change measure
could record negative values by default, now it can't.

The specification draft currently specifies both NonNegative and
Signed, but I think it's a mistake.

* rename instrument to descriptor

* license

* rework measurement values

* make measurement value a tagged union

* simplify to one kind of metrics

* add observers

* change some interfaces to match the spec

* keep integral measurement separate from floating ones

* remove duplicated measurement type

* add checking for options

* reorder some fields and functions

* rename a function

to avoid confusion between the Handle type and the Measure type

* drop disabled field from descriptor

* add back typed API for metrics

* make metric options type safe

* merge alternatives into a single bool

* make value kind name less stuttery

* fix observation callback prototype

* drop context parameter from NewHandle

* drop useless parameter names

* make descriptor an opaque struct

* use a store helper

* handle comment fixes

* reword Alternate comment

* drop the "any value" metrics

* make measurement value simpler

* document value stuff

* add tests for values

* docs

* do not panic if there is no span ID in the event
  • Loading branch information
jmacd authored and rghetia committed Oct 8, 2019
1 parent c2d5c66 commit be8fb0b
Show file tree
Hide file tree
Showing 26 changed files with 1,636 additions and 332 deletions.
1 change: 1 addition & 0 deletions api/core/key.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Value struct {
String string
Bytes []byte

// TODO See how segmentio/stats handles this type, it's much smaller.
// TODO Lazy value type?
}

Expand Down
323 changes: 290 additions & 33 deletions api/metric/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and
// limitations under the License.

//go:generate stringer -type=Kind,ValueKind

package metric

import (
Expand All @@ -21,62 +23,317 @@ import (
"go.opentelemetry.io/api/unit"
)

type MetricType int
// Kind categorizes different kinds of metric.
type Kind int

const (
Invalid MetricType = iota
Gauge // Supports Set()
Cumulative // Supports Inc()
// Invalid describes an invalid metric.
Invalid Kind = iota
// CounterKind describes a metric that supports Add().
CounterKind
// GaugeKind describes a metric that supports Set().
GaugeKind
// MeasureKind describes a metric that supports Record().
MeasureKind
// ObserverKind describes a metric that reports measurement on
// demand.
ObserverKind
)

// Handle is the implementation-level interface to Set/Add/Record
// individual metrics.
type Handle interface {
// RecordOne allows the SDK to observe a single metric event
RecordOne(ctx context.Context, value MeasurementValue)
}

// TODO this belongs outside the metrics API, in some sense, but that
// might create a dependency. Putting this here means we can't re-use
// a LabelSet between metrics and tracing, even when they are the same
// SDK.

// LabelSet represents a []core.KeyValue for use as pre-defined labels
// in the metrics API.
type LabelSet interface {
Meter() Meter
}

// ObservationCallback defines a type of the callback the observer
// will use to report the measurement
type ObservationCallback func(LabelSet, MeasurementValue)

// ObserverCallback defines a type of the callback SDK will call for
// the registered observers.
type ObserverCallback func(Meter, Observer, ObservationCallback)

// Meter is an interface to the metrics portion of the OpenTelemetry SDK.
type Meter interface {
// TODO more Metric types
GetFloat64Gauge(ctx context.Context, gauge *Float64GaugeHandle, labels ...core.KeyValue) Float64Gauge
// DefineLabels returns a reference to a set of labels that
// cannot be read by the application.
DefineLabels(context.Context, ...core.KeyValue) LabelSet

// RecordBatch atomically records a batch of measurements.
RecordBatch(context.Context, LabelSet, ...Measurement)

// NewHandle creates a Handle that contains the passed
// key-value pairs. This should not be used directly - prefer
// using GetHandle function of a metric.
NewHandle(*Descriptor, LabelSet) Handle
// DeleteHandle destroys the Handle and does a cleanup of the
// underlying resources.
DeleteHandle(Handle)

// RegisterObserver registers the observer with callback
// returning a measurement. When and how often the callback
// will be called is defined by SDK. This should not be used
// directly - prefer either RegisterInt64Observer or
// RegisterFloat64Observer, depending on the type of the
// observer to be registered.
RegisterObserver(Observer, ObserverCallback)
// UnregisterObserver removes the observer from registered
// observers. This should not be used directly - prefer either
// UnregisterInt64Observer or UnregisterFloat64Observer,
// depending on the type of the observer to be registered.
UnregisterObserver(Observer)
}

// DescriptorID is a unique identifier of a metric.
type DescriptorID uint64

// ValueKind describes the data type of the measurement value the
// metric generates.
type ValueKind int8

const (
// Int64ValueKind means that the metric generates values of
// type int64.
Int64ValueKind ValueKind = iota
// Float64ValueKind means that the metric generates values of
// type float64.
Float64ValueKind
)

// Descriptor represents a named metric with recommended
// local-aggregation keys.
type Descriptor struct {
name string
kind Kind
keys []core.Key
id DescriptorID
description string
unit unit.Unit
valueKind ValueKind
alternate bool
}

type Float64Gauge interface {
Set(ctx context.Context, value float64, labels ...core.KeyValue)
// Name is a required field describing this metric descriptor, should
// have length > 0.
func (d *Descriptor) Name() string {
return d.name
}

type Handle struct {
Name string
Description string
Unit unit.Unit
// Kind is the metric kind of this descriptor.
func (d *Descriptor) Kind() Kind {
return d.kind
}

// Keys are recommended keys determined in the handles obtained for
// this metric.
func (d *Descriptor) Keys() []core.Key {
return d.keys
}

// ID is uniquely assigned to support per-SDK registration.
func (d *Descriptor) ID() DescriptorID {
return d.id
}

// Description is an optional field describing this metric descriptor.
func (d *Descriptor) Description() string {
return d.description
}

// Unit is an optional field describing this metric descriptor.
func (d *Descriptor) Unit() unit.Unit {
return d.unit
}

Type MetricType
Keys []core.Key
// ValueKind describes the type of values the metric produces.
func (d *Descriptor) ValueKind() ValueKind {
return d.valueKind
}

type Option func(*Handle)
// Alternate defines the property of metric value dependent on a
// metric type.
//
// - for Counter, true implies that the metric is an up-down Counter
//
// - for Gauge/Observer, true implies that the metric is a
// non-descending Gauge/Observer
//
// - for Measure, true implies that the metric supports positive and
// negative values
func (d *Descriptor) Alternate() bool {
return d.alternate
}

// Measurement is used for reporting a batch of metric values.
type Measurement struct {
Descriptor *Descriptor
Value MeasurementValue
}

// Option supports specifying the various metric options.
type Option func(*Descriptor)

// OptionApplier is an interface for applying metric options that are
// valid for all the kinds of metrics.
type OptionApplier interface {
CounterOptionApplier
GaugeOptionApplier
MeasureOptionApplier
// ApplyOption is used to make some changes in the Descriptor.
ApplyOption(*Descriptor)
}

type optionWrapper struct {
F Option
}

var _ OptionApplier = optionWrapper{}

func (o optionWrapper) ApplyCounterOption(d *Descriptor) {
o.ApplyOption(d)
}

func (o optionWrapper) ApplyGaugeOption(d *Descriptor) {
o.ApplyOption(d)
}

func (o optionWrapper) ApplyMeasureOption(d *Descriptor) {
o.ApplyOption(d)
}

func (o optionWrapper) ApplyOption(d *Descriptor) {
o.F(d)
}

// WithDescription applies provided description.
func WithDescription(desc string) Option {
return func(m *Handle) {
m.Description = desc
func WithDescription(desc string) OptionApplier {
return optionWrapper{
F: func(d *Descriptor) {
d.description = desc
},
}
}

// WithUnit applies provided unit.
func WithUnit(unit unit.Unit) Option {
return func(m *Handle) {
m.Unit = unit
func WithUnit(unit unit.Unit) OptionApplier {
return optionWrapper{
F: func(d *Descriptor) {
d.unit = unit
},
}
}

// WithKeys applies required label keys. Multiple `WithKeys` options
// accumulate.
func WithKeys(keys ...core.Key) OptionApplier {
return optionWrapper{
F: func(d *Descriptor) {
d.keys = append(d.keys, keys...)
},
}
}

// WithKeys applies the provided dimension keys.
func WithKeys(keys ...core.Key) Option {
return func(m *Handle) {
m.Keys = keys
// WithNonMonotonic sets whether a counter is permitted to go up AND
// down.
func WithNonMonotonic(nm bool) CounterOptionApplier {
return counterOptionWrapper{
F: func(d *Descriptor) {
d.alternate = nm
},
}
}

func (mtype MetricType) String() string {
switch mtype {
case Gauge:
return "gauge"
case Cumulative:
return "cumulative"
default:
return "unknown"
// WithMonotonic sets whether a gauge is not permitted to go down.
func WithMonotonic(m bool) GaugeOptionApplier {
return gaugeOptionWrapper{
F: func(d *Descriptor) {
d.alternate = m
},
}
}

// WithSigned sets whether a measure is permitted to be negative.
func WithSigned(s bool) MeasureOptionApplier {
return measureOptionWrapper{
F: func(d *Descriptor) {
d.alternate = s
},
}
}

// Defined returns true when the descriptor has been registered.
func (d Descriptor) Defined() bool {
return len(d.name) != 0
}

// RecordBatch reports to the global Meter.
func RecordBatch(ctx context.Context, labels LabelSet, batch ...Measurement) {
GlobalMeter().RecordBatch(ctx, labels, batch...)
}

// Int64ObservationCallback defines a type of the callback the
// observer will use to report the int64 measurement.
type Int64ObservationCallback func(LabelSet, int64)

// Int64ObserverCallback defines a type of the callback SDK will call
// for the registered int64 observers.
type Int64ObserverCallback func(Meter, Int64Observer, Int64ObservationCallback)

// RegisterInt64Observer is a convenience wrapper around
// Meter.RegisterObserver that provides a type-safe callback for
// Int64Observer.
func RegisterInt64Observer(meter Meter, observer Int64Observer, callback Int64ObserverCallback) {
cb := func(m Meter, o Observer, ocb ObservationCallback) {
iocb := func(l LabelSet, i int64) {
ocb(l, NewInt64MeasurementValue(i))
}
callback(m, Int64Observer{o}, iocb)
}
meter.RegisterObserver(observer.Observer, cb)
}

// UnregisterInt64Observer is a convenience wrapper around
// Meter.UnregisterObserver for Int64Observer.
func UnregisterInt64Observer(meter Meter, observer Int64Observer) {
meter.UnregisterObserver(observer.Observer)
}

// Float64ObservationCallback defines a type of the callback the
// observer will use to report the float64 measurement.
type Float64ObservationCallback func(LabelSet, float64)

// Float64ObserverCallback defines a type of the callback SDK will
// call for the registered float64 observers.
type Float64ObserverCallback func(Meter, Float64Observer, Float64ObservationCallback)

// RegisterFloat64Observer is a convenience wrapper around
// Meter.RegisterObserver that provides a type-safe callback for
// Float64Observer.
func RegisterFloat64Observer(meter Meter, observer Float64Observer, callback Float64ObserverCallback) {
cb := func(m Meter, o Observer, ocb ObservationCallback) {
focb := func(l LabelSet, f float64) {
ocb(l, NewFloat64MeasurementValue(f))
}
callback(m, Float64Observer{o}, focb)
}
meter.RegisterObserver(observer.Observer, cb)
}

// UnregisterFloat64Observer is a convenience wrapper around
// Meter.UnregisterObserver for Float64Observer.
func UnregisterFloat64Observer(meter Meter, observer Float64Observer) {
meter.UnregisterObserver(observer.Observer)
}
Loading

0 comments on commit be8fb0b

Please sign in to comment.