Add support for reporting application metrics via logging #708
Description
Overview
We want to enable applications to emit arbitrary named metrics and have them captured by telemetry/logging systems (App Insights, Elasticsearch/ELK, etc.) as well as other metrics systems (Statsd, InfluxDB, etc.). An abstraction is desired in order to allow applications to emit metrics without being coupled to the destination of the metrics, particularly with features like App Insights auto-lightup where the app doesn't directly depend upon App Insights.
Proposal
We will add support for metrics reporting to ILogger
. It will be done via a new companion interface IMetricsLogger
, which loggers may optionally implement. Applications can emit metrics through this interface (see API below), and logger providers which support metrics can receive and process those metrics. Metrics are (name, value)
tuples, where value
is always floating point number. Metrics are expected to be viewed in aggregate, hence it doesn't make sense to use a generic data type like object
for the metric, so metric values are always double
s. Supporting arbitrary types of metric values is a non-goal. Reporting metrics should be as efficient as possible, especially in the case where no logger provider is registered to listen for them, or they are filtered out.
Why add to ILogger
?
Initially, I considered developing a new abstraction for Metrics. However, I encountered a few concerns:
- We would need to develop a structure for naming/organizing metrics (likely using type/namespace names as a base)
- We would need to develop a structure for filtering metrics (by categories, names, granularities, etc.)
- Many telemetry systems provide support for both log and metric collection, and would have to provide a new system
- Consumers have to pull in two different diagnostics reporters from DI (
ILogger<T>
,IMetricReporter<T>
, etc.) - This would create Yet Another Diagnostics Abstraction (on top of EventSource, EventCounters, DiagnosticSource, Perf Counters, yada yada yada (see what I did there? ;P))
The fact that 1 and 2 above already exist in the Logging pipeline, and many metric sinks are also logging sinks, led to the idea of integrating metrics into Logging rather than creating a completely new abstraction.
The main reason why we would not want to add this to ILogger
would be a risk
The API
IMetricLogger
Optional companion interface for ILogger
implementations to indicate their support for metrics. The ILogger
instances returned by ILoggerProvider
are expected to implement this interface if and only if they wish to receive metrics.
namespace Microsoft.Extensions.Logging
{
public interface IMetricLogger
{
/// <summary>
/// Define a new metric with the provided name and return an <see cref="IMetric"/> that can be used to report values for that metric.
/// </summary>
/// <param name="name">The name of the metric to define</param>
/// <returns>An <see cref="IMetric"/> that can be used to report values for the metric</returns>
IMetric DefineMetric(string name);
}
}
IMetric
Interface that allows metric data to be reported. Metric values
namespace Microsoft.Extensions.Logging
{
public interface IMetric
{
/// <summary>
/// Record a new value for this metric.
/// </summary>
/// <param name="value">The value to record for this metric</param>
void RecordValue(double value);
}
}
LoggerMetricsExtensions
Provides an extension method on ILogger
to enable consumers to emit metrics, even if the underlying logger provider doesn't support it (of course, the metrics will be ignored in that case).
namespace Microsoft.Extensions.Logging
{
public static class LoggerMetricsExtensions
{
/// <summary>
/// Define a new metric with the provided name and return an <see cref="IMetric"/> that can be used to report values for that metric.
/// </summary>
/// <remarks>
/// If none of the registered logger providers support metrics, values recorded by this metric will be lost.
/// </remarks>
/// <param name="name">The name of the metric to define</param>
/// <returns>An <see cref="IMetric"/> that can be used to report values for the metric</returns>
public static IMetric DefineMetric(this ILogger logger, string name)
{
if(logger is IMetricLogger metricLogger)
{
return metricLogger.DefineMetric(name);
}
return NullMetric.Instance;
}
}
}
MetricValueExtensions
Extension methods for IMetric
to support other common metric value types (as mentioned above, the underlying type is still double
, so the value must be representable as a double
).
Suggestion: We could have a .Time()
API that returns an IDisposable
which uses a Stopwatch to do the timing for you as well. That would be easy to add later though.
using System;
namespace Microsoft.Extensions.Logging
{
public static class MetricValueExtensions
{
/// <summary>
/// Record a new value for this metric.
/// </summary>
/// <remarks>
/// This is a convenience method that will convert the <see cref="TimeSpan"/> to a <see cref="double"/> via
/// the <see cref="TimeSpan.TotalMilliseconds"/> property.
/// </remarks>
/// <param name="metric">The metric to record the value on.</param>
/// <param name="value">The value to record for this metric.</param>
public static void RecordValue(this IMetric metric, TimeSpan value) => metric.RecordValue(value.TotalMilliseconds);
}
}
Logger
updates
The aggregate Logger
class we return from LoggerFactory.CreateLogger
would be updated to support IMetricsLogger
, even if none of the ILogger
s it aggregates support it. It would only send metrics to the loggers that are known to support the interface.
Define
/Record
pattern
In order to keep the performance cost as low as possible for recording the actual metric value, the API uses a Define
/Record
pattern. Consumers should expect to call DefineMetric("foo")
once for a particular value of "foo" and store the result as globally as possible (in singleton types, etc.). It is DefineMetric
that does the bulk of the work to set up the metric recording. After calling DefineMetric
, the RecordValue
call on the IMetric
interface can choose how to store the value based on the providers needs. For example, in a provider that plans to send pre-aggregated metrics, the RecordValue
could simply update rolling aggregate values and then drop the actual recording (thus having constant storage requirements for each metric). If a provider needs to report individual metrics, it can use dynamically-allocated buffers or ring buffers, depending upon the configuration and the needs of the provider. As an example of an IMetric
implementation based on pre-aggregating data, see https://gist.github.com/anurse/03c6746204317bd3616c372b4df9dbba
Filtering Metrics
Metrics should be filterable, and they should participate in the existing Logger filtering pipeline. My current proposal is that Metrics are treated like LogLevel.Critical
messages, in that if the category is enabled at all, the metrics are written. We could consider capturing a LogLevel
in .DefineMetric
and using that to provide different "levels" of metrics. Since the existing filters only let you filter by Provider, Category, and Level, it is very difficult to filter out specific metrics by name (as that would require a new Filter structure). This may make LogLevel
support for metrics more important.
@pakrym and I talked and came up with a specific suggestion
Metrics can be filtered at the provider and category name, but there is no concept of a LogLevel
for metrics. However, filters can disable all metrics for a particular provider/category combination. To do this, we'll add an AllowMetrics
boolean to LoggerFilterRule
which defaults to true
. When writing a metric, we'll check the provider and category name against the filter options as well as checking this boolean.
Scopes and Property Bags
FEEDBACK REQUESTED
My initial thought is that Metrics exist outside of scopes, as they are generally aggregated and Scopes imply per-event data. Providers could, in theory, give their IMetric
instances access to read active scope data, and attach them to metrics if they choose (for example, systems that report unaggregated metrics may want to). For similar reasons, property bags (i.e. Log Values/Structured Logging, etc.) don't really make sense for metrics in general as they are usually aggregated. Having said that, many providers do support arbitrary properties (InfluxDB, App Insights), so some level of support may be useful. We could easily add an optional parameter to .RecordValue
to allow adding arbitrary properties to each metric, and providers could choose to disregard it if it doesn't make sense for that provider.
Impact on existing Providers
There is minimal impact on existing Providers. Because the interface provides an opt-in model for a provider to receive metrics, existing providers are unaffected. I'd imagine providers like Serilog would not participate in this system as they don't have an infrastructure for metrics (/cc @nblumhardt , I may be wrong!). Providers like App Insights may choose to either add metrics support to their existing providers or add new providers that only handle metrics (and ignore log messages) that are co-registered.