-
Notifications
You must be signed in to change notification settings - Fork 36
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
Observable instruments #162
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, thanks for the contribution!
I overlooked a few details regarding the callback signature while making a draft.
There are a few comments from my side regarding the abstraction. The direction looks good and promising!
|
||
import cats.effect.Resource | ||
|
||
trait AsyncInstrumentBuilder[F[_], Input, Instrument[_[_], _]] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm sorry, I've updated the scope of #158 recently since I spotted that we have ObservableInstrumentBuilder
.
We can change it rather than adding a new AsyncInstrumentBuilder
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course.
|
||
def withUnit(unit: String): Self | ||
def withDescription(description: String): Self | ||
def withCallback(cb: Instrument[F, Input] => F[Unit]): Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I see. I missed the Java's signature while making a draft.
We can mimic Java API and add ObservableMeasurement
:
trait ObservableMeasurement[F[_], A] {
def record(value: A, attributes: attributes: Attribute[_]*): F[Unit]
}
Then, the bulidWithCallback
can be:
def buildWithCallback(cb: ObservableMeasurement[F, Input] => F[Unit]): Resource[F, Instrument[F, Input]]
|
||
def withUnit(unit: String): Self | ||
def withDescription(description: String): Self | ||
def withCallback(cb: Instrument[F, Input] => F[Unit]): Self |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be no way to create an observable metric without a callback.
So we can merge withCallback
and create
:
def createWithCallback[A](cb: ObservableMeasurement[F, Input] => F[Unit]): Resource[F, Instrument[F, Input]]
Upd: already changed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Haha. I did this before your comment :)
|
||
trait ObservableGauge[F[_], A] { | ||
|
||
def observe(value: A, attributes: Attribute[_]*): F[Unit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to Java's API, observable Gauge cannot 'observe' value on-demand, it picks up all values exclusively from the callback:
So we can keep the trait empty.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yea. I kinda misused this already existing trait such that you get the instrument in the callback which intuitively makes sense to me.
If this looks ok-ish, I will add the remaining instruments and try to test it. Also look at the macro stuff. |
Looks good so far |
@@ -16,4 +16,4 @@ | |||
|
|||
package org.typelevel.otel4s.metrics | |||
|
|||
trait ObservableCounter[F[_], A] | |||
trait ObservableCounter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure what to do with these. It seems strange that they have these type parameters, when we really only use them in the callback. Now I tried to remove them, and that seems to aid in testing at least.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking about this for a while and I tend to agree.
.observableGauge("gauge") | ||
.withUnit("unit") | ||
.withDescription("description") | ||
.createWithCallback(x => x.observe(42.0)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be worth adding an Attribute
to. To make sure we propagate it downstream.
|
||
import org.typelevel.otel4s.Attribute | ||
|
||
trait Observable[F[_], A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Observable
name could be too broad. Also, it may clash with java.util.Observable
.
What if we use ObservableMeasurement
there?
d88c3cb
to
872534c
Compare
3e82e3f
to
fcdc740
Compare
I think this is good for a round of reviews. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me! I have a few minor suggestions regarding the documentation and that's it.
def withUnit(unit: String): Self = this | ||
def withDescription(description: String): Self = this | ||
def createWithCallback( | ||
_cb: ObservableMeasurement[F, Double] => F[Unit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A minor change:
_cb: ObservableMeasurement[F, Double] => F[Unit] | |
cb: ObservableMeasurement[F, Double] => F[Unit] |
@@ -16,4 +16,4 @@ | |||
|
|||
package org.typelevel.otel4s.metrics | |||
|
|||
trait ObservableCounter[F[_], A] | |||
trait ObservableCounter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've been thinking about this for a while and I tend to agree.
import javax.management.MBeanServer | ||
import javax.management.ObjectName | ||
|
||
object ObservableExample extends IOApp.Simple { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice! Thanks for the relevant example.
def createWithCallback( | ||
cb: ObservableMeasurement[F, Long] => F[Unit] | ||
): Resource[F, ObservableCounter] = | ||
Dispatcher.parallel.flatMap(dispatcher => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering which dispatcher is more suitable. The initiator of the callback evaluation is OpenTelemtry internals (it's a MetricProducer#collectAllMetrics
, I believe).
Should we guarantee the order?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm definitely not an expert on Dispatcher
semantics. I'll just change this to sequential, since these callbacks are called quite rarely, so I don't think performance should be a worry.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think it's going to matter a whole lot. My gut feeling is sequential is right.
/** Creates an instrument with the given `unit` and `description` (if any). | ||
*/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's worth mentioning how the callback should behave. (mostly taken from OpenTelemetry Java doc)
/** Creates an instrument with the given `unit` and `description` (if any). | |
*/ | |
/** Creates an instrument with the given callback, using `unit` and | |
* `description` (if any). | |
* | |
* The callback will be called when the instrument is being observed. | |
* | |
* The callback is expected to abide by the following restrictions: | |
* - Short-living and (ideally) non-blocking | |
* - Run in a finite amount of time | |
* - Safe to call repeatedly, across multiple threads | |
* | |
* @param cb | |
* The callback which observes measurements when invoked | |
*/ |
import org.typelevel.otel4s.Attribute | ||
|
||
trait ObservableMeasurement[F[_], A] { | ||
def record(value: A, attributes: Attribute[_]*): F[Unit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Scaladoc:
def record(value: A, attributes: Attribute[_]*): F[Unit] | |
/** Records a value with a set of attributes. | |
* | |
* @param value | |
* the value to record | |
* | |
* @param attributes | |
* the set of attributes to associate with the value | |
*/ | |
def record(value: A, attributes: Attribute[_]*): F[Unit] |
Changing the type constraint from |
import org.typelevel.otel4s.Attribute | ||
|
||
trait ObservableMeasurement[F[_], A] { | ||
def record(value: A, attributes: Attribute[_]*): F[Unit] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@christiankjaer it would be nice to preserve laziness via macro. It should be nearly identical to HistogramMacro
.
Could you take a look there too?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sure. The reason why I didn't look at the macro is that I kinda assumed that the observable stuff is for instrumenting from the outside, and the noop is then rarely needed. I will take a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
observable stuff is for instrumenting from the outside
Good point. Since the callback is a function ObservableMeasurement[F, A] => F[Unit]
, it will never be executed if we use a noop implementation. So there's no need to worry about allocations.
So, we can keep it as-is and skip macro.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great work, thanks!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think we're due for an 0.2 and have no problem with the broken compatibility. This is great work.
@christiankjaer the |
Should work now. |
Observable instruments
Fixes #158
Fixes #159
@iRevive Before I work more on this, how do you think the API looks?