-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Log every API Request/Response #968
base: develop
Are you sure you want to change the base?
Changes from all commits
86758ee
64d921c
371504c
8052eec
2323bef
b59712d
128309b
433bed2
834f86f
d9fd6cb
ce2d8b1
57f2632
01a336c
140a75a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
| ||
using Gordon360.Utilities.Logger; | ||
using Microsoft.AspNetCore.Builder; | ||
// See https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/extensibility?view=aspnetcore-6.0 | ||
// for background on how this works. | ||
namespace Gordon360.Extensions | ||
{ | ||
public static class MiddlewareExtensions | ||
{ | ||
public static IApplicationBuilder UseFactoryActivatedMiddleware( | ||
this IApplicationBuilder app) | ||
=> app.UseMiddleware<RequestResponseLoggerMiddleware>(); | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
// <auto-generated> This file has been auto generated by EF Core Power Tools. </auto-generated> | ||
#nullable disable | ||
using System; | ||
using System.Collections.Generic; | ||
using System.ComponentModel.DataAnnotations; | ||
using System.ComponentModel.DataAnnotations.Schema; | ||
using Microsoft.EntityFrameworkCore; | ||
|
||
namespace Gordon360.Models.CCT | ||
{ | ||
[Table("RequestResponseLog", Schema = "dbo")] | ||
public partial class RequestResponseLog | ||
{ | ||
[Key] | ||
[StringLength(36)] | ||
[Unicode(false)] | ||
public string LogID { get; set; } | ||
[Required] | ||
[StringLength(15)] | ||
[Unicode(false)] | ||
public string ClientIP { get; set; } | ||
[Column(TypeName = "datetime")] | ||
public DateTime RequestDateTime { get; set; } | ||
[Required] | ||
[StringLength(510)] | ||
[Unicode(false)] | ||
public string UserAgent { get; set; } | ||
[Required] | ||
[StringLength(63)] | ||
[Unicode(false)] | ||
public string RequestHost { get; set; } | ||
[Required] | ||
[StringLength(7)] | ||
[Unicode(false)] | ||
public string RequestMethod { get; set; } | ||
[Required] | ||
[StringLength(100)] | ||
[Unicode(false)] | ||
public string RequestPath { get; set; } | ||
[StringLength(510)] | ||
[Unicode(false)] | ||
public string RequestQuery { get; set; } | ||
[Unicode(false)] | ||
public string RequestBody { get; set; } | ||
public int ResponseStatus { get; set; } | ||
public int? ResponseContentLength { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
using Gordon360.Models.CCT; | ||
using Gordon360.Models.CCT.Context; | ||
using Microsoft.AspNetCore.Diagnostics; | ||
using Microsoft.AspNetCore.Http; | ||
using Microsoft.Data.SqlClient; | ||
using Microsoft.Extensions.Logging; | ||
using Microsoft.Extensions.Options; | ||
using Microsoft.Extensions.Primitives; | ||
using Newtonsoft.Json; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text.Json; | ||
using System.Threading.Tasks; | ||
|
||
namespace Gordon360.Utilities.Logger | ||
{ | ||
public class RequestResponseLoggerMiddleware : IMiddleware | ||
{ | ||
private readonly RequestResponseLoggerOptionModel _options; | ||
private readonly CCTContext _context; | ||
public RequestResponseLoggerMiddleware | ||
(IOptions<RequestResponseLoggerOptionModel> options, CCTContext context) | ||
{ | ||
_options = options.Value; | ||
_context = context; | ||
} | ||
|
||
/// <summary> | ||
/// Invoke Async is called on all HTTP requests and are intercepted by httpContext pipelining | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "and are" -> "and is"? Besides explaining when it's called, please summarize what it does. |
||
/// </summary> | ||
/// <param name="httpContext"></param> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just naming the argument doesn't tell us anything we can't see from the code. Explain what it's for, so we know why it's here. |
||
/// <param name="next"></param> | ||
/// <returns></returns> | ||
public async Task InvokeAsync(HttpContext httpContext, RequestDelegate next) | ||
{ | ||
RequestResponseLog log = new RequestResponseLog(); | ||
// Middleware is enabled only when the | ||
// EnableRequestResponseLogging config value is set. | ||
if (_options == null || !_options.IsEnabled) | ||
{ | ||
await next(httpContext); | ||
return; | ||
} | ||
log.RequestDateTime = DateTime.UtcNow; | ||
HttpRequest request = httpContext.Request; | ||
|
||
/*log*/ | ||
log.LogID = Guid.NewGuid().ToString(); | ||
var ip = request.HttpContext.Connection.RemoteIpAddress; | ||
log.ClientIP = ip == null ? "" : ip.ToString(); | ||
|
||
/*request*/ | ||
log.RequestMethod = request.Method; | ||
log.RequestPath = request.Path; | ||
log.RequestQuery = request.QueryString.ToString(); | ||
log.UserAgent = request.Headers.UserAgent; | ||
log.RequestBody = await ReadBodyFromRequest(request); | ||
log.RequestHost = request.Host.ToString(); | ||
|
||
// Temporarily replace the HttpResponseStream, | ||
// which is a write-only stream, with a MemoryStream to capture | ||
// its value in-flight. | ||
HttpResponse response = httpContext.Response; | ||
var originalResponseBody = response.Body; | ||
using var newResponseBody = new MemoryStream(); | ||
response.Body = newResponseBody; | ||
|
||
// Call the next middleware in the pipeline | ||
try | ||
{ | ||
await next(httpContext); | ||
} | ||
catch | ||
{ | ||
|
||
} | ||
|
||
newResponseBody.Seek(0, SeekOrigin.Begin); | ||
var responseBodyText = | ||
await new StreamReader(response.Body).ReadToEndAsync(); | ||
|
||
newResponseBody.Seek(0, SeekOrigin.Begin); | ||
await newResponseBody.CopyToAsync(originalResponseBody); | ||
|
||
log.ResponseStatus = response.StatusCode; | ||
log.ResponseContentLength = GetResponseContentLength(response.Headers); | ||
_context.RequestResponseLog.Add(log); | ||
_context.SaveChanges(); | ||
} | ||
|
||
/// <summary> | ||
/// Returns length of response content, defaults to 0 | ||
/// </summary> | ||
/// <param name="headers"></param> | ||
/// <returns></returns> | ||
private int GetResponseContentLength(IHeaderDictionary headers) | ||
{ | ||
if (headers.ContentLength is long contentLenth) | ||
return (int)contentLenth; | ||
return 0; | ||
} | ||
|
||
private async Task<string> ReadBodyFromRequest(HttpRequest request) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Needs a comment saying what it does. (The internal comments explaining constraints and how it works are good.) |
||
{ | ||
// Ensure the request's body can be read multiple times | ||
// (for the next middlewares in the pipeline). | ||
request.EnableBuffering(); | ||
using var streamReader = new StreamReader(request.Body, leaveOpen: true); | ||
var requestBody = await streamReader.ReadToEndAsync(); | ||
// Reset the request's body stream position for | ||
// next middleware in the pipeline. | ||
request.Body.Position = 0; | ||
return requestBody; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
namespace Gordon360.Utilities.Logger | ||
{ | ||
public class RequestResponseLoggerOptionModel | ||
{ | ||
public bool IsEnabled { get; set; } | ||
public string Name { get; set; } | ||
public string DateTimeFormat { get; set; } | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should be ready for IPv6 addresses, and not break when we see them. So this should probably be varchar big enough for that.