Skip to content
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

Draft
wants to merge 14 commits into
base: develop
Choose a base branch
from
14 changes: 14 additions & 0 deletions Gordon360/Extensions/MiddlewareExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@

using Gordon360.Utilities.Logger;
using Microsoft.AspNetCore.Builder;

amos-cha marked this conversation as resolved.
Show resolved Hide resolved
namespace Gordon360.Extensions
{
public static class MiddlewareExtensions
{
public static IApplicationBuilder UseFactoryActivatedMiddleware(
this IApplicationBuilder app)
=> app.UseMiddleware<RequestResponseLoggerMiddleware>();
}

}
10 changes: 4 additions & 6 deletions Gordon360/Models/CCT/Context/CCTContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public CCTContext(DbContextOptions<CCTContext> options)
public virtual DbSet<Police> Police { get; set; }
public virtual DbSet<PrivType> PrivType { get; set; }
public virtual DbSet<REQUEST> REQUEST { get; set; }
public virtual DbSet<RequestResponseLog> RequestResponseLog { get; set; }
public virtual DbSet<RequestView> RequestView { get; set; }
public virtual DbSet<RoleType> RoleType { get; set; }
public virtual DbSet<RoomAssign> RoomAssign { get; set; }
Expand Down Expand Up @@ -399,8 +400,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.Property(e => e.PART_CDE).IsFixedLength();

entity.Property(e => e.SESS_CDE).IsFixedLength();

entity.Property(e => e.USER_NAME).IsFixedLength();
});

modelBuilder.Entity<MYSCHEDULE>(entity =>
Expand Down Expand Up @@ -509,9 +508,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)

modelBuilder.Entity<Participant>(entity =>
{
entity.HasKey(e => e.Username)
.HasName("PK__Particip__536C85E53B50E910");

entity.Property(e => e.ID).ValueGeneratedOnAdd();
});

Expand All @@ -525,6 +521,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasOne(d => d.ParticipantUsernameNavigation)
.WithMany(p => p.ParticipantActivity)
.HasForeignKey(d => d.ParticipantUsername)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_ParticipantActivity_Participant");

entity.HasOne(d => d.PrivType)
Expand All @@ -540,7 +537,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.WithMany(p => p.ParticipantNotification)
.HasForeignKey(d => d.ParticipantUsername)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_PartipantNotification_Participant");
.HasConstraintName("FK_ParticipantNotification_Participant");
});

modelBuilder.Entity<ParticipantStatusHistory>(entity =>
Expand All @@ -563,6 +560,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
entity.HasOne(d => d.ParticipantUsernameNavigation)
.WithMany(p => p.ParticipantTeam)
.HasForeignKey(d => d.ParticipantUsername)
.OnDelete(DeleteBehavior.ClientSetNull)
.HasConstraintName("FK_ParticipantTeam_Participant");

entity.HasOne(d => d.RoleType)
Expand Down
6 changes: 5 additions & 1 deletion Gordon360/Models/CCT/Context/efpt.CCT.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
"Name": "[dbo].[REQUEST]",
"ObjectType": 0
},
{
"Name": "[dbo].[RequestResponseLog]",
"ObjectType": 0
},
{
"Name": "[dbo].[Rooms]",
"ObjectType": 0
Expand Down Expand Up @@ -799,7 +803,7 @@
"ObjectType": 1
}
],
"UiHint": "SQLTrain1.CCT",
"UiHint": "sqltrain1.CCT.dbo",
"UseBoolPropertiesWithoutDefaultSql": false,
"UseDatabaseNames": true,
"UseDbContextSplitting": false,
Expand Down
48 changes: 48 additions & 0 deletions Gordon360/Models/CCT/dbo/RequestResponseLog.cs
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; }
Copy link
Member

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.

[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; }
}
}
12 changes: 11 additions & 1 deletion Gordon360/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
using Microsoft.OpenApi.Models;
using System.Collections.Generic;
using System;
using Gordon360.Utilities.Logger;
using Gordon360.Extensions;

var builder = WebApplication.CreateBuilder(args);

Expand Down Expand Up @@ -75,7 +77,7 @@
corsBuilder.WithOrigins(builder.Configuration.GetValue<string>("AllowedOrigin")).AllowAnyMethod().AllowAnyHeader();
}));

builder.Services.AddDbContext<CCTContext>(options =>
builder.Services.AddDbContextFactory<CCTContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("CCT"))
).AddDbContext<MyGordonContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("MyGordon"))
Expand Down Expand Up @@ -106,6 +108,12 @@
builder.Services.AddScoped<RecIM.IParticipantService, RecIM.ParticipantService>();
builder.Services.AddScoped<RecIM.ISportService, RecIM.SportService>();

/* Logging Options */
builder.Services.AddOptions<RequestResponseLoggerOptionModel>().Bind
(builder.Configuration.GetSection("RequestResponseLogger")).ValidateDataAnnotations();

builder.Services.AddTransient<RequestResponseLoggerMiddleware>();

builder.Services.AddMemoryCache();

var app = builder.Build();
Expand Down Expand Up @@ -136,4 +144,6 @@

app.MapControllers();

app.UseFactoryActivatedMiddleware();

app.Run();
118 changes: 118 additions & 0 deletions Gordon360/Utilities/Logger/LoggerMiddleware.cs
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
Copy link
Member

Choose a reason for hiding this comment

The 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>
Copy link
Member

Choose a reason for hiding this comment

The 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)
Copy link
Member

Choose a reason for hiding this comment

The 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; }
}
}