MetricFlow is a lightweight .NET library designed to simplify the way developers define and track technical and business related metrics (such as counters, timers, throughput, and dimensional breakdowns).
MetricFlow provides domain-oriented observability to monitor an entire application or surgically profile specific portions of your code (methods, background loops, external calls, batch jobs).
It could be used to create an in-memory metrics store with support for OpenTelemetry integration. It can be used in both ASP.NET Core and non-ASP.NET Core applications.
| Use Case | Best For | Registration Style | Primary Injections / APIs | Example Project |
|---|---|---|---|---|
| Simple Implementation | Targeted method/loop profiling, CLI jobs, algorithms | Standalone instantiation | new MetricTracker(...) |
BasicConsoleExample & AdvancedConsoleExample |
| DI-Based Implementation | Background workers, daemons, multi-tenant | Standard DI (IServiceCollection) |
IMetricTracker, [FromKeyedServices], IMetricFlow |
AdvancedConsoleWithDIExample |
| Console Log Sink & Triggers | Real-time formatted console logging, timer-free sampling | Standalone or DI | AddConsoleSink(...), ConfigureSinkTriggers(...) |
ConsoleSinkExample |
| Logger Sink (Serilog / ILogger) | Centralized structured logging, APM forwarders, log files | Standalone or DI | AddLoggerSink(...), ConfigureSinkTriggers(...) |
LoggerSinkExample |
| Web Implementation | Web APIs, microservices, HTTP routing | ASP.NET Core pipeline | app.UseMetricFlow(), app.MapMetricFlow("/metrics") |
WebApiExample |
| OpenTelemetry & Observability | Prometheus, Grafana, Datadog, OTLP collectors, CLI counters | OpenTelemetry SDK / BCL | .AddMetricFlowInstrumentation(), dotnet-counters |
OpenTelemetryConsoleExample |
- Counters: Track execution counts and occurrences of events.
- Timers & Duration: High-precision operation timing via lock-free stopwatch ticks.
- Throughput & Item Tracking: Measure batch sizes, entity counts, and processing rates (items/sec) with
ThroughputCounter. - Dimensional Breakdown & Slicing: Slice and compute operation distributions by business dimensions, tags, or computed rules with
DimensionCounterand built-in cardinality safeguards. - Memory Tracking: Measure per-operation heap allocations with
MemoryCounter. - Exception & Failure Tracking: Capture runtime exceptions with
ExceptionCounterand track logical versus exception failure distributions withFailureCounter. - OpenTelemetry Integration: Turnkey integration via
DotnetKit.MetricFlow.OpenTelemetryfor exporting metrics to Prometheus, Grafana, Datadog, and OTLP collectors with ambient distributed trace correlation. See the OpenTelemetry README. - Built on .NET Diagnostics: Built directly on native .NET BCL
System.Diagnostics.Metrics(Meter,Histogram,Counter,UpDownCounter) with lock-free hot paths and cardinality protection. See the Architecture Documentation. - CLI Diagnostics: Live real-time inspection in terminal via standard
dotnet-counters monitor. - Metadata and Tags: Add contextual information to metrics for rich analysis and filtering.
- Sampling: Thread-safe sampling control to balance performance and data volume.
- ASP.NET Core Integration: Turnkey middleware, endpoint routing resolution, and metric exposition endpoints.
- Pluggable & Extensible: Fully customizable counter lifecycle (
CounterBase<TState>) and trackers (MetricTrackerBase). - Multi-Targeting: Native support for
.NET 8.0and.NET 10.0.
- .NET SDK (8.0 or 10.0) installed on your machine
Install via NuGet package manager:
# Core library
dotnet add package DotnetKit.MetricFlow
# OpenTelemetry integration (optional)
dotnet add package DotnetKit.MetricFlow.OpenTelemetry
# ASP.NET Core integration (optional)
dotnet add package DotnetKit.MetricFlow.AspNetCoreOr clone and build locally:
git clone https://github.com/DotnetKit/MetricFlow.git
cd MetricFlow
dotnet restore
dotnet buildInitialize a tracker and optionally chain throughput, memory, exception, and dimension counters:
using DotnetKit.MetricFlow;
var tracker = new MetricTracker("OrderService", new()
{
["environment"] = "production"
})
.AddThroughputCounter()
.AddMemoryCounter()
.AddExceptionCounter()
.AddDimensionCounter("country");The simplest usage requires zero additional counters or complex configuration—by default, MetricTracker records high-precision execution duration:
using DotnetKit.MetricFlow;
// Initialize tracker (DurationCounter is included by default)
var tracker = new MetricTracker("BasicConsoleTopic", new()
{
["environment"] = "Development"
});
// 1. Scoped tracking with using statement
using (tracker.Track("ProcessOrder"))
{
await Task.Delay(10);
}
// 2. Delegate tracking with TrackAction
tracker.TrackAction("ValidatePayment", () => Thread.Sleep(5));
// 3. Print formatted telemetry
Console.WriteLine(tracker.ToString());Output:
BasicConsoleTopic
Topic Tags: environment:Development
[Duration] Metric: ProcessOrder
Duration (min, max, avg): 10.50 ms / 12.25 ms / 11.17 ms
Total duration: 55.86 ms
[Duration] Metric: ValidatePayment
Duration (min, max, avg): 5.64 ms / 5.83 ms / 5.70 ms
Total duration: 17.11 ms
Measures execution duration until the scope is disposed:
// Explicit metric name (with optional tags)
using (tracker.Track("ProcessOrder", new() { ["order_id"] = "123" }))
{
// work here
}
// Automatic metric name via [CallerMemberName]
void ProcessOrder()
{
using var _ = tracker.Track(); // Metric name is "ProcessOrder"
}Track batch or entity processing volume and calculate velocity (items/sec):
// 1. Specify item count upfront via TrackItems
using (tracker.TrackItems("ImportChannels", 500))
{
// Process 500 channels...
}
// 2. Or set dynamic count during / at completion of the operation
using (var scope = tracker.Track("IngestMessages"))
{
var count = await ReadAndProcessBatchAsync();
scope.SetItems(count); // Records processed count for ThroughputCounter
}
// Inspect results
var throughput = tracker.GetThroughputSnapshot("ImportChannels");
// throughput.TotalItems -> 500
// throughput.ItemsPerSecond -> e.g. 2,500 items/secSlice and categorize operation counts by business dimensions, tags, composite keys, or custom computed business rules with built-in cardinality safeguards:
// 1. Single dimension tag breakdown with cardinality limit (defaults to 250, overflow into [Other])
tracker.AddDimensionCounter("country", maxUniqueValues: 100);
// 2. Composite multi-tag dimension (e.g. "US / CreditCard", "DE / PayPal")
tracker.AddDimensionCounter(
name: "PaymentChannels",
dimensionKeys: ["country", "payment_method"]);
// 3. Computed business selector / conditional rules (zero custom metric classes needed)
tracker.AddDimensionCounter("CustomerTier", (tags, metadata) =>
{
var amount = metadata?.GetValueOrDefault("amount") ?? 0;
var country = tags?.GetValueOrDefault("country") ?? "Unknown";
if (amount >= 1000) return $"VIP_{country}";
if (amount >= 100) return $"Standard_{country}";
return null; // Return null to skip or mark untracked
});
// Tracking with tags and metadata
using (var scope = tracker.Track("ProcessOrder", new() { ["country"] = "US", ["payment_method"] = "CreditCard" }))
{
scope.SetMetadata("amount", 1500); // Evaluates CustomerTier to "VIP_US"
}
// Inspect snapshots
var countryDim = tracker.GetDimensionSnapshot("ProcessOrder", "country");
var paymentDim = tracker.GetDimensionSnapshot("ProcessOrder", "PaymentChannels");
var tierDim = tracker.GetDimensionSnapshot("ProcessOrder", "CustomerTier");Executes an action or task with automatic duration tracking and exception capture:
// Explicit metric name (sync or async, with optional return value)
tracker.TrackAction("ProcessOrder", () => DoWork());
var order = await tracker.TrackActionAsync("FetchOrder", async () => await FetchOrderAsync());
// Automatic metric name via [CallerMemberName]
void ProcessOrder()
{
tracker.TrackAction(() => DoWork()); // Metric name is "ProcessOrder"
}
async Task ProcessOrderAsync()
{
await tracker.TrackActionAsync(async () => await DoWorkAsync());
}MetricFlow allows you to retrieve strongly-typed telemetry snapshots programmatically using either generic queries or dedicated helper methods:
// 1. Generic snapshot queries by snapshot type
DurationSnapshot? duration = tracker.GetSnapshot<DurationSnapshot>("ProcessOrder");
ThroughputSnapshot? items = tracker.GetSnapshot<ThroughputSnapshot>("ProcessOrder");
ExceptionSnapshot? errors = tracker.GetSnapshot<ExceptionSnapshot>("ProcessOrder");
FailureSnapshot? failures = tracker.GetSnapshot<FailureSnapshot>("ProcessOrder");
MemorySnapshot? memory = tracker.GetSnapshot<MemorySnapshot>("ProcessOrder");
// With an explicit counter name (e.g. for custom counters or specific dimensions)
DimensionSnapshot? regionDim = tracker.GetSnapshot<DimensionSnapshot>("ProcessOrder", "Dimension:region");
// 2. Query multiple snapshots of the same type (e.g. all dimension breakdowns for an operation)
IEnumerable<DimensionSnapshot> allDims = tracker.GetSnapshots<DimensionSnapshot>("ProcessOrder");
// 3. Query all snapshots of a given type across the entire tracker
IEnumerable<ExceptionSnapshot> allErrors = tracker.GetAllSnapshots<ExceptionSnapshot>();
// 4. Dedicated typed helper methods (internally powered by GetSnapshot<T>)
DurationSnapshot? duration2 = tracker.GetDurationSnapshot("ProcessOrder");
ThroughputSnapshot? items2 = tracker.GetThroughputSnapshot("ProcessOrder");
ExceptionSnapshot? errors2 = tracker.GetExceptionSnapshot("ProcessOrder");
FailureSnapshot? failures2 = tracker.GetFailureSnapshot("ProcessOrder");
MemorySnapshot? memory2 = tracker.GetMemorySnapshot("ProcessOrder");
DimensionSnapshot? dimension = tracker.GetDimensionSnapshot("ProcessOrder", "region");Register MetricFlow in any .NET application using Microsoft.Extensions.DependencyInjection without ASP.NET Core dependencies:
using Microsoft.Extensions.DependencyInjection;
using DotnetKit.MetricFlow;
// Register MetricFlow with topic and optional configuration
services.AddMetricFlow("WorkerDaemon", options =>
{
options.SamplingRate = 1.0;
options.AddTagsEnricher(tags =>
{
tags["env"] = "Production";
});
});
// Inject IMetricTracker or MetricTracker anywhere in your application
public class QueueWorker(IMetricTracker tracker)
{
public async Task ProcessAsync()
{
using (tracker.Track("ProcessMessage"))
{
await HandleMessageAsync();
}
}
}Register multiple isolated topic trackers in the same application via the fluent builder (AddMetricTracker) and resolve them via the IMetricFlow facade or native keyed injection:
// Fluent builder registration
services.AddMetricFlow("WebApi", options => ...)
.AddMetricTracker("WeatherRadar", options => ...);
// 1. Resolve via IMetricFlow facade
public class IngestionService(IMetricFlow metricFlow)
{
public void Run()
{
var tracker = metricFlow.GetTracker("WeatherRadar");
using var scope = tracker.Track("ScanRadar");
}
}
// 2. Or resolve via native Keyed Services (.NET 8+)
public class RadarWorker([FromKeyedServices("WeatherRadar")] IMetricTracker tracker)
{
// ...
}Enable automated HTTP request duration, memory allocation, and failure tracking via middleware:
using DotnetKit.MetricFlow.AspNetCore.Extensions;
var builder = WebApplication.CreateBuilder(args);
// Register MetricFlow with optional tag enrichment
builder.Services.AddMetricFlow("WebApiExample", options =>
{
options.EnrichTags = (tags, context) =>
{
if (context.Request.Headers.TryGetValue("X-Tenant-ID", out var tenantId))
{
tags["tenant_id"] = tenantId!;
}
};
});
var app = builder.Build();
// Automated request tracking middleware
app.UseMetricFlow();
// Expose metric snapshot endpoint
app.MapMetricFlow("/metrics");
app.Run();MetricFlow seamlessly bridges domain metrics and scoped tracking to the standard OpenTelemetry .NET ecosystem via DotnetKit.MetricFlow.OpenTelemetry.
For full setup guides, fluent builder APIs (.WithOpenTelemetry()), standalone meter provider instrumentation (.AddMetricFlowInstrumentation()), BCL instrument mappings, and ambient trace correlation, see the DotnetKit.MetricFlow.OpenTelemetry README.
Because MetricFlow is backed directly by System.Diagnostics.Metrics.Meter, you can inspect active metrics in real time in your terminal without configuring any external collector:
# Monitor live operations for topic "OrderProcessingService"
dotnet-counters monitor -p <PID> --counters DotnetKit.MetricFlow.OrderProcessingService
# Or monitor across all MetricFlow topics
dotnet-counters monitor -p <PID> --counters DotnetKit.MetricFlowMetricFlow is engineered around lock-free hot-path execution, decoupled state-token lifecycles, and a zero-dependency core bridging directly to .NET BCL System.Diagnostics.Metrics.
For detailed architecture, hot-path dispatch mechanics, the System.Diagnostics bridge, concurrency model, and custom counter lifecycles, see ARCHITECTURE.md.
- BasicConsoleExample: Simplest implementation demonstrating minimal tracker setup and duration measurement with zero optional counters.
- AdvancedConsoleExample: Full multi-counter demonstration including duration, throughput (items/sec and batch sizing), memory allocation, exceptions, and delegate tracking.
- AdvancedConsoleWithDIExample: Standard Microsoft DI integration demonstrating fluent builder (
AddMetricTracker),AddTagsEnricher, multi-topic tracking, keyed services ([FromKeyedServices]), worker pipelines, and programmatic telemetry queries. - ConsoleSinkExample: Structured console log sink (
ConsoleMetricSink) featuring ANSI color coding, prefixes, timestamps, and timer-free execution-lifecycle sampling triggers (stride, latency thresholds, and failure triggers). - LoggerSinkExample: Structured
ILoggersink (LoggerMetricSink) integrated with Serilog, demonstrating dynamic log level elevation on failures, structured property extraction, and timer-free sampling triggers. - OpenTelemetryConsoleExample: Complete OpenTelemetry integration demonstrating
.AddMetricFlowInstrumentation(), raw console metric export, cardinality protection, batch items throughput, and trace correlation. - WebApiExample: Demonstrates ASP.NET Core integration, middleware, and
/metricsendpoint. - CustomCounters: Demonstrates extension capabilities by implementing custom counters and trackers.
Run the examples:
# Basic console example (minimal setup)
dotnet run --project examples/BasicConsoleExample
# Multi-counter console example (advanced: duration, throughput, memory, exceptions)
dotnet run --project examples/AdvancedConsoleExample
# Dependency injection console example (DI, AddTagsEnricher, keyed services)
dotnet run --project examples/AdvancedConsoleWithDIExample
# Structured console log sink example (ConsoleMetricSink, timer-free lifecycle triggers)
dotnet run --project examples/ConsoleSinkExample
# Structured ILogger sink example (Serilog integration, dynamic log levels)
dotnet run --project examples/LoggerSinkExample
# OpenTelemetry console example (OpenTelemetry SDK, BCL bridge, console exporter)
dotnet run --project examples/OpenTelemetryConsoleExample
# ASP.NET Core Web API example
dotnet run --project examples/WebApiExampleMetricFlow is designed to be extensible. You can implement custom counters by deriving from CounterBase<TState> and pre-configure trackers by inheriting from MetricTrackerBase.
See examples/CustomCounters and the Architecture Guide for complete implementation patterns.
See ROADMAP.md for the development roadmap, upcoming milestones, and architectural improvements.
See CHANGELOG.md for a detailed history of changes, releases, and fixes.