Ich möchte gerne wissen, wie oft meine Methoden aufgerufen wird oder wie lange diese gerade braucht, dann braucht man eine Art von Instrumentierung. Es gibt wie immer verschiedene Wege, um die Anforderung zu erreichen. Es gibt die PerformanceCounter (Windows based) aus dem Namespace System.Diagnostics, EventCounters (Cross Plattform), Third-party APIs oder eben den Namespace System.Diagnostics.Metrics.
Mit den Klassen aus diesem Namespace werde ich mich in diesem Artikel beschäftigen. Zentrale Klasse in diesem Namespace ist der Type Meter. Ich benutze zwei Erzeugungsmethoden CreateCounter(), zum Zählen der Methoden-Aufrufe, und CreateHistogram() zur Aufnahme der Processing Time einer Methode. Des Weiteren habe ich eine Abstraktionsebene geschaffen, die es mir erlaubt, eine beliebige Klasse und deren öffentliche Methoden mit diesen beiden Metriken, Zählen der Methodenaufrufe und Aufnahme der Zeiten, die der Methodenaufruf dauert, auszustatten. Dabei setze ich auf Castle.Core in Verbindung mit dem Microsoft.Extensions.DependencyInjection Package. Über Castle.Core kann ich das IInterceptor Interface implementieren, um vor dem eigentlichen Methodenaufruf die Stopwatch Logik zum Start zu implementieren und danach, um die Metrik-Logik aufzurufen.
Der Interceptor sieht so aus:
using System.Diagnostics;
using Castle.DynamicProxy;
namespace CounterTestApp;
public class PerformanceMetricInterceptor<TImplementation>(IServicePerformanceMetrics<TImplementation> servicePerformanceMetrics) : IInterceptor where TImplementation : class
{
public void Intercept(IInvocation invocation)
{
var stopwatch = new Stopwatch();
stopwatch.Start();
invocation.Proceed();
servicePerformanceMetrics.RecordMethod(invocation.Method.Name, stopwatch.ElapsedMilliseconds);
}
}
Vor und nach dem Methodenaufruf erfolgen die speziellen Arbeiten. Das Interface IServicePerformanceMetrics liefert die Metrikmethoden. Die Klasse sieht wie folgt aus:
using System.Collections.Concurrent;
using System.Diagnostics.Metrics;
namespace CounterTestApp;
public class ServicePerformanceMetrics<T> : IServicePerformanceMetrics<T> where T : class
{
private readonly T _implementationType;
private readonly ConcurrentDictionary<string, MethodMetrics> _methodMetrics = new();
private readonly Meter _meter;
public ServicePerformanceMetrics(T implementationType, IMeterFactory meterFactory)
{
_implementationType = implementationType ?? throw new ArgumentNullException(nameof(implementationType));
var meterFactoryCheck = meterFactory ?? throw new ArgumentNullException(nameof(meterFactory));
var baseName = GetBaseName();
_meter = meterFactoryCheck.Create(baseName);
}
private string GetBaseName()
{
var genericType = _implementationType.GetType();
var typeName = genericType.FullName ??
throw new InvalidOperationException(
„Initialization error. Type T does not have a fullname property value“);
return typeName;
}
public void RecordMethod(string methodName, long requestDurationInMilliseconds)
{
var methodMetrics = GetMethodMetrics(methodName);
methodMetrics.RecordMethod(requestDurationInMilliseconds);
}
public void RecordError(string methodName)
{
var methodMetrics = GetMethodMetrics(methodName);
methodMetrics.RecordError();
}
private MethodMetrics GetMethodMetrics(string methodName)
{
return _methodMetrics.GetOrAdd(methodName,
(_) => new MethodMetrics(_meter, methodName));
}
}
public interface IServicePerformanceMetrics<T>
{
void RecordMethod(string methodName, long requestDurationInMilliseconds);
void RecordError(string methodName);
}
In dieser Klasse wird zu einem beliebigen Typen, in meinem Beispiel der Klasse WordCounter ein Meter-Objekt mit dem Namen CounterTestApp.WordCounter erstellt. Diesen kann ich dann später zur Laufzeit abfragen. Wie das geht, zeige ich später.
Weiterhin benutze ich hier noch eine Hilfsklasse, die pro Methode die verschiedenen Metriken in sich trägt und aktualisiert. Diese sieht dann so aus:
using System.Diagnostics.Metrics;
namespace CounterTestApp;
public class MethodMetrics
{
private readonly Counter<int> _counterErrors;
private readonly Counter<int> _counterCalls;
private readonly Histogram<double> _histogramMilliseconds;
public MethodMetrics(Meter meter, string baseName)
{
var errorCounterName = $“{baseName}.errors“;
_counterErrors = meter.CreateCounter<int>(errorCounterName);
var callCounterName = $“{baseName}.count“;
_counterCalls = meter.CreateCounter<int>(callCounterName);
var durationName = $“{baseName}.processing_time“;
_histogramMilliseconds = meter.CreateHistogram<double>(durationName, „ms“);
}
public void RecordMethod(long requestDurationInMilliseconds)
{
_counterCalls.Add(1);
_histogramMilliseconds.Record(requestDurationInMilliseconds);
}
public void RecordError()
{
_counterErrors.Add(1);
}
Stellt sich noch die Frage wie der konkrete Type mit dem Interceptor verbunden wird. Das geschieht über eine Extension Methoden auf dem IServiceCollectionInterface. Diese sieht dann so aus:
using Castle.DynamicProxy;
using Microsoft.Extensions.DependencyInjection;
namespace CounterTestApp;
public static class CounterServiceExtension
{
public static IServiceCollection AddMeasurementTransient<TService, TImplementation>(this IServiceCollection collection) where TService : class
where TImplementation : class, TService
{
collection.AddSingleton<IInterceptor, PerformanceMetricInterceptor<TImplementation>>();
collection.AddSingleton<IServicePerformanceMetrics<TImplementation>, ServicePerformanceMetrics<TImplementation>>();
collection.AddSingleton<TImplementation>();
collection.Add(new ServiceDescriptor(typeof(TService), provider =>
{
var generator = new ProxyGenerator();
var interceptor = provider.GetRequiredService<IInterceptor>();
return generator.CreateInterfaceProxyWithTarget<TService>(ActivatorUtilities.CreateInstance<TImplementation>(provider), interceptor);
}, ServiceLifetime.Singleton));
return collection;
}
}
Im Program.cs sieht das dann so aus:
using CounterTestApp;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
var host = Host.CreateDefaultBuilder().ConfigureServices(collection =>
{
collection.AddMeasurementTransient<IWordCounter, WordCounter>();
}).Build();
//dotnet-counters monitor -n CounterTestApp CounterTestApp.WordCounter
var wordCounter = host.Services.GetRequiredService<IWordCounter>();
for (var i = 0; i < 100; i++)
{
wordCounter.CountWords();
Thread.Sleep(1000);
}
Console.ReadLine();
Der WordCounter selbst hat keine Ahnung, dass er untersucht wird. Über die Extension-Methode wird alles erledigt.
Über das Tool dotnet-counter kann ich mir die Live-Daten anschauen. Das passiert aus der Command Prompt dann so:
CounterTestApp == Name der Applikation
CounterTestApp.WordCounter == Name der bei der Ezeugung des Typs Meter mitgegeben wurde (Namespace.Klassenname)
Sollte das CLI Tool noch nicht installiert sein:
dotnet tool install –global dotnet-counters àÜber Command Prompt als Admin
dotnet-counters monitor -n CounterTestApp CounterTestApp.WordCounter
Dieses Beispiel findet ihr auf GitHub.