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
Section titled “Table of Contents”- Installation
- How It Works
- Getting Started
- Method Signature Requirements
- Generated Code
- Error Handling
- Advanced Usage
- Troubleshooting
Installation
Section titled “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.RoslynHow It Works
Section titled “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
Section titled “Getting Started”1. Create a Partial Class
Section titled “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
Section titled “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
Section titled “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
Section titled “Method Signature Requirements”All metrics methods must follow specific signature requirements:
Required Structure
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “Generated Code”The source generator creates efficient implementations. Here’s what gets generated for a counter:
Your Declaration
Section titled “Your Declaration”[Counter<int>("user_logins", "User login attempts")]static partial void CountUserLogin(IMeter<AuthService> meter, string result, string userId);Generated Implementation
Section titled “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
Section titled “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
Section titled “Error Handling”The source generator provides compile-time validation with helpful error messages:
METRICS001: Missing First Parameter
Section titled “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
Section titled “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
Section titled “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
Section titled “Advanced Usage”Working with Scoped Metrics
Section titled “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);}
// Usagepublic async Task ProcessOrderAsync(Order order){ // Using anonymous type for cleaner syntax using var scope = _meter.BeginScope(new { 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);}You can also use property name inference with anonymous types:
public async Task ProcessOrderAsync(Order order){ // Property names are inferred from the source using var scope = _meter.BeginScope(new { order.Id, order.CustomerId, CustomerType = order.CustomerType });
OrderMetrics.CountOrderStep(scope, "processing");}Complex Tag Scenarios
Section titled “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);}
// UsageApiMetrics.CountRequestSize(_meter, request.ContentLength, "/users", "POST", 201, true);Organizing Metrics
Section titled “Organizing Metrics”Group related metrics in focused partial classes:
// Authentication metricspublic 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 metricspublic 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
Section titled “Troubleshooting”Source Generator Not Running
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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
Section titled “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.