Smart Logging in .NET with Serilog
Logging is more than just writing messages to a file.. it’s the nervous system of your application. It tells you what went wrong, when, and often why. In production systems, logging becomes a lifeline for diagnosing issues, tracking user behavior, and maintaining operational clarity.
But logging isn’t just about volume, it’s about control. Too little, and you’re flying blind. Too much, and you’re drowning in noise.
Enter Serilog, a structured logging library for .NET that brings clarity, flexibility, and power to your logging strategy. Unlike traditional loggers, Serilog treats logs as rich data — not just strings — making it easy to filter, query, and route logs to multiple destinations.
Serilog offers:
- Structured logging with named properties
- Flexible sinks to write logs to files, consoles, databases, cloud services, and more
- Runtime control over log levels
- Custom filtering to avoid flooding external systems
- Extensibility to integrate your own sinks — log targets
Whether you’re debugging a flaky feature or monitoring critical errors in production, in this article, we’ll explore how to achieve control over Serilog — without redeploying or restarting your app.
Utilizing the above features of Serilog, we will explore how to:
- Dynamically control Serilog’s log level at runtime using
LoggingLevelSwitch
- Expose log level control via an admin dashboard or API
- Auto-elevate logging during exceptions and revert after a timeout
- Push filtered logs to AWS SNS using a custom sink
Runtime Log Level Control
Let’s start by configuring Serilog in the application
Log.Logger = new LoggerConfiguration()
.WriteTo.Console()
.CreateLogger();
Add a level switch and register it (Dependency Injection)
var levelSwitch = new LoggingLevelSwitch(LogEventLevel.Error);
builder.Services.AddSingleton(levelSwitch);
The Serilog setup will become like:
Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.Console()
.WriteTo.File("logs/log.txt")
.CreateLogger();
Next, use this level switch in the app to control log level when needed:
levelSwitch.MinimumLevel = LogEventLevel.Information;
Exposing Log Level Control via API
Since now we have a switch, we can use or expose it via some API to be invoked directly or from some Admin dashboard.
Define a new controller, or add this to existing admin level set of APIs
[ApiController]
[Route("api/logging")]
public class LoggingController : ControllerBase
{
private readonly LoggingLevelSwitch _levelSwitch;
public LoggingController(LoggingLevelSwitch levelSwitch)
{
_levelSwitch = levelSwitch;
}
[HttpPost("set-level")]
public IActionResult SetLevel([FromQuery] string level)
{
_levelSwitch.MinimumLevel = Enum.Parse<LogEventLevel>(level, true);
return Ok($"Log level set to {level}");
}
}
Log Level Control from Admin Dashboard
The above-mentioned API can be invoked from some frontend / dashboard.
A toggle dropdown:
<select onChange={e => setLogLevel(e.target.value)}>
<option value="Error">Error</option>
<option value="Information">Information</option>
<option value="Debug">Debug</option>
</select>
API Call:
const setLogLevel = async (level) => {
await fetch(`/api/logging/set-level?level=${level}`, { method: "POST" });
};
This will give your team real-time control over logging verbosity.. without redeploys.
Auto-Elevate Logging on Exceptions
Not just via API, we can also use the log level switch silently in our app, for example in case of some fatal error.
Let’s use the switch in global exception handler of the app:
public class GlobalExceptionMiddleware
{
private readonly RequestDelegate _next;
private readonly LoggingLevelSwitch _levelSwitch;
public GlobalExceptionMiddleware(RequestDelegate next, LoggingLevelSwitch levelSwitch)
{
_next = next;
_levelSwitch = levelSwitch;
}
public async Task Invoke(HttpContext context)
{
try
{
await _next(context);
}
catch (Exception ex)
{
_levelSwitch.MinimumLevel = LogEventLevel.Information;
Log.Error(ex, "Unhandled exception occurred");
//add elevation message in logs
Log.Information("Log level elevated to Info");
_ = Task.Delay(TimeSpan.FromMinutes(5)).ContinueWith(_ =>
{
Log.Information("Log level reverted to Error");
_levelSwitch.MinimumLevel = LogEventLevel.Error;
});
context.Response.StatusCode = 500;
await context.Response.WriteAsync("Something went wrong.");
}
}
}
Note: The Log.Information(...)
must come before setting the level back to ‘Error’, or it won’t be emitted.
Don’t forget to register the middleware:
app.UseMiddleware<GlobalExceptionMiddleware>();
How can log level elevation help debugging?
We can use Log.Information(...)
at different points in our app for traceability, but the log level set to Error will not emit these messages in logs until we change the log level. Then these info messages in logs will help us finding out where things broke, particularly in background jobs.
Setting up and using a Custom Sink
Sink is any target where you want Serilog to send logs to, it can be a file, a message queue or some cloud notification service.
In our example, we will create a sink which will send logs to AWS SNS topic.
public class AwsSnsSink : ILogEventSink
{
private readonly IAmazonSimpleNotificationService _snsClient;
private readonly string _topicArn;
public AwsSnsSink(IAmazonSimpleNotificationService snsClient, string topicArn)
{
_snsClient = snsClient;
_topicArn = topicArn;
}
public void Emit(LogEvent logEvent)
{
var message = logEvent.RenderMessage();
_snsClient.PublishAsync(new PublishRequest
{
TopicArn = _topicArn,
Message = $"{logEvent.Timestamp}: {logEvent.Level} - {message}"
});
}
}
Register the sink in Program.cs
and also add to Serilog setup:
builder.Services.AddAWSService<IAmazonSimpleNotificationService>();
builder.Services.AddSingleton(levelSwitch);
var snsClient = builder.Services.BuildServiceProvider().GetRequiredService<IAmazonSimpleNotificationService>();
var topicArn = builder.Configuration["AWS:SnsTopicArn"];
Log.Logger = new LoggerConfiguration()
.MinimumLevel.ControlledBy(levelSwitch)
.WriteTo.Console()
.WriteTo.File("logs/log.txt")
.WriteTo.Sink(new AwsSnsSink(snsClient, topicArn))
.CreateLogger();
Make sure your AWS credentials and region are configured via environment variables or appsettings.json
.
Filtering to Avoid Flooding SNS
Now that we have a custom sink to receive logs, we have to be careful about the cost of that cloud service and not to flood the topic. Too much noise will also be expensive.
We can modify the Emit method of the sink class:
public void Emit(LogEvent logEvent)
{
if (logEvent.Level < LogEventLevel.Warning) return;
//continue sending
}
Or we can add the filter in Serilog setup in Program.cs
...
.WriteTo.Logger(lc => lc
.Filter.ByIncludingOnly(le => le.Level >= LogEventLevel.Warning)
WriteTo.Sink(new AwsSnsSink(snsClient, topicArn))
...
This ensures only Warning, Error, and Fatal logs reach SNS — keeping this option cost effective and cloud pipeline lean and meaningful.
Conclusion
With Serilog’s LoggingLevelSwitch
, you gain runtime control over logging verbosity. Combine that with custom sinks and smart filtering, and you’ve got a logging pipeline that’s both developer-friendly and production-safe.
Whether you’re pushing logs to AWS SNS, Slack, or a custom dashboard, this architecture gives you the flexibility to observe, debug, and scale — without drowning in log noise.