etishor / Metrics.NET

The Metrics.NET library provides a way of instrumenting applications with custom metrics (timers, histograms, counters etc) that can be reported in various ways and can provide insights on what is happening inside a running application.

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

RatioGauge and HitRatio "Gauge" ?

thunderstumpges opened this issue · comments

As we are moving from Windows Performance Counters to using Metrics.Net the first thing I noticed was that there is currently no "first class" support for a ratio gauge. I noticed in the java project there is an abstract RatioGauge but I don't see any implementations of it.

I took a shot in our fork and ported the abstract RatioGauge pretty easily. However I'd still like to provide better support for easily tracking Hit Ratio (or other type of success vs. total ratio). We are already using a Timer for most of these scenarios, and my prototype implementation used a FunctionGauge to divide the OneMinuteRate values between a "Hit" Meter and the Timer's OneMinuteRate. I don't know if maybe there is a cleaner way to do this?

I also started encapsulating this into a "HitRatioGauge" but then realized it kinda goes beyond what the understood meaning of "Gauge" is, as it had methods to mark a hit and a miss. I also considered forcing the HitRatioGauge to have only external MeterValueSources passed in for the Hits and Misses. But there is also no uniform way to use either a Meter or a Timer for the MeterValue of the "totals". It also does not encapsulate things well because the user still has to mark the hit externally from the HitRatioGauge.

I find it hard to believe someone else hasn't already thought about this or had to deal with it. Any suggestions or ideas? I'd be glad to help contribute this back if we can come up with a desired approach.

FYI my first fully external "prototype" looks like this:

private readonly Timer _fetchTimer = Metric.Timer("Fetches", Unit.Calls);
private readonly Meter _hits = Metric.Meter("Hits", Unit.Calls);
...
var hitData = Metric.DataProvider.CurrentMetricsData.Meters.Single(g => g.Name == "Hits");
var fetchesData = Metric.DataProvider.CurrentMetricsData.Timers.Single(g => g.Name == "Fetches");
Metric.Advanced.Gauge("HitRatio", () => new FunctionGauge(() =>
{
    var hits = hitData.Value.OneMinuteRate;
    var total = fetchesData.Value.Rate.OneMinuteRate;
    if (Double.IsNaN(total) || Double.IsInfinity(total) || total == 0)
        return Double.NaN;
    return hits/total;
}), Unit.Percent);

Thoughts ? Comments?

If I understand this correctly you basically need a Gauge that derives its value from other metrics.

It would make sense to have a new Gauge type that can access other metric values and aggregate on them. Let me think about this for a bit and i'll get back with an update.

Thanks, yes that is mostly it. Please see the above change for an initial take at it. It seems to work pretty well. I ported the RatioGauge class from the java project mostly directly. Then I added a MeterRatioGauge Which takes two MetricValueSource<MeterValue> or one and a MetricValueSource<TimerValue> and an optional function to resolve which rate to use for the ratio.

To use it I do this:

var hitData = Metric.DataProvider.CurrentMetricsData.Meters.Single(g => g.Name == "Hits");
var fetchesData = Metric.DataProvider.CurrentMetricsData.Timers.Single(g => g.Name == "Fetches");
Metric.Advanced.Gauge("HitRatio", () => new MeterRatioGauge(hitData,fetchesData), Unit.Percent);

What do you think?

Heh, I just found almost exactly what I was doing in that MeterRatioGauge here:
https://dropwizard.github.io/metrics/3.1.0/manual/core/#ratio-gauges

Checkout this commit 34e5b49

This should allow you to do exactly what you need.

Let me know what you think.

I really like the ValueReader, that makes it easy to get at values. Thanks for that. As for the RatioGauge implementation, that looks good also. However I'd be interested in a little more encapsulation (using something like the 'MeterRatioGauge' I provided in my commit. That would apply the same function to extract a value from each meter (or timer) so that we don't accidentally end up doing what you did in your sample (you used the absolute count in the numerator and a 1-minute rate in the denominator which doesn't really work does it?)

If it is amenable to you, I'd be glad to take your latest and adapt my MeterRatioGauge to be a specialization of your new RatioGauge. (really it is just a few more constructors taking the meter/timer as input and an optional single function for getting the value). If you don't want that in your project, I can keep it in mine, but I'd at least need to remove the 'sealed' modifier on the RatioGauge class.

And finally, do you think it good to protect a divide-by-zero on the ratio calculation as done in the port from Java?

// ReSharper disable once CompareOfFloatsByEqualityOperator
if (double.IsNaN(_denominator) || double.IsInfinity(_denominator) || _denominator == 0)
{
        return Double.NaN;
}
return _numerator / _denominator;

Oh, on a similar note, as I went to start using the MeterRatioGauge I had created, I found myself repeatedly creating the same three metrics over and over : A Meter or Timer for the "totals" of a hit ratio, and a Meter for the actual Hits on a hit ratio, and a custom "MeterRatioGauge" to track the Ratio. Lots of code duplication, so in our application code (this is not currently in the Metrics.NET library, but I can add it if you like) I did a sort of "composite" "HitRatioMetric" and "TimedHitRatioMetric" that looks like this, what do you think?

    public static class HitRatioMetricExtension
    {
        public static TimedHitRatioMetric TimedHitRatio(this MetricsContext rootContext, string rootMetricName, Unit unit, SamplingType samplingType = SamplingType.FavourRecent, TimeUnit rateUnit = TimeUnit.Seconds, TimeUnit durationUnit = TimeUnit.Milliseconds, MetricTags tags = default(MetricTags))
        {
            return new TimedHitRatioMetric(rootContext, rootMetricName, unit, samplingType, rateUnit, durationUnit, tags);
        }

        public static HitRatioMetric HitRatio(this MetricsContext rootContext, string rootMetricName, Unit unit, TimeUnit rateUnit = TimeUnit.Seconds, MetricTags tags = default(MetricTags))
        {
            return new HitRatioMetric(rootContext, rootMetricName, unit, rateUnit, tags);
        }
    }
    public class TimedHitRatioMetric : Timer
    {
        private readonly Timer _callTimer;
        private readonly Meter _hitMeter;

        public TimedHitRatioMetric(MetricsContext rootContext, string rootMetricName, Unit unit, SamplingType samplingType = SamplingType.FavourRecent, TimeUnit rateUnit = TimeUnit.Seconds, TimeUnit durationUnit = TimeUnit.Milliseconds, MetricTags tags = default(MetricTags))
        {
            _hitMeter = rootContext.Meter(rootMetricName + "Hits", unit,rateUnit,tags);
            _callTimer = rootContext.Timer(rootMetricName + "Calls", unit, samplingType, rateUnit, durationUnit, tags);

            var hitData = rootContext.DataProvider.CurrentMetricsData.Meters.Single(g => g.Name == rootMetricName + "Hits");
            var fetchesData = rootContext.DataProvider.CurrentMetricsData.Timers.Single(g => g.Name == rootMetricName + "Calls");
            rootContext.Advanced.Gauge(rootMetricName + "HitRatio", () => new MeterRatioGauge(hitData, fetchesData), Unit.Percent);
        }

        #region Timer interface proxy
        public long CurrentTime()
        {
            return _callTimer.CurrentTime();
        }

        public long EndRecording()
        {
            return _callTimer.EndRecording();
        }

        public TimerContext NewContext(string userValue = null)
        {
            return _callTimer.NewContext(userValue);
        }

        public void Record(long time, TimeUnit unit, string userValue = null)
        {
            _callTimer.Record(time, unit, userValue);
        }

        public long StartRecording()
        {
            return _callTimer.StartRecording();
        }

        public T Time<T>(Func<T> action, string userValue = null)
        {
            return _callTimer.Time(action, userValue);
        }

        public void Time(Action action, string userValue = null)
        {
            _callTimer.Time(action, userValue);
        }

        public void Reset()
        {
            _callTimer.Reset();
            _hitMeter.Reset();
        }
        #endregion Timer interface proxy

        public void MarkHit()
        {
            _hitMeter.Mark();
        }
    }

    public class HitRatioMetric
    {
        private readonly Meter _totalMeter;
        private readonly Meter _hitMeter;

        public HitRatioMetric(MetricsContext rootContext, string rootMetricName, Unit unit, TimeUnit rateUnit = TimeUnit.Seconds, MetricTags tags = default(MetricTags))
        {
            _totalMeter = rootContext.Meter(rootMetricName + "Calls", unit, rateUnit, tags);
            _hitMeter = rootContext.Meter(rootMetricName + "Hits", unit, rateUnit, tags);

            var hitData = rootContext.DataProvider.CurrentMetricsData.Meters.Single(g => g.Name == rootMetricName + "Hits");
            var totalData = rootContext.DataProvider.CurrentMetricsData.Meters.Single(g => g.Name == rootMetricName + "Calls");
            rootContext.Advanced.Gauge(rootMetricName + "HitRatio", () => new MeterRatioGauge(hitData, totalData), Unit.Percent);
        }

        public void MarkHit()
        {
            _hitMeter.Mark();
            _totalMeter.Mark();
        }

        public void MarkMiss()
        {
            _totalMeter.Mark();
        }
    }

I have merged your changes in, Please see this compare for a potential pull request. The 'MeterRatioGauge' is now quite clean, and just consistently applies a meter value extraction across two meters or a meter and a timer.
dev...ntent-ad:dev

Let me know what you think. Also let me know if you're interested in the aggregate HitRatioMetric and TimedHitRatioMetric helpers above.

Thanks!
Thunder

I'm only on phone internet atm but i hope to have some time tomorrow (if it will rain and i can't go climbing :) ) to look into this.

What i like about the current RatioGauge is that it is simple and straightforward. But i also like the idea of encapsulating common usages - i just did not had that many cases where I needed a ratio. The meter+timer sample you provided makes sense, especially for caches.

The current sample was not really meant to make sense, just to show how properties from different metrics can be combined.

I don't know why in java there is a need to protect from the division by zero but in clr there is no exception when operands are double. Deviding by zero is either positive or negative infinity or NaN if the numerator is zero. So there is no need for the upfront check. The only reason (but i don't think is worthed) would be to avoid calling the numerator if the denomnator is zero. This might be useful if the function is expensive, but then you would use some form of caching of the value.

It would be great if you can summarize what would be the best api on the MetricsContext that would allow you to register the RatioGauge cases you need and we can decide on the implementation from there. As a sidenote i have no problem removing the sealed modifier from RatioGauge - it only exists to prevent users from using inheritance when there is an easier way of accomplishing the same thing.

(Typed on a phone, excuse inherent typos)

I've merged the changes from your branch. I've also renamed the MeterRatioGauge to HitRatioGauge as it seems be more revealing.

About HitRatioMetric & TimedHitRatioMetric... I've played a bit with a few variants, but they seem a bit to specific. For now I think is best to keep them out of the main library and register them as extensions methods.

Thanks

Closing this for now. Feel free to re-open is necessary.

I have picked up latest in /dev and everything looks great. Thanks for working this in!