ASP.NET Web API (.NET Framework) · Cross-Cutting

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.

Intermediate 6 min Lesson 21 of 36Covers ASP.NET Web API 2.2 on .NET Framework 4.8.
Before you start:Global Exception Handling
0/36 in ASP.NET Web API
On this page · 6 min · Intermediate

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#

C# Illustrative
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#

C#LoggingConfig.cs Illustrative
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();
    }
}
Expected output
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#

  1. LoggingConfig builds a Serilog logger that writes one file per day and keeps 30 days.
  2. SerilogExceptionLogger records every unhandled exception with the method and URL.
  3. RequestTimingFilter starts a stopwatch before each action and logs status and duration afterwards.
  4. In the controller, {OrderId} and {User} become searchable properties, not just text.
  5. 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#

Logging secrets

A debug line that logs the login request body stores every user’s password in plain text. Never log credentials or tokens.

String concatenation in log calls

"Order " + id + " placed" loses the structured OrderId field and allocates even when the level is disabled. Use templates.

Writing logs inside the web root

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?
Log.Warning("Low stock for {ProductId}: {Stock} left", id, stock);
Show solution
C# Illustrative
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#

Logging quizMultiple choice Question 1 of 3

Which log call is structured?

Score so far: 0/0

Want more? Practise all ASP.NET Web API MCQs · ASP.NET Web API interview questions

My notes#

Saved in this browser.

Progress is saved in this browser. Sign in to sync.