-
Notifications
You must be signed in to change notification settings - Fork 2
Migration From ILogger
This guide shows you how to migrate from traditional manual ILogger usage to the Purview Telemetry Source Generator's structured logging generation.
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
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 DI
services.AddOrderServiceTelemetry();
// Use in your service
public 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}
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();
}
// Usage
public async Task ProcessPaymentAsync(Guid paymentId, decimal amount)
{
using (_telemetry.ProcessingPayment(paymentId, amount))
{
_telemetry.ValidatingPaymentDetails();
// Process payment...
_telemetry.PaymentCompletedSuccessfully();
}
}Tip
Return IDisposable? from a logging method to create a scoped log entry. The generated code automatically measures duration and logs at both start and end.
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 DI
services.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();
}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;
}
}Note
The Exception parameter is automatically included in the log entry. You don't need to include it in the message template.
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 = Completed
Tip
Use [ExpandEnumerable] to log array/collection contents. The maximumValueCount parameter limits how many items are logged to prevent unbounded enumeration.
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
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)]
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);The generator automatically creates stable event IDs based on method names. You can override:
[Info(EventId = 1001)]
void UserLoggedIn(int userId);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.
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
Make sure you have the correct using statement:
using Purview.Telemetry; // v4.xEnsure 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}()
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.
- 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
Important
Consider helping children around the world affected by conflict. You can donate any amount to War Child here - any amount can help save a life.
Purview Telemetry Source Generator v4.0.0-prerelease.1 | Home | Getting Started | FAQ | Breaking Changes | GitHub