aspnet-core | api | api-management

.NET Core API Gateway Ocelot - Logging HTTP Requests & Response Including Headers & Body

Setup Ocelot API Gateway in ASP.NET Core and configure HTTP request and response logging including headers and body.

Abhith Rajan
Abhith RajanDecember 26, 2019 · 5 min read · Last Updated:

We have many microservices running under one umbrella on production which is Azure Application Gateway and has only one endpoint for all. We are using path-based rules to route to each service.

During development, we may need to simulate the same experience, i.e one endpoint instead of we referring separate URL endpoint for each service.

For the microservices, I used to refer dotnet-architecture/eShopOnContainers. And I read about the Ocelot API gateway from there. So I set up a local API gateway using Ocelot, for that

  1. Created a new ASP.NET Core 3.1 web application and choose the Empty template.
  2. Added the following NuGet packages
1<PackageReference Include="Ocelot" Version="13.8.0" />
2<PackageReference Include="Serilog" Version="2.9.0" />
3<PackageReference Include="Serilog.AspNetCore" Version="3.2.0" />
4<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
  1. Updated the Program.cs
1using System.IO;
2using Microsoft.AspNetCore;
3using Microsoft.AspNetCore.Hosting;
4using Microsoft.Extensions.Configuration;
5using Microsoft.Extensions.Hosting;
6using Microsoft.Extensions.DependencyInjection;
7using Serilog;
8using Serilog.Events;
9using Ocelot.DependencyInjection;
10using Ocelot.Middleware;
11using Microsoft.AspNetCore.Builder;
12using OcelotApiGw.Middlewares;
13
14namespace OcelotApiGw
15{
16 public class Program
17 {
18 public static IWebHost BuildWebHost(string[] args)
19 {
20 var builder = WebHost.CreateDefaultBuilder(args);
21
22 // Ocelot configuration file
23 builder.ConfigureAppConfiguration(
24 ic => ic.AddJsonFile(Path.Combine("configuration",
25 "configuration.json")))
26 .ConfigureServices(s =>
27 {
28 s.AddSingleton(builder);
29 s.AddOcelot();
30 })
31 .UseStartup<Startup>()
32 .UseSerilog((_, config) =>
33 {
34 config
35 .MinimumLevel.Information()
36 .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
37 .Enrich.FromLogContext()
38 .WriteTo.File(@"Logs\log.txt", rollingInterval: RollingInterval.Day);
39 })
40 .Configure(app =>
41 {
42 app.UseMiddleware<RequestResponseLoggingMiddleware>();
43 app.UseOcelot().Wait();
44 });
45
46 var host = builder.Build();
47 return host;
48 }
49
50 public static void Main(string[] args)
51 {
52 BuildWebHost(args).Run();
53 }
54 }
55}

Here we are configuring the Ocelot middleware as well as Serilog for logging. The Ocelot configuration file is added under “configuration” folder in the root directory.

A sample Ocelot configuration file looks like this.

configuration.json

1{
2 "ReRoutes": [
3 {
4 "DownstreamPathTemplate": "/{version}/{everything}",
5 "DownstreamScheme": "http",
6 "DownstreamHostAndPorts": [
7 {
8 "Host": "localhost",
9 "Port": 5010
10 }
11 ],
12 "UpstreamPathTemplate": "/d/{version}/{everything}",
13 "UpstreamHttpMethod": ["POST", "PUT", "GET"]
14 },
15 {
16 "DownstreamPathTemplate": "/b/{everything}",
17 "DownstreamScheme": "http",
18 "DownstreamHostAndPorts": [
19 {
20 "Host": "localhost",
21 "Port": 5011
22 }
23 ],
24 "UpstreamPathTemplate": "/a/b/{everything}",
25 "UpstreamHttpMethod": ["POST", "PUT", "GET"]
26 },
27 {
28 "DownstreamPathTemplate": "/{everything}",
29 "DownstreamScheme": "http",
30 "DownstreamHostAndPorts": [
31 {
32 "Host": "localhost",
33 "Port": 5013
34 }
35 ],
36 "UpstreamPathTemplate": "/c/{everything}",
37 "UpstreamHttpMethod": ["POST", "PUT", "GET", "OPTIONS"]
38 }
39 ],
40 "GlobalConfiguration": {
41 "BaseUrl": "https://localhost:44390"
42 }
43}

Apart from that, we are also adding one custom middleware RequestResponseLoggingMiddleware and its purpose is clear from its name, log request and response.

The Logging Middleware

This is a slightly modified version of middleware mentioned in the article, Using Middleware in ASP.NET Core to Log Requests and Responses.

1using Microsoft.AspNetCore.Http;
2using Microsoft.Extensions.Logging;
3using System.IO;
4using System.Text;
5using System.Threading.Tasks;
6
7namespace OcelotApiGw.Middlewares
8{
9 public class RequestResponseLoggingMiddleware
10 {
11 private readonly ILogger<RequestResponseLoggingMiddleware> _logger;
12 private readonly RequestDelegate _next;
13
14 public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)
15 {
16 _next = next;
17 _logger = logger;
18 }
19
20 public async Task InvokeAsync(HttpContext context)
21 {
22 context.Request.EnableBuffering();
23
24 var builder = new StringBuilder();
25
26 var request = await FormatRequest(context.Request);
27
28 builder.Append("Request: ").AppendLine(request);
29 builder.AppendLine("Request headers:");
30 foreach (var header in context.Request.Headers)
31 {
32 builder.Append(header.Key).Append(':').AppendLine(header.Value);
33 }
34
35 //Copy a pointer to the original response body stream
36 var originalBodyStream = context.Response.Body;
37
38 //Create a new memory stream...
39 using var responseBody = new MemoryStream();
40 //...and use that for the temporary response body
41 context.Response.Body = responseBody;
42
43 //Continue down the Middleware pipeline, eventually returning to this class
44 await _next(context);
45
46 //Format the response from the server
47 var response = await FormatResponse(context.Response);
48 builder.Append("Response: ").AppendLine(response);
49 builder.AppendLine("Response headers: ");
50 foreach (var header in context.Response.Headers)
51 {
52 builder.Append(header.Key).Append(':').AppendLine(header.Value);
53 }
54
55 //Save log to chosen datastore
56 _logger.LogInformation(builder.ToString());
57
58 //Copy the contents of the new memory stream (which contains the response) to the original stream, which is then returned to the client.
59 await responseBody.CopyToAsync(originalBodyStream);
60 }
61
62 private async Task<string> FormatRequest(HttpRequest request)
63 {
64 // Leave the body open so the next middleware can read it.
65 using var reader = new StreamReader(
66 request.Body,
67 encoding: Encoding.UTF8,
68 detectEncodingFromByteOrderMarks: false,
69 leaveOpen: true);
70 var body = await reader.ReadToEndAsync();
71 // Do some processing with body…
72
73 var formattedRequest = $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {body}";
74
75 // Reset the request body stream position so the next middleware can read it
76 request.Body.Position = 0;
77
78 return formattedRequest;
79 }
80
81 private async Task<string> FormatResponse(HttpResponse response)
82 {
83 //We need to read the response stream from the beginning...
84 response.Body.Seek(0, SeekOrigin.Begin);
85
86 //...and copy it into a string
87 string text = await new StreamReader(response.Body).ReadToEndAsync();
88
89 //We need to reset the reader for the response so that the client can read it.
90 response.Body.Seek(0, SeekOrigin.Begin);
91
92 //Return the string for the response, including the status code (e.g. 200, 404, 401, etc.)
93 return $"{response.StatusCode}: {text}";
94 }
95 }
96}

Usually, the request body can be read only once. Here we are making use of the new EnableBuffering extension method to read the request body multiple times. You can read more about the feature here, Re-reading ASP.Net Core request bodies with EnableBuffering().

That’s it, run your projects, make some requests and then check the logs directory.

This logging middleware can be used in any other ASP.NET Core project.

Here we used in the Ocelot API Gateway so that we don’t need to check the logs of each microservices.

Bonus

I am using a free tool Tailviewer to go through my logs. Check it out, it is small in size also an opensource project.

This page is open source. Noticed a typo? Or something unclear?
Improve this page on GitHub


Abhith Rajan

Written byAbhith Rajan
Abhith Rajan is a software engineer by day and a full-stack developer by night. He's coding for almost a decade now. He codes 🧑‍💻, write ✍️, learn 📖 and advocate 👍.
Connect

Is this page helpful?

Related ArticlesView All

Related VideosView All

Don't Use AutoMapper in C#! Do THIS Instead!

Exploring Source Generation for Logging

HTTP Logging in ASP.NET Core 6.0

Related Tools & ServicesView All

flurl.dev

Flurl

Flurl is a modern, fluent, asynchronous, testable, portable, buzzword-laden URL builder and HTTP client library for .NET.
httpstatuses.com

HTTP Status Codes — httpstatuses.com

HTTP Status Code directory, with definitions, details and helpful code references.