Troubleshooting web application issues can be a tricky problem. In many cases, the app works as expected during development and testing, but exhibits some unexpected behavior out in the wild.
The main challenge in this case is we have to rely on user reports and try to reproduce the issues locally with minimal information to go on. When starting an investigation, I try to start by looking at the application logs to see if there are any hints as to what is going wrong.
Default Request Logging
By default, the ASP.NET 5 framework logs some basic information about each request. This includes the HTTP method (GET, PUT, POST, or DELETE) along with the path that is being requested:
Microsoft.AspNet.Hosting.Internal.HostingEngine: Information: Request starting HTTP/1.1 GET http://localhost:50861/
In addition, it logs out the request duration and status code:
Microsoft.AspNet.Hosting.Internal.HostingEngine: Information: Request finished in 0ms 200
This is a really good start and should help in identifying issues in our applications. What if we want a bit more information about each request however? Each request has a lot of interesting information associated with it – what is the best way to get at that information and make sure it is logged out so that debugging issues is easier?
Custom Request Logging via Middleware
One such way is to add our own Middleware into the request pipeline. In essence, Middleware allows us to hook into the request lifecycle as a request is coming into our application, allow it to execute, and run again as the response is being returned back to the caller.
This makes Middleware an ideal candidate for adding some extra logging to our ASP.NET 5 application. We can essentially intercept each incoming request, capture some information about it, then allow it to complete execution. After the request handling completes, we can grab some more information about the outgoing response and finally log that out.
I won’t go into much detail about the specifics of Middleware in this post. For that, I’d recommend taking a look at the official ASP.NET 5 documentation on Middleware here.
Demonstration
For demonstration purposes, let’s start off with a basic “Hello World” ASP.NET 5 app:
Startup.cs
public class Startup { public void ConfigureServices(IServiceCollection services) { } public void Configure(IApplicationBuilder app) { app.UseIISPlatformHandler(); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); } // Entry point for the application. public static void Main(string[] args) => WebApplication.Run<Startup>(args); }
project.json
The first thing I did was add in some logging so I could actually see what ASP.NET 5 was logging out for me. To do that, I had to add a couple of logging packages to my project.json:
"dependencies": { "Microsoft.AspNet.Hosting": "1.0.0-rc1-final", "Microsoft.AspNet.IISPlatformHandler": "1.0.0-rc1-final", "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final", "Microsoft.Extensions.Logging": "1.0.0-rc1-final", "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final" },
The first package Microsoft.Extensions.Logging
adds the logging framework and the second package Microsoft.Extensions.Logging.Debug
allows me to enable logging to the Debug
output window.
Adding Debug Logging
Now I can configure logging within the Configure
method:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { loggerFactory.AddDebug(); app.UseIISPlatformHandler(); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }
Now when I fire up my application and navigate to it in a browser, I see the following in my Debug
output window:
Microsoft.AspNet.Hosting.Internal.HostingEngine: Information: Request starting HTTP/1.1 GET http://localhost:50861/ ... Microsoft.AspNet.Hosting.Internal.HostingEngine: Information: Request finished in 0.0062ms 200
Custom Middleware
This is a good start – but there’s a lot more useful information I’d like to see. For that, I decided to create a RequestLoggingMiddleware
component:
public class RequestLoggingMiddleware { private readonly RequestDelegate _next; private readonly ILogger<RequestLoggingMiddleware> _logger; public RequestLoggingMiddleware(RequestDelegate next, ILogger<RequestLoggingMiddleware> logger) { _next = next; _logger = logger; } public async Task Invoke(HttpContext context) { var startTime = DateTime.UtcNow; var watch = Stopwatch.StartNew(); await _next.Invoke(context); watch.Stop(); var logTemplate = @" Client IP: {clientIP} Request path: {requestPath} Request content type: {requestContentType} Request content length: {requestContentLength} Start time: {startTime} Duration: {duration}"; _logger.LogInformation(logTemplate, context.Connection.RemoteIpAddress.ToString(), context.Request.Path, context.Request.ContentType, context.Request.ContentLength, startTime, watch.ElapsedMilliseconds); } }
It’s pretty simple. The constructor is making use of dependency injection to receive a two components. The first is the RequestDelegate that represents the next Middleware that the RequestLoggingMiddleware
should call after it is complete.
The next required component is a logger instance that my Middleware can write to. I make use of it in the following Invoke
method.
As you might have guessed, the Invoke
method is called when a request is being processed. All information about the incoming request can be accessed from the HttpContext
parameter that is passed in.
My Invoke
implementation is straight forward. I grab the start time and start a stopwatch when the method is first called. I then call await _next.Invoke(context);
to allow the rest of the Middleware pipeline to handle the request. In this case, it will just call the inline Middleware defined in the Configure
method of Startup
that prints out Hello World
.
Once the request handling is completed, I’m able to execute some more code. This is where I stop the stopwatch and actually log out some request information.
Getting Client IP Address
Edit: Thanks to David Fowler via Twitter for noting that you can easily retrieve the client IP address via the Connection
property on the HttpContext
(code sample above has been updated already).
Everything there is pretty standard – except for fetching the client’s IP address. For that, I made use of an extension method shared by Tugberk Ugurlu here. Even though the post is from an earlier revision of ASP.NET 5, it still mostly worked with minor tweaks. Here’s what my adaptation looks like:
/// <summary> /// Adapted from http://www.tugberkugurlu.com/archive/getting-the-clients-ip-address-in-asp-net-vnext-web-applications /// </summary> public static class HttpContextExtensions { public static string GetClientIPAddress(this HttpContext context) { if (null == context) { throw new ArgumentNullException(nameof(context)); } var connection = context.Features.Get<IHttpConnectionFeature>(); return connection?.RemoteIpAddress?.ToString(); } }
Register Middleware
All that is left is to tell ASP.NET 5 to use my new Middleware component. To do that, I added the following line to Configure
:
public void Configure(IApplicationBuilder app, ILoggerFactory loggerFactory) { loggerFactory.AddDebug(); app.UseIISPlatformHandler(); app.UseMiddleware<RequestLoggingMiddleware>(); app.Run(async (context) => { await context.Response.WriteAsync("Hello World!"); }); }
Test
Now when making a request to my application, I can see the following log output:
RequestLogging.RequestLoggingMiddleware: Information: Request path: / Client IP: ::1 Request content type: Request content length: Start time: 01/06/2016 05:20:58 Duration: 20
In this case the information is a bit sparse: the request has no body so no content type or length is defined. But you should get the idea nonetheless – adding a simple Middleware is a good strategy for logging out some information about each request that can be helpful in tracing and debugging requests.