Skip to content

Migration From Ilogger

Stable Telemetry SourceGenerator Reviewed 2026-09-01 purview-dev/telemetry-sourcegenerator activity c-sharp distributed-tracing dotnet events high-performance-logging instrumentation logging melt metrics nuget observability open-telemetry open-telemetry-csharp otel roslyn source-generator spans telemetry tracing

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.

Benefits of Using the Source Generator:

  • Compile-time safety - typos and parameter mismatches caught at build time
  • Zero boilerplate - no manual LoggerMessage.Define or 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();
}
}

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;
}
}

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

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}Telemetry instead of ILogger<T>
  • Replace log calls - Change _logger.LogXxx() calls to _telemetry.MethodName()
  • Remove LoggerMessage.Define - Delete manual LoggerMessage.Define declarations
  • 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:

  1. Explore Generation v2 - See Logging Generation v2 for advanced features
  2. Add Activities - Combine logging with distributed tracing in Multi-Targeting
  3. Add Metrics - Track performance alongside logs with Metrics
  4. Configure OpenTelemetry - Export structured logs to observability platforms

“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

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.