Metrics
The Cratis Fundamentals library provides a comprehensive metrics system for tracking and monitoring your application's performance and behavior. This system is built on top of .NET's System.Diagnostics.Metrics and provides a developer-friendly API with source generation capabilities.
Table of Contents
Overview
The metrics system in Cratis Fundamentals offers:
- Type-safe metrics through generic interfaces
- Source generation for automatic metrics implementation
- Scoped metrics for contextual measurements
- Tag support for dimensional metrics
- Counter and Gauge instruments with standardized APIs
Getting Started
To start using the metrics system in your project:
- Add a reference to the
Cratis.Fundamentalspackage - Add the
Cratis.Metrics.Roslynsource generator package (optional but recommended) - Define your metrics interface
- Use dependency injection to get access to your metrics
// Add to your service registration
services.AddSingleton<IMeter<MyService>>();
Core Concepts
Meters
A meter is a factory for creating and managing metrics instruments. Each meter is typed to a specific class or service:
public interface IMeter<T>
{
Meter ActualMeter { get; }
}
Meter Scopes
Meter scopes provide contextual metrics with associated tags:
public interface IMeterScope<T> : IDisposable
{
Meter Meter { get; }
IDictionary<string, object> Tags { get; }
}
Use scopes to add context to your metrics:
using var scope = meter.BeginScope(new Dictionary<string, object>
{
["user_id"] = userId,
["operation"] = "login"
});
Tags
Tags allow you to add dimensions to your metrics for better filtering and analysis. Tags can be:
- Added at the scope level (applied to all metrics within the scope)
- Added at the individual metric level through method parameters
Available Metrics
Counters
Counters track cumulative values that only increase. Use for counting events, requests, errors, etc.
[Counter<int>("user_logins", "Number of user login attempts")]
static partial void LogUserLogin(IMeter<AuthService> meter, string userId, string result);
Gauges
Gauges track values that can go up and down. Use for measuring current state like memory usage, active connections, etc.
[Gauge<double>("memory_usage_bytes", "Current memory usage in bytes")]
static partial void RecordMemoryUsage(IMeter<MemoryService> meter, double value);
Using Source Generation
The Cratis.Metrics.Roslyn source generator automatically implements your metrics methods. To use it:
- Create a partial class
- Define partial methods with appropriate attributes
- The source generator will implement the methods
Method Signature Requirements
All metrics methods must follow these rules:
- First parameter must be either
IMeter<T>orIMeterScope<T> - Value parameter (for gauges) must match the generic type in the attribute
- Additional parameters become tags automatically
- Methods must be
static partial
Example Implementation
using Cratis.Metrics;
namespace MyApplication.Services;
public partial class UserMetrics
{
[Counter<int>("user_operations", "Track user operations")]
static partial void CountUserOperation(
IMeter<UserService> meter,
string operationType,
string userId,
int count = 1);
[Gauge<double>("active_sessions", "Number of active user sessions")]
static partial void RecordActiveSessions(
IMeter<UserService> meter,
double value,
string region);
// Using with scopes
[Counter<long>("scoped_operations", "Operations within a scope")]
static partial void CountScopedOperation(
IMeterScope<UserService> scope,
string operation,
long duration);
}
Examples
Basic Counter Usage
public class AuthService
{
private readonly IMeter<AuthService> _meter;
public AuthService(IMeter<AuthService> meter)
{
_meter = meter;
}
public async Task<bool> LoginAsync(string username, string password)
{
try
{
var success = await ValidateCredentialsAsync(username, password);
// Count successful logins
if (success)
{
AuthMetrics.CountLogin(_meter, "success", username);
}
else
{
AuthMetrics.CountLogin(_meter, "failure", username);
}
return success;
}
catch (Exception ex)
{
AuthMetrics.CountLogin(_meter, "error", username);
throw;
}
}
}
public partial class AuthMetrics
{
[Counter<int>("auth_attempts", "Authentication attempts by result")]
static partial void CountLogin(IMeter<AuthService> meter, string result, string username);
}
Using Scoped Metrics
public class OrderService
{
private readonly IMeter<OrderService> _meter;
public OrderService(IMeter<OrderService> meter)
{
_meter = meter;
}
public async Task ProcessOrderAsync(Order order)
{
using var scope = _meter.BeginScope(new Dictionary<string, object>
{
["customer_id"] = order.CustomerId,
["order_type"] = order.Type
});
// All metrics within this scope will include the customer_id and order_type tags
OrderMetrics.StartProcessing(scope);
try
{
await ValidateOrderAsync(order);
OrderMetrics.CountValidation(scope, "success");
await SaveOrderAsync(order);
OrderMetrics.CountSave(scope, "success");
OrderMetrics.CompleteProcessing(scope);
}
catch (ValidationException)
{
OrderMetrics.CountValidation(scope, "failure");
throw;
}
catch (Exception)
{
OrderMetrics.CountSave(scope, "failure");
throw;
}
}
}
public partial class OrderMetrics
{
[Counter<int>("order_processing_started", "Orders that started processing")]
static partial void StartProcessing(IMeterScope<OrderService> scope);
[Counter<int>("order_processing_completed", "Orders that completed processing")]
static partial void CompleteProcessing(IMeterScope<OrderService> scope);
[Counter<int>("order_validation", "Order validation attempts")]
static partial void CountValidation(IMeterScope<OrderService> scope, string result);
[Counter<int>("order_save", "Order save attempts")]
static partial void CountSave(IMeterScope<OrderService> scope, string result);
}
Gauge Metrics
public class MemoryMonitorService
{
private readonly IMeter<MemoryMonitorService> _meter;
private readonly Timer _timer;
public MemoryMonitorService(IMeter<MemoryMonitorService> meter)
{
_meter = meter;
_timer = new Timer(RecordMemoryUsage, null, TimeSpan.Zero, TimeSpan.FromSeconds(30));
}
private void RecordMemoryUsage(object? state)
{
var process = Process.GetCurrentProcess();
var workingSet = process.WorkingSet64;
var privateMemory = process.PrivateMemorySize64;
SystemMetrics.RecordWorkingSet(_meter, workingSet);
SystemMetrics.RecordPrivateMemory(_meter, privateMemory);
}
}
public partial class SystemMetrics
{
[Gauge<long>("memory_working_set", "Current working set memory in bytes")]
static partial void RecordWorkingSet(IMeter<MemoryMonitorService> meter, long bytes);
[Gauge<long>("memory_private", "Current private memory in bytes")]
static partial void RecordPrivateMemory(IMeter<MemoryMonitorService> meter, long bytes);
}
Best Practices
- Use meaningful names for your metrics that clearly describe what they measure
- Include units in metric descriptions (e.g., "bytes", "seconds", "count")
- Keep tag cardinality low to avoid performance issues
- Use scopes for contextual metrics to reduce tag duplication
- Group related metrics in the same partial class for organization
- Use appropriate metric types - counters for cumulative values, gauges for current state
See Also
- Roslyn Source Generator - Detailed guide on the source generation capabilities
- .NET Metrics Documentation - Official Microsoft documentation