Lyo.Metrics 2.0.0

dotnet add package Lyo.Metrics --version 2.0.0
                    
NuGet\Install-Package Lyo.Metrics -Version 2.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Lyo.Metrics" Version="2.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Lyo.Metrics" Version="2.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Lyo.Metrics" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Lyo.Metrics --version 2.0.0
                    
#r "nuget: Lyo.Metrics, 2.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Lyo.Metrics@2.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Lyo.Metrics&version=2.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Lyo.Metrics&version=2.0.0
                    
Install as a Cake Tool

Lyo.Metrics

Thread-safe counters, gauges, histograms, timings, errors, and events — in-memory, OpenTelemetry, and null implementations.

Features

  • Concurrency. Per-key locks plus ConcurrentDictionary.
  • Metric types. Gauges, counters, timings, histograms, events, errors.
  • Implementations. OpenTelemetryMetrics, MetricsService (in-memory), and NullMetrics for tests.
  • Bounds. MaxHistogramValues, MaxEventQueueSize, and key-lock cleanup on KeyLockCleanupIntervalMinutes.
  • Safety. Bounded collections and overflow protection on counters.
  • Options. ValidateTags, SamplingRate, ThrowOnConversionErrors, InvalidTagCharacters.
  • No dependencies. Only Lyo.Exceptions, no NuGet packages. Service registration lives in Lyo.Metrics.DependencyInjection so instrumenting a low-dependency package stays cheap.
  • Values. Accepts IConvertible numbers (long, int, decimal, float, and similar).

Examples

Subscribe to events

using Lyo.Metrics;

// Create a metrics service
var metrics = new MetricsService();

// Record a counter
metrics.IncrementCounter("requests.total");

// Record a counter with value
metrics.IncrementCounter("bytes.processed", 1024);

// Record a counter with tags
metrics.IncrementCounter("requests.total", tags: [("method", "GET"), ("status", "200")]);

// Record a gauge (current value)
metrics.RecordGauge("cache.size", 1500);

// Record timing using a timer
using (metrics.StartTimer("operation.duration"))
{
    // Your operation here
    await DoSomethingAsync();
}

// Record an error
try
{
    await ProcessDataAsync();
}
catch (Exception ex)
{
    metrics.RecordError("data.processing", ex);
}

Construct directly, no container

using Lyo.Metrics;

// A package that only records metrics needs nothing but this reference.
// Take IMetrics as an optional constructor parameter and fall back to the no-op
// singleton so callers who do not care about metrics pay nothing.
public sealed class MyService(IMetrics? metrics = null)
{
    private readonly IMetrics _metrics = metrics ?? NullMetrics.Instance;

    public async Task ProcessAsync()
    {
        using (_metrics.StartTimer("my_service.process"))
        {
            _metrics.IncrementCounter("my_service.calls");
            await DoWorkAsync();
        }
    }
}

// For container registration, reference Lyo.Metrics.DependencyInjection
// and call services.AddLyoMetrics().

MetricsOptions

var options = new MetricsOptions
{
    // Maximum number of events to keep in the event queue
    MaxEventQueueSize = 10000,
    
    // Maximum number of values per histogram
    MaxHistogramValues = 1000,
    
    // Whether to throw exceptions on conversion errors
    ThrowOnConversionErrors = false,
    
    // Interval for cleaning up unused key locks (in minutes)
    KeyLockCleanupIntervalMinutes = 60,
    
    // Sampling rate (0.0 to 1.0)
    // 1.0 = record all metrics, 0.5 = record 50% of metrics
    SamplingRate = 1.0,
    
    // Whether to validate and sanitize tag keys/values
    ValidateTags = true,
    
    // Characters not allowed in tag keys/values
    InvalidTagCharacters = new HashSet<char> { '|', '=', '\n', '\r' }
};

var metrics = new MetricsService(options);

ASP.NET Core

// Registration requires the Lyo.Metrics.DependencyInjection package.
public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddLyoMetrics(options =>
        {
            options.MaxEventQueueSize = 50000;
            options.SamplingRate = 1.0;
        });
        
        services.AddControllers();
    }
}

public class MyController : ControllerBase
{
    private readonly IMetrics _metrics;
    
    public MyController(IMetrics metrics)
    {
        _metrics = metrics;
    }
    
    [HttpGet]
    public async Task<IActionResult> Get()
    {
        using (_metrics.StartTimer("api.get.duration"))
        {
            _metrics.IncrementCounter("api.requests", tags: [("endpoint", "get"), ("method", "GET")]);
            
            var result = await ProcessRequestAsync();
            
            _metrics.IncrementCounter("api.requests.success");
            return Ok(result);
        }
    }
}

Background service

public class MyBackgroundService : BackgroundService
{
    private readonly IMetrics _metrics;
    
    public MyBackgroundService(IMetrics metrics)
    {
        _metrics = metrics;
    }
    
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            using (_metrics.StartTimer("background.job.duration"))
            {
                try
                {
                    await ProcessJobAsync();
                    _metrics.IncrementCounter("background.job.success");
                }
                catch (Exception ex)
                {
                    _metrics.RecordError("background.job", ex);
                    _metrics.IncrementCounter("background.job.failure");
                }
            }
            
            await Task.Delay(TimeSpan.FromMinutes(1), stoppingToken);
        }
    }
}

Errors

try
{
    await ProcessDataAsync();
}
catch (Exception ex)
{
    metrics.RecordError("data.processing", ex);
    
    // With additional tags
    metrics.RecordError("data.processing", ex, tags: [("source", "api"), ("user_id", userId)]);
}

Events

// Simple event
metrics.RecordEvent("user.login");

// Event with value
metrics.RecordEvent("file.uploaded", fileSizeBytes);

// Event with tags
metrics.RecordEvent("user.login", tags: [("provider", "google")]);

Read a counter

var metrics = new MetricsService();

metrics.IncrementCounter("requests.total", tags: [("method", "GET")]);

var count = metrics.GetCounterValue("requests.total", tags: [("method", "GET")]);

Read a gauge

metrics.RecordGauge("cache.size", 1500);

var size = metrics.GetGaugeValue("cache.size");
if (size.HasValue)
{
    Console.WriteLine($"Cache size: {size.Value}");
}

Read a histogram

metrics.RecordHistogram("response.size", 1024);
metrics.RecordHistogram("response.size", 2048);
metrics.RecordHistogram("response.size", 4096);

var histogram = metrics.GetHistogram("response.size");
if (histogram != null)
{
    var min = histogram.Values.Min();
    var max = histogram.Values.Max();
    var avg = histogram.Values.Average();
    Console.WriteLine($"Min: {min}, Max: {max}, Avg: {avg}");
}

Read events

// Get events (default: last 1000)
var events = metrics.GetEvents(); // last 1000
var events100 = metrics.GetEvents(100);

foreach (var evt in events)
{
    Console.WriteLine($"{evt.Name}: {evt.Value} at {evt.Timestamp}");
}

Clear metrics

metrics.Clear(); // Clears all counters, gauges, histograms, and events

Export a snapshot

var snapshot = metrics.Export();

Console.WriteLine($"Total metrics recorded: {snapshot.TotalMetricsRecorded}");
Console.WriteLine($"Counters: {snapshot.Counters.Count}");
Console.WriteLine($"Gauges: {snapshot.Gauges.Count}");
Console.WriteLine($"Histograms: {snapshot.Histograms.Count}");

// Serialize to JSON
var json = JsonSerializer.Serialize(snapshot);

Names that mean something

// Good
metrics.IncrementCounter("http.requests.total");
metrics.RecordGauge("cache.size_bytes");

// Bad
metrics.IncrementCounter("c1");
metrics.RecordGauge("x");

Tags as dimensions

// Good - use tags for filtering/grouping
metrics.IncrementCounter("requests.total", tags: [("method", "GET"), ("status", "200"), ("endpoint", "/api/users")]);

// Bad - create separate metrics for each dimension
metrics.IncrementCounter("requests.get.200.users");
metrics.IncrementCounter("requests.get.200.products");

Sampling high-volume metrics

var options = new MetricsOptions
{
    SamplingRate = 0.1 // Sample 10% of metrics
};

Timers around work

// Good - automatic timing
using (metrics.StartTimer("operation.duration"))
{
    await DoWorkAsync();
}

// Bad - manual timing (error-prone)
var sw = Stopwatch.StartNew();
try
{
    await DoWorkAsync();
}
finally
{
    sw.Stop();
    metrics.RecordTiming("operation.duration", sw.Elapsed);
}

Record errors

try
{
    await ProcessDataAsync();
}
catch (Exception ex)
{
    metrics.RecordError("data.processing", ex, tags: [("source", "api")]);
    throw; // Re-throw if needed
}

Counters

Monotonic totals are recorded by IncrementCounter and DecrementCounter. Use them for request counts, bytes processed, or occurrences.

// Increment by 1 (default)
metrics.IncrementCounter("requests.total");

// Increment by specific value
metrics.IncrementCounter("bytes.processed", 1024);

// Decrement counter
metrics.DecrementCounter("items.in_queue", 5);

// With tags
metrics.IncrementCounter("requests.total", tags: [("method", "POST"), ("endpoint", "/api/users")]);

Gauges

The last value for a name and tag set is stored by RecordGauge. Use it for cache size, queue length, or memory usage.

// Record current value
metrics.RecordGauge("cache.size", 1500);

// Update gauge value
metrics.RecordGauge("memory.usage_mb", 512.5);

// With tags
metrics.RecordGauge("queue.length", 42, tags: [("queue_name", "email_queue")]);

Histograms

A numeric sample is appended to a bounded value list by RecordHistogram. Use it for response sizes or other numeric distributions.

// Record a value
metrics.RecordHistogram("response.size_bytes", 2048);

// Record multiple values (they'll be aggregated)
metrics.RecordHistogram("response.size_bytes", 1024);
metrics.RecordHistogram("response.size_bytes", 4096);

// With tags
metrics.RecordHistogram("response.size_bytes", 2048, tags: [("endpoint", "/api/data")]);

Timings

Duration measurements are a special case of histograms. Use the Timer class for automatic timing.

// Using StartTimer (recommended)
using (metrics.StartTimer("operation.duration"))
{
    await DoWorkAsync();
}

// Manual timing
var stopwatch = Stopwatch.StartNew();
await DoWorkAsync();
stopwatch.Stop();
metrics.RecordTiming("operation.duration", stopwatch.Elapsed);

// With tags
using (metrics.StartTimer("database.query", tags: [("table", "users")]))
{
    await QueryDatabaseAsync();
}

MetricsOptions

Tune MetricsService here:

DI configuration

Registrations on IServiceCollection moved to Lyo.Metrics.DependencyInjection. They still sit in namespace Lyo.Metrics, so an existing using Lyo.Metrics; keeps working — only the ProjectReference changes. This package itself has no NuGet dependencies, which is what lets Lyo.Query.Evaluation and other low-dependency packages instrument themselves without inheriting Microsoft.Extensions.Configuration.Binder.

MetricsService (in-memory)

The default IMetrics implementation. Counters, gauges, histograms, and events live in process memory.

var metrics = new MetricsService();
// or
var metrics = new MetricsService(new MetricsOptions { ... });

OpenTelemetryMetrics

OpenTelemetry is the export path for this IMetrics implementation. Reach for it when Prometheus scrapes the process, OTLP ships elsewhere, or several processes need a shared collector.

using Lyo.Metrics.OpenTelemetry;

services.AddLyoMetricsWithOpenTelemetryFromConfiguration(builder.Configuration);
// OpenTelemetry:ServiceName / Protocol / MetricExportIntervalMs in appsettings.
// Endpoint stays empty so OTEL_EXPORTER_OTLP_ENDPOINT (JetBrains plugin) can supply it.

Behavior.

  • OpenTelemetry instruments
  • OTLP when options Endpoint or the OTEL endpoint env var is set; Console / Prometheus via configureMeterProvider

When to use.

  • Multiple instances
  • Prometheus, Grafana, a JetBrains OpenTelemetry plugin, or an OTLP collector

The Lyo.Metrics.OpenTelemetry README has the package details.

NullMetrics

Tests and optional recording use this no-op IMetrics. Construction is private; the singleton is NullMetrics.Instance. Register it through DI or take the static instance.

services.AddNullMetrics();
// or
IMetrics metrics = NullMetrics.Instance;

Behavior.

  • No recording
  • No exceptions
  • StartTimer returns default(MetricsTimer). Disposal is a no-op, so using (metrics.StartTimer(...)) allocates nothing.

When to use.

  • Unit tests
  • Optional metrics
  • Turn recording off without changing call sites

Statistics on histograms (MathExtensions)

Histogram samples become Lyo.Mathematics.Functions (StatisticsFunctions) through Lyo.Metrics.MathExtensions. The same helpers attach to HistogramData? (cached snapshots) and to MetricsService (lookup by name + tags). Missing or empty histograms yield null / empty arrays rather than exceptions.

// On a HistogramData? (e.g. from snapshot.Histograms.Values or MetricsService.GetHistogram(...))
HistogramData? h = metrics.GetHistogram("latency.ms");
var stats = h.Describe(sample: true); // DescriptiveStatisticsResult?
var quartiles = h.Quartiles(); // QuartilesResult?
var iqr = h.InterquartileRange();
var p95 = h.Percentile(0.95);
var sma = h.MovingAverage(windowSize: 30);
var ema = h.ExponentialMovingAverage(smoothingFactor: 0.2);
var rollingStd = h.RollingStandardDeviation(windowSize: 30);
var rollingMed = h.RollingMedian(windowSize: 30);
var mad = h.MedianAbsoluteDeviation();
var z = h.LatestZScore();
var anomalousZ = h.IsLatestValueAnomalous(threshold: 3d);
var anomalousMad = h.IsLatestValueAnomalousByMad(threshold: 3.5d);
var ci95 = h.MeanConfidenceInterval(confidenceLevel: 0.95);
var pearson = h.PearsonCorrelation(other); // null if either is empty

// Tag-aware lookups directly on MetricsService
var p99 = metrics.GetHistogramPercentile("latency.ms", percentile: 0.99,
                                           tags: new[] { ("endpoint", "/api/users") });
var pcts = snapshot.GetHistogramPercentiles("latency.ms", 0.5, 0.9, 0.99);
var pearr = metrics.GetHistogramPearsonCorrelation(
                "service_a.latency", "service_b.latency");

Keep tag cardinality down

Tags with huge cardinality (user IDs are the usual example) explode unique series. Keep tag values small and bounded.

// Good - low cardinality
metrics.IncrementCounter("requests.total", tags: [("method", "GET"), ("status", "200")]); // Only a few values

// Bad - high cardinality
metrics.IncrementCounter("requests.total", tags: [("user_id", userId)]); // Thousands of unique values!

Thread safety

Concurrent calls from many threads are supported; every implementation is thread-safe:

// Safe to use from multiple threads
Parallel.ForEach(items, item =>
{
    metrics.IncrementCounter("items.processed");
});

Performance

  • Sampling. Use SamplingRate < 1.0 for high-volume metrics.
  • Tag cardinality. Limit unique tag combinations.
  • Histogram size. Set MaxHistogramValues.
  • Event queue. Set MaxEventQueueSize from available memory.

Dependencies

Generated from ProjectReference / PackageReference (same model as docs/Lyo.ProjectGraph.html).

  • Lyo.Exceptions (direct, lyo)
Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  net8.0 was computed.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
.NET Core netcoreapp2.0 was computed.  netcoreapp2.1 was computed.  netcoreapp2.2 was computed.  netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.0 is compatible.  netstandard2.1 was computed. 
.NET Framework net461 was computed.  net462 was computed.  net463 was computed.  net47 was computed.  net471 was computed.  net472 was computed.  net48 was computed.  net481 was computed. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen40 was computed.  tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (50)

Showing the top 5 NuGet packages that depend on Lyo.Metrics:

Package Downloads
Lyo.Compression

A production-ready .NET compression library providing efficient, thread-safe compression for the built-in BCL algorithms (GZip, Deflate, and on net10+ Brotli, ZLib). Additional algorithms (LZ4, LZMA, Snappier, Zstd, BZip2, XZ) ship as separate Lyo.Compression.* addon packages so consumers only pay for what they use.

Lyo.Api.Client

API client library for consuming Lyo APIs.

Lyo.IO.Temp

Temp file and directory service with session tracking, logging, and metrics support.

Lyo.Cache

Cache service abstractions and local IMemoryCache implementation.

Lyo.Query

Query filtering and property comparison services for IQueryable. Uses Lyo.Query.Models for query types.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
2.0.0 1,822 9/9/2026
1.0.13 2,288 8/25/2026
1.0.11 2,533 8/23/2026
1.0.9 2,656 8/22/2026
1.0.6 3,117 8/20/2026
1.0.4 2,927 8/20/2026
1.0.3 2,538 8/19/2026
1.0.2 2,554 8/19/2026
1.0.1 2,486 8/18/2026
1.0.0 2,216 8/16/2026