8000 Adds withDynamicTags to metrics by vigoo · Pull Request #6234 · zio/zio · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Adds withDynamicTags to metrics #6234

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

Merged
merged 8 commits into from
Jan 17, 2022
Merged

Conversation

vigoo
Copy link
Contributor
@vigoo vigoo commented Jan 1, 2022

While migrating zio-aws to the new metrics API I ran into the problem of defining a metric aspect in which the tags depend on the result value of the wrapped effect. In this particular example, each AWS call returns a Described[A] value where the Described wrapper contains metadata about the call such as AWS service and operation name, which could be used as tags of the metric.

I tried to find a way in which required changes to the new the metric API are minimal while all the predefined and user defined metric aspects can be somehow used with this more dynamic tagging approach.

The result is a def withDynamicTags(f: A => Chunk[MetricLabel]): ZIOMetric[A] on each metric type which converts them to a metric which no longer exposes any metric-type specific API (as the tags are unknown) but are still valid metric aspects.

@vigoo vigoo requested review from jdegoes and adamgfraser January 1, 2022 11:10
private def changeCounter(value: A)(implicit trace: ZTraceElement): UIO[Unit] =
ZIO.succeed {
val extraTags = f(value)
val allTags = self.tags ++ extraTags
Copy link
Contributor Author

Choose a reason for hiding this comment

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

it could also replace the tags instead of combining the static ones with the dynamic ones

ZIO.succeed {
val extraTags = f(value)
val allTags = self.tags ++ extraTags
if (self.counter.metricKey.tags != allTags) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this chunk comparison can be dropped but then each invocation allocates a new internal.metrics.Counter

Copy link
Member

Choose a reason for hiding this comment

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

The happy path should be free of allocations, to the extent possible.

* Converts this counter metric to one where the tags depend on the measured
* effect's result value
*/
def withDynamicTags(f: A => Chunk[MetricLabel]): ZIOMetric[A] =
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def withDynamicTags(f: A => Chunk[MetricLabel]): ZIOMetric[A] =
def taggedWith(f: A => Chunk[MetricLabel]): ZIOMetric[A] =

@@ -278,15 +300,15 @@ object ZIOMetric {
* values over time.
*/
abstract class Gauge[A](final val name: String, final val tags: Chunk[MetricLabel]) extends ZIOMetric[A] { self =>
private[this] val gauge = internal.metrics.Gauge(name, tags)
private[this] val gauge = ZFiberRef.unsafeMake(internal.metrics.Gauge(name, tags))
Copy link
Member

Choose a reason for hiding this comment

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

The addition of a FiberRef wrapper here will impact performance. It may be better to make tags mutable if we can guard access sufficiently as to not violate referential transparency.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

One idea that would only make the construction non-threadsafe which I think is OK:

  • keep the non-fiber ref internal metric fields as they were but change them to var
  • add a fiber-ref version too which is also a var and initialized to null
  • the taggedWith sets the direct reference to null and initializes the fiber ref
  • using a helper function to get the gauge from either the fiber ref or the direct ref to work with the metric

I was thinking a lot on how else could I achieve this - to be able to use the existing (and arbitray) aspects with dynamic tags but could not come up with anything that is better or more performant so if you have a better idea please let me know

Copy link
Contributor Author

Choose a reason for hiding this comment

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

(by the way I did not do what I described here first because I assumed that FiberRef itself does something like this - that it's cheap until its never used on multiple fibers)

@vigoo
Copy link
Contributor Author
vigoo commented Jan 15, 2022

I added the above described optimization

* measured effect's result value
*/
def taggedWith(f: A => Chunk[MetricLabel]): ZIOMetric[A] = {
if (self.setCountRef eq null) {
Copy link
Member

Choose a reason for hiding this comment

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

I think this may violate referential transparency, because it has a side-effect which is not described in the return type.

Will the behaviour of the original self differ in any way after this method is invoked?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks, this made me realize that there is such a problem: if c1 is a counter and c2 is the modified counter, applying c2 first modifies the FiberRef and does not restore it, so a next call to c1 on the same fiber will have wrong tags.

taggedWith should call self.copy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually just checked and copy does not work correctly currently either (without my change):

  val count = ZIOMetric
    .count("test", MetricLabel("x", "1"))
    .copy("test2", Chunk(MetricLabel("x", "2")))

  val run =
    for {
      _      <- ZIO.succeed("hello") @@ count
      states <- UIO(MetricClient.unsafeStates)
      _      <- ZIO.debug(states.toList.mkString("\n"))
    } yield ()

prints

(Counter(test2,Chunk(MetricLabel(x,2))),MetricState(test2{x->2}, Counter(0.0)))
(Counter(test,Chunk(MetricLabel(x,1))),MetricState(test{x->1}, Counter(1.0)))

(because the copied aspect just calls the original self's apply which refers to the original metric).

So in addition to fix the referential transparency issue, should also:

  • fix copy
  • write some metric tests :)

self.apply(zio.tap(changeSetCount))

private def changeSetCount(value: A)(implicit trace: ZTraceElement): UIO[Unit] =
self.setCountRef.update { setCount =>
Copy link
Member

Choose a reason for hiding this comment

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

You can improve performance by factoring this lambda out into a constant lambda inside the class. That way, it won't be created each time changeSetCount is invoked.

@adamgfraser adamgfraser merged commit 6b66bba into zio:series/2.x Jan 17, 2022
@vigoo vigoo deleted the metric-dynamic-tags branch January 18, 2022 09:01
@vigoo vigoo mentioned this pull request Jan 18, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants
0