Logging
When something goes wrong in production, logs are all you have. Good logging tells you what happened, to which request, how long it took — without leaking secrets.
On this page · 6 min · Intermediate
On this page
- Learning objectives
- Explanation
- Syntax
- Code example
- Step by step
- Real-world use
- Common mistakes
- Best practices
- Remember this
- Practice exercise
- Mini quiz
- Related topics
- My notes
- Reading time
- 6 min
- Difficulty
- Intermediate
- Updated
- Oct 28, 2026
Related tutorials
Learning objectives#
- Configure Serilog with rolling log files
- Write structured log messages with properties
- Log unhandled exceptions and request timings globally
- Know what must never be logged
Explanation#
Web API 2 has no built-in logging framework. Popular choices on .NET Framework are Serilog, NLog and log4net. Serilog’s structured logging works well with tools like Seq, Elasticsearch and Azure Application Insights.
Structured logging uses named placeholders: Log.Information("Order {OrderId} placed by {UserId}", orderId, userId). The values are stored as searchable fields, not just text.
Use levels consistently: Debug (developer detail), Information (normal events), Warning (unexpected but handled), Error (failed operation), Fatal (app cannot continue).
Log globally rather than in every action: an IExceptionLogger for exceptions and an action filter (or message handler) for method, path, status and duration.
Never log passwords, tokens, full card numbers or personal documents. Avoid logging whole request bodies.
Call Log.CloseAndFlush() in Application_End so buffered entries are written when IIS recycles the app.
Syntax#
Log.Information("Order {OrderId} placed for {Amount}", order.Id, order.Total);
Log.Warning("Stock low for product {ProductId}: {Stock} left", id, stock);
Log.Error(ex, "Payment failed for order {OrderId}", order.Id);
Code example: Serilog for a Web API 2 app#
using System.Diagnostics;
using System.Web;
using System.Web.Hosting;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ExceptionHandling;
using System.Web.Http.Filters;
using Serilog;
public static class LoggingConfig
{
public static void Configure(HttpConfiguration config)
{
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.Enrich.WithProperty("App", "ShopApi")
.WriteTo.File(HostingEnvironment.MapPath("~/App_Data/logs/shopapi-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30)
.CreateLogger();
config.Services.Add(typeof(IExceptionLogger), new SerilogExceptionLogger());
config.Filters.Add(new RequestTimingFilter());
}
}
public class SerilogExceptionLogger : ExceptionLogger
{
public override void Log(ExceptionLoggerContext context)
{
// "Log" here is this method, so use the full name for Serilog's static logger
Serilog.Log.Error(context.Exception, "Unhandled exception for {Method} {Url}",
context.Request.Method, context.Request.RequestUri);
}
}
public class RequestTimingFilter : ActionFilterAttribute
{
private const string Key = "__timer";
public override void OnActionExecuting(HttpActionContext context)
{
context.Request.Properties[Key] = Stopwatch.StartNew();
}
public override void OnActionExecuted(HttpActionExecutedContext context)
{
var timer = (Stopwatch)context.Request.Properties[Key];
int status = context.Response != null ? (int)context.Response.StatusCode : 500;
Log.Information("{Method} {Path} responded {Status} in {Elapsed} ms",
context.Request.Method, context.Request.RequestUri.AbsolutePath, status, timer.ElapsedMilliseconds);
}
}
public class OrdersController : ApiController
{
[HttpPost, Route("api/orders/{id:int}/cancel")]
public IHttpActionResult Cancel(int id)
{
Log.Information("Cancelling order {OrderId} for {User}", id, User.Identity.Name);
return Ok(new { id, status = "Cancelled" });
}
}
public class WebApiApplication : HttpApplication
{
protected void Application_Start()
{
GlobalConfiguration.Configure(config =>
{
config.MapHttpAttributeRoutes();
LoggingConfig.Configure(config);
});
}
protected void Application_End()
{
Log.CloseAndFlush();
}
}
App_Data/logs/shopapi-20261028.log 2026-10-28 09:15:02.114 +05:30 [INF] Cancelling order 7 for asha 2026-10-28 09:15:02.131 +05:30 [INF] POST /api/orders/7/cancel responded 200 in 17 ms 2026-10-28 09:16:40.502 +05:30 [ERR] Unhandled exception for GET http://shop/api/products/0 System.InvalidOperationException: Connection to SQL Server failed ...
This example is illustrative. Copy or download it and run it in your own environment; the expected output is shown above.
Step-by-step explanation#
- LoggingConfig builds a Serilog logger that writes one file per day and keeps 30 days.
- SerilogExceptionLogger records every unhandled exception with the method and URL.
- RequestTimingFilter starts a stopwatch before each action and logs status and duration afterwards.
- In the controller, {OrderId} and {User} become searchable properties, not just text.
- Application_End flushes the logger when IIS shuts the app down.
Real-world use case#
An operations team ships the Serilog files to Seq. They set an alert for “Status >= 500 more than 10 times in 5 minutes” and a dashboard of the slowest endpoints by average Elapsed.
Common mistakes#
A debug line that logs the login request body stores every user’s password in plain text. Never log credentials or tokens.
"Order " + id + " placed" loses the structured OrderId field and allocates even when the level is disabled. Use templates.
Logs in a public folder can be downloaded. Write to App_Data (not served by IIS) or outside the site.
Best practices#
- Structured message templates with named properties
- Global exception logger and timing filter
- Correct levels; Information in production
- Never log personal or secret data
Remember this#
Templates, not concatenation: "Order {OrderId}".
Log globally: exception logger + timing filter.
Passwords, tokens, card numbers → never in logs.
Practice exercise#
Your task: Add a warning log to an action GET api/products/{id}/stock when stock is below 5, including ProductId and Stock as properties.
Need a hint?
Show solution
using System.Web.Http;
using Serilog;
public class StockController : ApiController
{
[HttpGet, Route("api/products/{id:int}/stock")]
public IHttpActionResult Get(int id)
{
int stock = id % 7; // pretend lookup
if (stock < 5)
Log.Warning("Low stock for {ProductId}: {Stock} left", id, stock);
return Ok(new { id, stock });
}
}
Mini quiz#
Which log call is structured?
Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions
My notes#
Saved in this browser.