1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2025-07-17 01:12:45 +02:00

Add observer metric (#474)

* wip: observers

* wip: float observers

* fix copy pasta

* wip: rework observers in sdk

* small fix in global meter

* wip: aggregators and selectors

* wip: monotonicity option for observers

* some refactor

* wip: docs

needs more package docs (especially for api/metric and sdk/metric)

* fix ci

* Fix copy-pasta in docs

Co-Authored-By: Mauricio Vásquez <mauricio@kinvolk.io>

* recycle unused recorders in observers

if a recorder for a labelset is unused for a second collection cycle
in a row, drop it

* unregister

* thread-safe set callback

* Fix docs

* Revert "wip: aggregators and selectors"

This reverts commit 37b7d05aed5dc90f6d5593325b6eb77494e21736.

* update selector

* tests

* Rework number equality

Compare concrete numbers, so we can get actual numbers in the error
message when they are not equal, not some uint64 representation. This
also uses InDelta for comparing floats.

* Ensure that Observers are registered in the same order

* Run observers in fixed order

So the tests can be reproducible - iterating a map made the order of
measurements random.

* Ensure the proper alignment of the delegates

This wasn't checked at all. After adding the checks, the test-386
failed.

* Small tweaks to the global meter test

* Ensure proper alignment of the callback pointer

test-386 was complaining about it

* update docs

* update a TODO

* address review issues

* drop SetCallback

Co-authored-by: Mauricio Vásquez <mauricio@kinvolk.io>
Co-authored-by: Rahul Patel <rghetia@yahoo.com>
This commit is contained in:
Rahul Patel
2020-03-05 12:15:30 -08:00
committed by GitHub
parent 547d584da8
commit a202f16100
16 changed files with 1072 additions and 124 deletions

View File

@ -43,6 +43,9 @@ type (
// current maps `mapkey` to *record.
current sync.Map
// observers is a set of `*observer` instances
observers sync.Map
// empty is the (singleton) result of Labels()
// w/ zero arguments.
empty labels
@ -115,16 +118,121 @@ type (
recorder export.Aggregator
}
observerResult struct {
observer *observer
}
int64ObserverResult struct {
result observerResult
}
float64ObserverResult struct {
result observerResult
}
observerCallback func(result observerResult)
observer struct {
meter *SDK
descriptor *export.Descriptor
// recorders maps encoded labelset to the pair of
// labelset and recorder
recorders map[string]labeledRecorder
callback observerCallback
}
labeledRecorder struct {
recorder export.Aggregator
labels *labels
modifiedEpoch int64
}
int64Observer struct {
observer *observer
}
float64Observer struct {
observer *observer
}
ErrorHandler func(error)
)
var (
_ api.Meter = &SDK{}
_ api.LabelSet = &labels{}
_ api.InstrumentImpl = &instrument{}
_ api.BoundInstrumentImpl = &record{}
_ api.Meter = &SDK{}
_ api.LabelSet = &labels{}
_ api.InstrumentImpl = &instrument{}
_ api.BoundInstrumentImpl = &record{}
_ api.Int64Observer = int64Observer{}
_ api.Float64Observer = float64Observer{}
_ api.Int64ObserverResult = int64ObserverResult{}
_ api.Float64ObserverResult = float64ObserverResult{}
)
func (r observerResult) observe(number core.Number, ls api.LabelSet) {
r.observer.recordOne(number, ls)
}
func (o *observer) recordOne(number core.Number, ls api.LabelSet) {
if err := aggregator.RangeTest(number, o.descriptor); err != nil {
o.meter.errorHandler(err)
return
}
recorder := o.getRecorder(ls)
if recorder == nil {
// The instrument is disabled according to the
// AggregationSelector.
return
}
if err := recorder.Update(context.Background(), number, o.descriptor); err != nil {
o.meter.errorHandler(err)
return
}
}
func (o *observer) getRecorder(ls api.LabelSet) export.Aggregator {
labels := o.meter.labsFor(ls)
lrec, ok := o.recorders[labels.encoded]
if ok {
lrec.modifiedEpoch = o.meter.currentEpoch
o.recorders[labels.encoded] = lrec
return lrec.recorder
}
rec := o.meter.batcher.AggregatorFor(o.descriptor)
if o.recorders == nil {
o.recorders = make(map[string]labeledRecorder)
}
// This may store nil recorder in the map, thus disabling the
// observer for the labelset for good. This is intentional,
// but will be revisited later.
o.recorders[labels.encoded] = labeledRecorder{
recorder: rec,
labels: labels,
modifiedEpoch: o.meter.currentEpoch,
}
return rec
}
func (o *observer) unregister() {
o.meter.observers.Delete(o)
}
func (r int64ObserverResult) Observe(value int64, labels api.LabelSet) {
r.result.observe(core.NewInt64Number(value), labels)
}
func (r float64ObserverResult) Observe(value float64, labels api.LabelSet) {
r.result.observe(core.NewFloat64Number(value), labels)
}
func (o int64Observer) Unregister() {
o.observer.unregister()
}
func (o float64Observer) Unregister() {
o.observer.unregister()
}
func (i *instrument) Meter() api.Meter {
return i.meter
}
@ -275,8 +383,8 @@ func (m *SDK) labsFor(ls api.LabelSet) *labels {
return &m.empty
}
func (m *SDK) newInstrument(name string, metricKind export.Kind, numberKind core.NumberKind, opts *api.Options) *instrument {
descriptor := export.NewDescriptor(
func newDescriptor(name string, metricKind export.Kind, numberKind core.NumberKind, opts *api.Options) *export.Descriptor {
return export.NewDescriptor(
name,
metricKind,
opts.Keys,
@ -284,6 +392,10 @@ func (m *SDK) newInstrument(name string, metricKind export.Kind, numberKind core
opts.Unit,
numberKind,
opts.Alternate)
}
func (m *SDK) newInstrument(name string, metricKind export.Kind, numberKind core.NumberKind, opts *api.Options) *instrument {
descriptor := newDescriptor(name, metricKind, numberKind, opts)
return &instrument{
descriptor: descriptor,
meter: m,
@ -332,8 +444,66 @@ func (m *SDK) NewFloat64Measure(name string, mos ...api.MeasureOptionApplier) ap
return api.WrapFloat64MeasureInstrument(m.newMeasureInstrument(name, core.Float64NumberKind, mos...))
}
// Collect traverses the list of active records and exports data for
// each active instrument. Collect() may not be called concurrently.
func (m *SDK) RegisterInt64Observer(name string, callback api.Int64ObserverCallback, oos ...api.ObserverOptionApplier) api.Int64Observer {
if callback == nil {
return api.NoopMeter{}.RegisterInt64Observer("", nil)
}
opts := api.Options{}
api.ApplyObserverOptions(&opts, oos...)
descriptor := newDescriptor(name, export.ObserverKind, core.Int64NumberKind, &opts)
cb := wrapInt64ObserverCallback(callback)
obs := m.newObserver(descriptor, cb)
return int64Observer{
observer: obs,
}
}
func wrapInt64ObserverCallback(callback api.Int64ObserverCallback) observerCallback {
return func(result observerResult) {
typeSafeResult := int64ObserverResult{
result: result,
}
callback(typeSafeResult)
}
}
func (m *SDK) RegisterFloat64Observer(name string, callback api.Float64ObserverCallback, oos ...api.ObserverOptionApplier) api.Float64Observer {
if callback == nil {
return api.NoopMeter{}.RegisterFloat64Observer("", nil)
}
opts := api.Options{}
api.ApplyObserverOptions(&opts, oos...)
descriptor := newDescriptor(name, export.ObserverKind, core.Float64NumberKind, &opts)
cb := wrapFloat64ObserverCallback(callback)
obs := m.newObserver(descriptor, cb)
return float64Observer{
observer: obs,
}
}
func wrapFloat64ObserverCallback(callback api.Float64ObserverCallback) observerCallback {
return func(result observerResult) {
typeSafeResult := float64ObserverResult{
result: result,
}
callback(typeSafeResult)
}
}
func (m *SDK) newObserver(descriptor *export.Descriptor, callback observerCallback) *observer {
obs := &observer{
meter: m,
descriptor: descriptor,
recorders: nil,
callback: callback,
}
m.observers.Store(obs, nil)
return obs
}
// Collect traverses the list of active records and observers and
// exports data for each active instrument. Collect() may not be
// called concurrently.
//
// During the collection pass, the export.Batcher will receive
// one Export() call per current aggregation.
@ -343,6 +513,13 @@ func (m *SDK) Collect(ctx context.Context) int {
m.collectLock.Lock()
defer m.collectLock.Unlock()
checkpointed := m.collectRecords(ctx)
checkpointed += m.collectObservers(ctx)
m.currentEpoch++
return checkpointed
}
func (m *SDK) collectRecords(ctx context.Context) int {
checkpointed := 0
m.current.Range(func(key interface{}, value interface{}) bool {
@ -358,25 +535,66 @@ func (m *SDK) Collect(ctx context.Context) int {
// TODO: Reconsider this logic.
if inuse.refMapped.inUse() || atomic.LoadInt64(&inuse.modified) != 0 {
atomic.StoreInt64(&inuse.modified, 0)
checkpointed += m.checkpoint(ctx, inuse)
checkpointed += m.checkpointRecord(ctx, inuse)
}
// Always continue to iterate over the entire map.
return true
})
m.currentEpoch++
return checkpointed
}
func (m *SDK) checkpoint(ctx context.Context, r *record) int {
if r.recorder == nil {
func (m *SDK) collectObservers(ctx context.Context) int {
checkpointed := 0
m.observers.Range(func(key, value interface{}) bool {
obs := key.(*observer)
result := observerResult{
observer: obs,
}
obs.callback(result)
checkpointed += m.checkpointObserver(ctx, obs)
return true
})
return checkpointed
}
func (m *SDK) checkpointRecord(ctx context.Context, r *record) int {
return m.checkpoint(ctx, r.descriptor, r.recorder, r.labels)
}
func (m *SDK) checkpointObserver(ctx context.Context, obs *observer) int {
if len(obs.recorders) == 0 {
return 0
}
r.recorder.Checkpoint(ctx, r.descriptor)
labels := export.NewLabels(r.labels.sorted, r.labels.encoded, m.labelEncoder)
err := m.batcher.Process(ctx, export.NewRecord(r.descriptor, labels, r.recorder))
checkpointed := 0
for encodedLabels, lrec := range obs.recorders {
epochDiff := m.currentEpoch - lrec.modifiedEpoch
if epochDiff == 0 {
checkpointed += m.checkpoint(ctx, obs.descriptor, lrec.recorder, lrec.labels)
} else if epochDiff > 1 {
// This is second collection cycle with no
// observations for this labelset. Remove the
// recorder.
delete(obs.recorders, encodedLabels)
}
}
if len(obs.recorders) == 0 {
obs.recorders = nil
}
return checkpointed
}
func (m *SDK) checkpoint(ctx context.Context, descriptor *export.Descriptor, recorder export.Aggregator, labels *labels) int {
if recorder == nil {
return 0
}
recorder.Checkpoint(ctx, descriptor)
exportLabels := export.NewLabels(labels.sorted, labels.encoded, m.labelEncoder)
exportRecord := export.NewRecord(descriptor, exportLabels, recorder)
err := m.batcher.Process(ctx, exportRecord)
if err != nil {
m.errorHandler(err)
}