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
- Created a new ASP.NET Core 3.1 web application and choose the Empty template.
- 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" />
- 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;1314namespace OcelotApiGw15{16 public class Program17 {18 public static IWebHost BuildWebHost(string[] args)19 {20 var builder = WebHost.CreateDefaultBuilder(args);2122 // Ocelot configuration file23 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 config35 .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 });4546 var host = builder.Build();47 return host;48 }4950 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": 501010 }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": 501122 }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": 501334 }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;67namespace OcelotApiGw.Middlewares8{9 public class RequestResponseLoggingMiddleware10 {11 private readonly ILogger<RequestResponseLoggingMiddleware> _logger;12 private readonly RequestDelegate _next;1314 public RequestResponseLoggingMiddleware(RequestDelegate next, ILogger<RequestResponseLoggingMiddleware> logger)15 {16 _next = next;17 _logger = logger;18 }1920 public async Task InvokeAsync(HttpContext context)21 {22 context.Request.EnableBuffering();2324 var builder = new StringBuilder();2526 var request = await FormatRequest(context.Request);2728 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 }3435 //Copy a pointer to the original response body stream36 var originalBodyStream = context.Response.Body;3738 //Create a new memory stream...39 using var responseBody = new MemoryStream();40 //...and use that for the temporary response body41 context.Response.Body = responseBody;4243 //Continue down the Middleware pipeline, eventually returning to this class44 await _next(context);4546 //Format the response from the server47 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 }5455 //Save log to chosen datastore56 _logger.LogInformation(builder.ToString());5758 //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 }6162 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…7273 var formattedRequest = $"{request.Scheme} {request.Host}{request.Path} {request.QueryString} {body}";7475 // Reset the request body stream position so the next middleware can read it76 request.Body.Position = 0;7778 return formattedRequest;79 }8081 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);8586 //...and copy it into a string87 string text = await new StreamReader(response.Body).ReadToEndAsync();8889 //We need to reset the reader for the response so that the client can read it.90 response.Body.Seek(0, SeekOrigin.Begin);9192 //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.