Migration From Ilogger
Migrating from Manual ILogger to Source Generator
Section titled “Migrating from Manual ILogger to Source Generator”This guide shows you how to migrate from traditional manual ILogger usage to the Purview Telemetry Source Generator’s structured logging generation.
Why Migrate?
Section titled “Why Migrate?”Benefits of Using the Source Generator:
- ✅ Compile-time safety - typos and parameter mismatches caught at build time
- ✅ Zero boilerplate - no manual
LoggerMessage.Defineor magic strings - ✅ Consistent structure - enforced logging patterns across your codebase
- ✅ Performance - optimized code generation with minimal allocations
- ✅ Testability - easy to mock and verify logging calls
- ✅ Message templates - automatic message generation from method signatures
- ✅ OpenTelemetry integration - built-in support for structured logging standards
Migration Examples
Section titled “Migration Examples”Example 1: Basic Logging
Section titled “Example 1: Basic Logging”Before (Manual ILogger):
using Microsoft.Extensions.Logging;
public class OrderService{ private readonly ILogger<OrderService> _logger;
public OrderService(ILogger<OrderService> logger) { _logger = logger; }
public async Task ProcessOrderAsync(int orderId, string customerName) { _logger.LogInformation("Processing order {OrderId} for customer {CustomerName}", orderId, customerName);
try { // Process order...
_logger.LogInformation("Order {OrderId} processed successfully", orderId); } catch (Exception ex) { _logger.LogError(ex, "Failed to process order {OrderId}", orderId); throw; } }}After (Source Generator):
using Purview.Telemetry;
// Define telemetry interface[Logger]public interface IOrderServiceTelemetry{ [Info] void ProcessingOrder(int orderId, string customerName);
[Info] void OrderProcessedSuccessfully(int orderId);
[Error] void FailedToProcessOrder(Exception ex, int orderId);}
// Register in DIservices.AddOrderServiceTelemetry();
// Use in your servicepublic class OrderService{ private readonly IOrderServiceTelemetry _telemetry;
public OrderService(IOrderServiceTelemetry telemetry) { _telemetry = telemetry; }
public async Task ProcessOrderAsync(int orderId, string customerName) { _telemetry.ProcessingOrder(orderId, customerName);
try { // Process order...
_telemetry.OrderProcessedSuccessfully(orderId); } catch (Exception ex) { _telemetry.FailedToProcessOrder(ex, orderId); throw; } }}Generated Message Templates:
ProcessingOrder: OrderId = {OrderId}, CustomerName = {CustomerName}OrderProcessedSuccessfully: OrderId = {OrderId}FailedToProcessOrder: OrderId = {OrderId}Example 2: Scoped Logging
Section titled “Example 2: Scoped Logging”Before (Manual ILogger with Scope):
public async Task ProcessPaymentAsync(Guid paymentId, decimal amount){ using (_logger.BeginScope("Processing payment {PaymentId} for {Amount}", paymentId, amount)) { _logger.LogDebug("Validating payment details");
// Process payment...
_logger.LogInformation("Payment completed successfully"); }}After (Source Generator - Scoped Logging):
// Define scoped logging method (returns IDisposable)[Logger]public interface IPaymentTelemetry{ [Info] IDisposable? ProcessingPayment(Guid paymentId, decimal amount);
[Debug] void ValidatingPaymentDetails();
[Info] void PaymentCompletedSuccessfully();}
// Usagepublic async Task ProcessPaymentAsync(Guid paymentId, decimal amount){ using (_telemetry.ProcessingPayment(paymentId, amount)) { _telemetry.ValidatingPaymentDetails();
// Process payment...
_telemetry.PaymentCompletedSuccessfully(); }}Example 3: LoggerMessage.Define Pattern
Section titled “Example 3: LoggerMessage.Define Pattern”Before (High-Performance Logging with LoggerMessage.Define):
public class UserService{ private readonly ILogger<UserService> _logger;
private static readonly Action<ILogger, int, string, Exception?> _userLoggedIn = LoggerMessage.Define<int, string>( LogLevel.Information, new EventId(1001, nameof(UserLoggedIn)), "User {UserId} logged in from {IpAddress}");
private static readonly Action<ILogger, string, Exception?> _invalidLoginAttempt = LoggerMessage.Define<string>( LogLevel.Warning, new EventId(1002, nameof(InvalidLoginAttempt)), "Invalid login attempt for username {Username}");
public UserService(ILogger<UserService> logger) { _logger = logger; }
public void UserLoggedIn(int userId, string ipAddress) { _userLoggedIn(_logger, userId, ipAddress, null); }
public void InvalidLoginAttempt(string username) { _invalidLoginAttempt(_logger, username, null); }}After (Source Generator):
using Purview.Telemetry;
[Logger]public interface IUserServiceTelemetry{ [Info] void UserLoggedIn(int userId, string ipAddress);
[Warning] void InvalidLoginAttempt(string username);}
// Register in DIservices.AddUserServiceTelemetry();
// Use directly - no manual LoggerMessage.Define needed!public class UserService{ private readonly IUserServiceTelemetry _telemetry;
public UserService(IUserServiceTelemetry telemetry) { _telemetry = telemetry; }
public void OnUserLogin(int userId, string ipAddress) { _telemetry.UserLoggedIn(userId, ipAddress); }
public void OnInvalidLogin(string username) { _telemetry.InvalidLoginAttempt(username); }}What Gets Generated:
The source generator creates optimized logging code similar to LoggerMessage.Define, but automatically. You get the same performance without the boilerplate!
// Generated code (simplified for clarity)private void UserLoggedIn_Logging(int userId, string ipAddress){ if (!_logger.IsEnabled(LogLevel.Information)) return;
var state = LoggerMessageHelper.ThreadLocalState; state.ReserveTagSpace(3); state.TagArray[0] = new("{OriginalFormat}", "UserLoggedIn: UserId = {UserId}, IpAddress = {IpAddress}"); state.TagArray[1] = new("userId", userId); state.TagArray[2] = new("ipAddress", ipAddress);
_logger.Log(LogLevel.Information, new EventId(...), state, null, static (s, _) => $"UserLoggedIn: UserId = {s.TagArray[1].Value}, IpAddress = {s.TagArray[2].Value}");
state.Clear();}Example 4: Exception Logging
Section titled “Example 4: Exception Logging”Before (Manual Exception Logging):
public async Task<Data> FetchDataAsync(string url){ try { return await _httpClient.GetFromJsonAsync<Data>(url); } catch (HttpRequestException ex) { _logger.LogError(ex, "HTTP request failed for URL {Url}. Status: {StatusCode}", url, ex.StatusCode); throw; } catch (Exception ex) { _logger.LogCritical(ex, "Unexpected error fetching data from {Url}", url); throw; }}After (Source Generator):
[Logger]public interface IDataServiceTelemetry{ [Error] void HttpRequestFailed(Exception ex, string url, int? statusCode);
[Critical] void UnexpectedErrorFetchingData(Exception ex, string url);}
public async Task<Data> FetchDataAsync(string url){ try { return await _httpClient.GetFromJsonAsync<Data>(url); } catch (HttpRequestException ex) { _telemetry.HttpRequestFailed(ex, url, (int?)ex.StatusCode); throw; } catch (Exception ex) { _telemetry.UnexpectedErrorFetchingData(ex, url); throw; }}Example 5: Complex Structured Data
Section titled “Example 5: Complex Structured Data”Before (Manual Structured Logging):
public void RecordOrderDetails(Order order){ _logger.LogInformation( "Order details: ID={OrderId}, Items={ItemCount}, Total={Total}, Customer={CustomerId}, Status={Status}", order.Id, order.Items.Count, order.Total, order.CustomerId, order.Status);}After (Source Generator with Enumerable Expansion):
[Logger]public interface IOrderTelemetry{ [Info] void RecordingOrderDetails( int orderId, [ExpandEnumerable(maximumValueCount: 20)] string[] itemNames, decimal total, int customerId, string status);}
public void RecordOrderDetails(Order order){ _telemetry.RecordingOrderDetails( order.Id, order.Items.Select(i => i.Name).ToArray(), order.Total, order.CustomerId, order.Status.ToString());}Generated Log Output:
RecordingOrderDetails: OrderId = 12345, ItemNames = [ "Widget", "Gadget" ], Total = 99.99, CustomerId = 789, Status = CompletedMigration Checklist
Section titled “Migration Checklist”When migrating from manual ILogger to the source generator:
- Create telemetry interface - Define an interface with
[Logger]attribute - Define methods - Add methods for each log entry with appropriate level attributes (
[Info],[Warning],[Error], etc.) - Register with DI - Call the generated
Add{InterfaceName}()extension method - Update constructors - Inject
I{YourInterface}Telemetryinstead ofILogger<T> - Replace log calls - Change
_logger.LogXxx()calls to_telemetry.MethodName() - Remove LoggerMessage.Define - Delete manual
LoggerMessage.Definedeclarations - Test - Verify log messages appear with correct structure
Common Patterns
Section titled “Common Patterns”Pattern 1: Log Level Selection
Section titled “Pattern 1: Log Level Selection”v3 Style:
_logger.LogInformation("Message");_logger.LogWarning("Warning");_logger.LogError(ex, "Error");Source Generator Style:
[Info]void Message();
[Warning]void Warning();
[Error]void Error(Exception ex);Available log level attributes: [Trace], [Debug], [Info], [Warning], [Error], [Critical], or [Log(LogLevel.Xxx)]
Pattern 2: Message Template Control
Section titled “Pattern 2: Message Template Control”By default, the generator creates a message template from the method name and parameters:
Method: void ProcessingOrder(int orderId, string status)
Generated Template: "ProcessingOrder: OrderId = {OrderId}, Status = {Status}"
You can customize this with the [Log] attribute:
[Log(LogLevel.Information, "Order #{OrderId} is now {Status}")]void ProcessingOrder(int orderId, string status);Pattern 3: Event IDs
Section titled “Pattern 3: Event IDs”The generator automatically creates stable event IDs based on method names. You can override:
[Info(EventId = 1001)]void UserLoggedIn(int userId);Pattern 4: Conditional Logging
Section titled “Pattern 4: Conditional Logging”Manual conditional logging:
if (_logger.IsEnabled(LogLevel.Debug)){ _logger.LogDebug("Expensive operation result: {Result}", ExpensiveComputation());}Source generator (automatically optimized):
[Debug]void ExpensiveOperationResult(string result);
// Call it - the generated code includes IsEnabled check_telemetry.ExpensiveOperationResult(ExpensiveComputation());The generated code automatically includes IsEnabled checks, so you don’t need to add them manually.
Next Steps
Section titled “Next Steps”After migrating to structured logging:
- Explore Generation v2 - See Logging Generation v2 for advanced features
- Add Activities - Combine logging with distributed tracing in Multi-Targeting
- Add Metrics - Track performance alongside logs with Metrics
- Configure OpenTelemetry - Export structured logs to observability platforms
Troubleshooting
Section titled “Troubleshooting”“Type or namespace ‘LoggerAttribute’ could not be found”
Section titled ““Type or namespace ‘LoggerAttribute’ could not be found””Make sure you have the correct using statement:
using Purview.Telemetry; // v4.x“No logger implementation generated”
Section titled ““No logger implementation generated””Ensure your interface:
- Has the
[Logger]attribute at the interface level - Has at least one method with a log level attribute
- Calls the DI registration method:
services.Add{InterfaceName}()
“Parameter names don’t match casing in logs”
Section titled ““Parameter names don’t match casing in logs””In v4, parameter names are converted to snake_case by default (OpenTelemetry convention). To revert to v3 behavior:
[assembly: TelemetryGeneration(NamingConvention = NamingConvention.Legacy)]See Breaking Changes - OpenTelemetry Naming for details.
See Also
Section titled “See Also”- Logging Overview - Logging generation types and options
- Logging Generation v2 - Default logging generation with message templates
- Logging Generation v1 - Legacy high-performance logging
- Multi-Targeting - Combine logging with activities and metrics
- Breaking Changes - v3 to v4 migration guide