Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add observer metric #474

Merged
merged 29 commits into from
Mar 5, 2020
Merged

Add observer metric #474

merged 29 commits into from
Mar 5, 2020

Conversation

krnowak
Copy link
Member

@krnowak krnowak commented Feb 11, 2020

Here is the initial stab at observers.

This adds integral and floating point observer instruments. The meter got Register{Int,Float}64Observer(name, callback, options...) functions. The observers are interfaces like as follows:

type {Int,Float}64Observer interface {
	SetCallback(callback)
	Unregister()
}

The callback is func({Int,Float}64ObserverResult) and the result type is:

type {Int,Float}64ObserverResult interface {
	Observe({int,float}64 value, LabelSet)
}

Both observer and observer result interfaces are implemented by SDK and mocks, noop and global only implement observer.

On the SDK side, each labelset passed to the result's observe function gets its own recorder/aggregator. So, every observer may have several aggregators if the callback called observe several times, each time with a different labelset. That more-or-less mimics what happens when you bind an instrument several times, each time with a different labelset.

There are also three new aggregators for observers, which are basically compositions of two other aggregators: (gauge, minmaxsumcount), (gauge, ddsketch) and (gauge, array). These are used by the simple selector for observer instruments.

SDK calls the observer callbacks in the collection routine, after checkpointing records. Currently there is no protection against a callback that does sleep(time.Eternity).

TODO:

  • docs
    • some are done, need more package docs (especially api/metric and sdk/metric)
  • tests
  • monotonicity option for observers
  • the observe implementation in sdk could likely be split into two parts:
    • first part would remain in observe and could call Update on recorder every time observer callback calls Observe on result.
    • second part would be moved to the observer loop in Collect and could call Process on batcher after observer callback is finished
  • the checkpointed variable should rather be incremented once per observer callback, not once per observation in the observer callback, I think
  • add observer aggregators and update aggregator selectors to handle the observer metric kind

Fixes #423

@krnowak
Copy link
Member Author

krnowak commented Feb 11, 2020

I must have some misunderstanding of the observers. During collection period we are going to call the callback of the observer. The callback receives a result object on which the callback can call the observe function many times. Also, observer is like a gauge, so by default we remember the value from the last event. So if we call the observe function several times within a single callback, we basically overwrite the last value all the time. Is this some corner case where calling observe multiple times doesn't make sense?

Also, I tried splitting the observe function as I described in the PR description and it looks to me that the value is not connected to the passed label set. We seem to care about label sets during checkpoint, for the batcher.

I wonder if it would make sense for the observer result object to have two functions:

type Int64ObserverResult interface {
    Observe(value ...int64)
    Labels(labels ...LabelSet)
}

Anyway, I need to read and understand the standard implementation for observers first. This seems to invalidate a lot I wrote above.

@krnowak
Copy link
Member Author

krnowak commented Feb 11, 2020

Scratch the above post. Now each observer has a separate recorder for every labelset passed to the observe function and it makes more sense now.

@krnowak
Copy link
Member Author

krnowak commented Feb 12, 2020

I'd like to get some initial opinion on the PR before I embark on a dull task of writing the tests for observers. ;)

@krnowak krnowak changed the title Observer Add observer metric Feb 12, 2020
@krnowak krnowak force-pushed the observer branch 2 times, most recently from 2addc87 to 0972daa Compare February 14, 2020 19:40
Copy link
Contributor

@rghetia rghetia left a comment

Choose a reason for hiding this comment

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

Overall looks good.

func (obs int64ObsImpl) SetCallback(callback metric.Int64ObserverCallback) {
if obs.trySetCallbackInDelegate(callback) {
return
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Are two attempts necessary?
Wouldn't this work?

obs.observer.setCallback(callback)
obs.trySetCallbackInDelegate(callback)

Copy link
Member Author

Choose a reason for hiding this comment

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

That would work too, but it is an (unprofiled ;) ) optimization - obs.observer.setCallback(callback) involves locking the mutex. And I expect the first call to trySetCallbackInDelegate to usually succeed anyway.

@@ -127,10 +135,53 @@ type Meter interface {
// a given name and customized with passed options.
NewFloat64Measure(name string, mos ...MeasureOptionApplier) Float64Measure

// NewInt64Observer creates a new integral observer with a
Copy link
Contributor

Choose a reason for hiding this comment

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

nit:s/NewInt64Observer/RegisterInt64Observer/

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

// given name, running a given callback, and customized with
// passed options. Callback can be nil.
RegisterInt64Observer(name string, callback Int64ObserverCallback, oos ...ObserverOptionApplier) Int64Observer
// NewFloat64Observer creates a new floating point observer
Copy link
Contributor

Choose a reason for hiding this comment

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

same.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

api/metric/api.go Show resolved Hide resolved
Copy link
Contributor

@jmacd jmacd left a comment

Choose a reason for hiding this comment

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

I couldn't finish looking this over. It's looking good but I've found a few ways we can limit scope. I'll keep reviewing this again tomorrow.

api/metric/api.go Show resolved Hide resolved
api/metric/api.go Show resolved Hide resolved
sdk/metric/aggregator/observerarray/observerarray.go Outdated Show resolved Hide resolved
sdk/metric/sdk.go Show resolved Hide resolved
@krnowak krnowak force-pushed the observer branch 2 times, most recently from 4668a7b to 0efae30 Compare February 24, 2020 21:07
@krnowak
Copy link
Member Author

krnowak commented Feb 24, 2020

Updated.

@krnowak
Copy link
Member Author

krnowak commented Feb 25, 2020

TestDirect seems to fail for some reason. I have run it on my computer in 100 iterations loop and all passed. :/

I suppose that we should be checking the core.Number equality in a different way, especially when dealing with floats.

@krnowak krnowak force-pushed the observer branch 3 times, most recently from 6069ce8 to 8780935 Compare March 3, 2020 14:57
@krnowak
Copy link
Member Author

krnowak commented Mar 3, 2020

Rebased. And fixed the TestDirect failure. Basically global meter was registering the observers at the delegate meter in random order. And mock meter was calling observer callbacks in random order too, so the order of taken measurements was also random. TestDirect was expecting some fixed order. My earlier runs of 100 iterations were badly done, because go test cached the test results. Go me.

Added some tests for the internal global meter alignment stuff - test 386 was failing on those.

Copy link
Contributor

@jmacd jmacd left a comment

Choose a reason for hiding this comment

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

Nice work!

api/metric/api.go Show resolved Hide resolved
sdk/metric/sdk.go Outdated Show resolved Hide resolved
@jmacd
Copy link
Contributor

jmacd commented Mar 3, 2020

One thing I wonder about is why we should have a SetCallback interface, as opposed to a constructor argument that would bind the callback once for the lifetime of the Observer?

@bogdandrutu It occurs to me that we haven't specified this detail, what do you think?

@rghetia
Copy link
Contributor

rghetia commented Mar 5, 2020

One thing I wonder about is why we should have a SetCallback interface, as opposed to a constructor argument that would bind the callback once for the lifetime of the Observer?

I agree. Observer without a Callback is incomplete and most likely it would not require changing it. If we do find a use case later to update the callback then we can always add the SetCallback or UpdateCallback api.

krnowak and others added 21 commits March 5, 2020 14:23
needs more package docs (especially for api/metric and sdk/metric)
Co-Authored-By: Mauricio Vásquez <[email protected]>
if a recorder for a labelset is unused for a second collection cycle
in a row, drop it
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.
So the tests can be reproducible - iterating a map made the order of
measurements random.
This wasn't checked at all. After adding the checks, the test-386
failed.
test-386 was complaining about it
@krnowak
Copy link
Member Author

krnowak commented Mar 5, 2020

Rebased to master. Dropped the SetCallback function.

@rghetia rghetia merged commit a202f16 into open-telemetry:master Mar 5, 2020
@krnowak krnowak deleted the observer branch March 5, 2020 20:30
@jmacd
Copy link
Contributor

jmacd commented Mar 5, 2020

👏

MikeGoldsmith pushed a commit to MikeGoldsmith/opentelemetry-go that referenced this pull request Mar 13, 2020
* 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 <[email protected]>

* 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 37b7d05.

* 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 <[email protected]>
Co-authored-by: Rahul Patel <[email protected]>
@pellared pellared added this to the untracked milestone Nov 8, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:metrics Part of OpenTelemetry Metrics
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Implement Observer metric instrument
6 participants