Metrics Roslyn Source Generator
The Cratis.Metrics.Roslyn package provides a powerful source generator that automatically implements metrics methods for you. This eliminates boilerplate code and ensures consistent, efficient metrics collection across your application.
Table of Contents
- Installation
- How It Works
- Getting Started
- Method Signature Requirements
- Generated Code
- Error Handling
- Advanced Usage
- Troubleshooting
Installation
Add the Cratis.Metrics.Roslyn NuGet package to your project:
<PackageReference Include="Cratis.Metrics.Roslyn" Version="[version]" />
Or via the .NET CLI:
dotnet add package Cratis.Metrics.Roslyn
How It Works
The source generator analyzes your code at compile time and looks for:
- Partial classes containing partial methods
- Methods decorated with
[Counter<T>]or[Gauge<T>]attributes - Proper method signatures that follow the required pattern
For each qualifying method, it generates a complete implementation that:
- Creates the appropriate metrics instrument (Counter or Gauge)
- Handles tag collection from method parameters and scopes
- Calls the underlying .NET metrics APIs
- Includes proper null checks and error handling
Getting Started
1. Create a Partial Class
Create a partial class to contain your metrics methods:
using Cratis.Metrics;
namespace MyApplication.Metrics;
public partial class UserMetrics
{
// Metrics methods will be defined here
}
2. Define Partial Methods with Attributes
Add partial method declarations with the appropriate attributes:
public partial class UserMetrics
{
[Counter<int>("user_registrations", "Number of user registrations")]
static partial void CountUserRegistration(IMeter<UserService> meter, string source, string country);
[Gauge<double>("active_sessions", "Current number of active sessions")]
static partial void RecordActiveSessions(IMeter<UserService> meter, double value, string region);
}
3. Use in Your Services
Inject the meter and use your metrics methods:
public class UserService
{
private readonly IMeter<UserService> _meter;
public UserService(IMeter<UserService> meter)
{
_meter = meter;
}
public async Task RegisterUserAsync(string email, string country, string source)
{
// ... registration logic ...
UserMetrics.CountUserRegistration(_meter, source, country);
}
}
Method Signature Requirements
All metrics methods must follow specific signature requirements:
Required Structure
[Counter<T>|Gauge<T>("name", "description")]
static partial void MethodName(
IMeter<TService> meter | IMeterScope<TService> scope, // Required first parameter
[value parameter], // Optional for counters, required for gauges
[tag parameters...] // Optional additional parameters become tags
);
First Parameter Rules
The first parameter must be either:
IMeter<T>- For basic metrics without scoped contextIMeterScope<T>- For metrics within a scope (includes scope tags automatically)
Value Parameter Rules
- Counters: Value parameter is optional. If not provided, defaults to incrementing by 1
- Gauges: Value parameter is required and must match the generic type
Tin the attribute - Type matching: The value parameter type must exactly match
Tin[Counter<T>]or[Gauge<T>]
Tag Parameters
- All parameters after the first (and value parameter if present) become metric tags
- Parameter names become tag names
- Parameter values become tag values
- Keep tag cardinality reasonable for performance
Examples
// Counter without value parameter (increments by 1)
[Counter<int>("http_requests", "HTTP requests received")]
static partial void CountHttpRequest(IMeter<WebService> meter, string method, string endpoint);
// Counter with value parameter
[Counter<long>("bytes_processed", "Bytes processed by operation")]
static partial void CountBytesProcessed(IMeter<DataService> meter, long bytes, string operation);
// Gauge (value parameter required)
[Gauge<double>("cpu_usage", "Current CPU usage percentage")]
static partial void RecordCpuUsage(IMeter<SystemService> meter, double percentage, string core);
// Using scoped meter
[Counter<int>("scoped_operations", "Operations within a scope")]
static partial void CountScopedOperation(IMeterScope<ProcessingService> scope, string operation, int duration);
Generated Code
The source generator creates efficient implementations. Here's what gets generated for a counter:
Your Declaration
[Counter<int>("user_logins", "User login attempts")]
static partial void CountUserLogin(IMeter<AuthService> meter, string result, string userId);
Generated Implementation
static Counter<int>? CountUserLoginMetric;
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Cratis.Metrics.Roslyn", "1.0.0")]
static partial void CountUserLogin(IMeter<AuthService> meter, string result, string userId)
{
if (CountUserLoginMetric is null && meter.ActualMeter is not null)
{
CountUserLoginMetric = meter.ActualMeter.CreateCounter<int>("user_logins", "User login attempts");
}
var tags = new TagList(new ReadOnlySpan<KeyValuePair<string, object?>>(new KeyValuePair<string, object?>[]
{
new("result", result),
new("userId", userId)
}));
CountUserLoginMetric?.Add(1, tags);
}
Key Features of Generated Code
- Lazy initialization - Metrics instruments are created only when first used
- Null safety - Proper null checks prevent exceptions
- Efficient tagging - Uses
TagListandReadOnlySpanfor performance - Scope integration - Automatically merges scope tags when using
IMeterScope<T> - Generated code attributes - Clearly marks generated code for debugging
Error Handling
The source generator provides compile-time validation with helpful error messages:
METRICS001: Missing First Parameter
// ❌ Error: Missing required first parameter
[Counter<int>("test", "test")]
static partial void BadMethod();
Fix: Add IMeter<T> or IMeterScope<T> as first parameter.
METRICS002: Invalid First Parameter Type
// ❌ Error: Wrong first parameter type
[Counter<int>("test", "test")]
static partial void BadMethod(string invalidParameter);
Fix: Use IMeter<T> or IMeterScope<T> as first parameter.
METRICS003: Missing Value Parameter
// ❌ Error: Gauge missing required value parameter
[Gauge<double>("test", "test")]
static partial void BadGauge(IMeter<Service> meter, string tag);
Fix: Add a parameter of type double for the gauge value.
Advanced Usage
Working with Scoped Metrics
When using IMeterScope<T>, the generator automatically includes scope tags:
public partial class OrderMetrics
{
[Counter<int>("order_steps", "Steps in order processing")]
static partial void CountOrderStep(IMeterScope<OrderService> scope, string step);
}
// Usage
public async Task ProcessOrderAsync(Order order)
{
using var scope = _meter.BeginScope(new Dictionary<string, object>
{
["order_id"] = order.Id,
["customer_type"] = order.CustomerType
});
// This will include both the scope tags (order_id, customer_type)
// and the method tag (step)
OrderMetrics.CountOrderStep(scope, "validation");
await ValidateOrder(order);
OrderMetrics.CountOrderStep(scope, "processing");
await ProcessOrder(order);
}
Complex Tag Scenarios
public partial class ApiMetrics
{
// Multiple tags with different types
[Counter<long>("api_request_size", "Size of API requests")]
static partial void CountRequestSize(
IMeter<ApiService> meter,
long bytes,
string endpoint,
string method,
int statusCode,
bool isAuthenticated);
}
// Usage
ApiMetrics.CountRequestSize(_meter, request.ContentLength, "/users", "POST", 201, true);
Organizing Metrics
Group related metrics in focused partial classes:
// Authentication metrics
public partial class AuthMetrics
{
[Counter<int>("login_attempts", "Login attempts by result")]
static partial void CountLogin(IMeter<AuthService> meter, string result);
[Counter<int>("token_validations", "Token validation attempts")]
static partial void CountTokenValidation(IMeter<AuthService> meter, string result);
}
// Database metrics
public partial class DatabaseMetrics
{
[Counter<int>("queries", "Database queries executed")]
static partial void CountQuery(IMeter<DatabaseService> meter, string operation, string table);
[Gauge<double>("connection_pool_usage", "Database connection pool usage")]
static partial void RecordConnectionPoolUsage(IMeter<DatabaseService> meter, double percentage);
}
Troubleshooting
Source Generator Not Running
If methods aren't being generated:
- Check build output for any compilation errors
- Verify package reference includes
Cratis.Metrics.Roslyn - Ensure methods are partial and in partial classes
- Check method signatures match the required pattern
Metrics Not Appearing
If metrics aren't showing up in your monitoring:
- Verify meter registration in dependency injection
- Check your metrics collection configuration
- Ensure ActivitySource is properly configured for your application
Build Errors
For compilation issues:
- Review error messages - the generator provides specific error codes
- Check parameter types match attribute generic types
- Verify namespace imports include
Cratis.Metrics
Debugging Generated Code
To see the generated code:
- Enable source generators in your IDE
- Check the Dependencies > Analyzers node in Solution Explorer
- Look for generated files under
Cratis.Metrics.Roslyn
Best Practices
- Use descriptive names for both metrics and method names
- Keep tag cardinality low to avoid performance issues
- Group related metrics in focused partial classes
- Use meaningful descriptions that explain what the metric measures
- Consider scoped metrics for contextual measurements
- Follow naming conventions for consistency across your application
Performance Considerations
The generated code is optimized for performance:
- Lazy initialization prevents unnecessary metric creation
- Efficient tagging uses
TagListandReadOnlySpan - Minimal allocations through careful memory management
- Null safety prevents exceptions without performance overhead
The source generator runs at compile time, so there's no runtime performance impact from the generation process itself.