From 5617f6f73539c2cfce4ad2d07d13e344000edfbf Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 10:06:01 -0500 Subject: [PATCH 01/25] Add logging classes for HTTP transactions and exceptions Introduce new classes and extensions to facilitate logging of HTTP requests, responses, and exceptions in a web API project. - Add `ExceptionModel` in `ExceptionModel.cs` to capture exception details. - Add `HeaderDictionaryExtensions` in `HeaderDictionaryExtensions.cs` to convert `IHeaderDictionary` to `Dictionary`. - Add `HttpLog` in `HttpLog.cs` to represent a log entry for an HTTP transaction. - Add `HttpRequestLog` in `HttpRequestLog.cs` to capture HTTP request details. - Add `HttpResponseLog` in `HttpResponseLog.cs` to capture HTTP response details. --- .../Logging/Models/ExceptionModel.cs | 26 +++++++++++++ .../Models/HeaderDictionaryExtensions.cs | 18 +++++++++ .../Logging/Models/HttpLog.cs | 21 ++++++++++ .../Logging/Models/HttpRequestLog.cs | 38 +++++++++++++++++++ .../Logging/Models/HttpResponseLog.cs | 20 ++++++++++ 5 files changed, 123 insertions(+) create mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs new file mode 100644 index 000000000..c59a5ae23 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore.Http; +using System.Collections; + +namespace FluentCMS.Web.Api.Logging.Models; + +public class ExceptionModel +{ + public IDictionary? Data { get; set; } + public string HelpLink { get; set; } = string.Empty; + public int HResult { get; set; } = 0; + public string Message { get; set; } = string.Empty; + public string Source { get; set; } = string.Empty; + public string StackTrace { get; set; } = string.Empty; + + public ExceptionModel() { } + + public ExceptionModel(Exception exception) + { + Data = exception.Data; + HelpLink = exception.HelpLink ?? string.Empty; + HResult = exception.HResult; + Message = exception.Message; + Source = exception.Source ?? string.Empty; + StackTrace = exception.StackTrace ?? string.Empty; + } +} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs new file mode 100644 index 000000000..33cb7df88 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs @@ -0,0 +1,18 @@ +using Microsoft.AspNetCore.Http; + +namespace FluentCMS.Web.Api.Logging.Models; + +internal static class HeaderDictionaryExtensions +{ + public static Dictionary ToStringDictionary(this IHeaderDictionary dictionary) + { + var result = new Dictionary(); + + foreach (var (key, value) in dictionary) + { + result.Add(key, value.ToString()); + } + + return result; + } +} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs new file mode 100644 index 000000000..e49cadca0 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs @@ -0,0 +1,21 @@ +namespace FluentCMS.Web.Api.Logging.Models; + +public sealed class HttpLog : Entity +{ + public DateTime Time { get; set; } + public HttpRequestLog? Request { get; set; } + public HttpResponseLog? Response { get; set; } + public int StatusCode { get; set; } + public long Duration { get; set; } + public string AssemblyName { get; set; } = string.Empty; + public string AssemblyVersion { get; set; } = string.Empty; + public int ProcessId { get; set; } + public string ProcessName { get; set; } = string.Empty; + public int ThreadId { get; set; } + public long MemoryUsage { get; set; } + public string MachineName { get; set; } = string.Empty; + public string EnvironmentName { get; set; } = string.Empty; + public string EnvironmentUserName { get; set; } = string.Empty; + public IApiExecutionContext? Context { get; set; } + public ExceptionModel? Exception { get; set; } +} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs new file mode 100644 index 000000000..8a2bf979c --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; + +namespace FluentCMS.Web.Api.Logging.Models; + +public class HttpRequestLog +{ + public string DisplayUrl { get; set; } = string.Empty; + public string Protocol { get; set; } = string.Empty; + public string Method { get; set; } = string.Empty; + public string Scheme { get; set; } = string.Empty; + public string PathBase { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string QueryString { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public long? ContentLength { get; set; } + public string Body { get; set; } = string.Empty; + public Dictionary Headers { get; set; } = []; + + public HttpRequestLog() + { + } + + public HttpRequestLog(HttpRequest request, string requestBody) + { + DisplayUrl = request.GetDisplayUrl(); + Protocol = request.Protocol; + Method = request.Method; + Scheme = request.Scheme; + PathBase = request.PathBase; + Path = request.Path; + QueryString = request.QueryString.Value ?? string.Empty; + ContentType = request.ContentType ?? string.Empty; + ContentLength = request.ContentLength; + Headers = request.Headers.ToStringDictionary(); + Body = requestBody; + } +} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs new file mode 100644 index 000000000..5c3d6ec31 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; + +namespace FluentCMS.Web.Api.Logging.Models; + +public class HttpResponseLog +{ public string ContentType { get; set; } = string.Empty; + public long? ContentLength { get; set; } + public string Body { get; set; } = string.Empty; + public Dictionary Headers { get; set; } = []; + + public HttpResponseLog() { } + + public HttpResponseLog(HttpResponse response, string responseBody) + { + ContentType = response.ContentType ?? string.Empty; + ContentLength = response.ContentLength; + Body = responseBody; + Headers = response.Headers.ToStringDictionary(); + } +} From 3075ea1a9b3dc751ed67a2cd64dea1a911236839 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 12:26:38 -0500 Subject: [PATCH 02/25] Refactor logging and add HTTP logging middleware Refactored `ExceptionModel`, `HttpLog`, `HttpRequestLog`, `HttpResponseLog`, and `IApiExecutionContext` to new namespace `FluentCMS.Entities.Logging`. Removed `HeaderDictionaryExtensions.cs`. Added `HttpLoggingMiddleware` for logging HTTP requests and responses. Introduced `IHttpLogRepository` and `HttpLogRepository` for logging to LiteDB. --- .../IApiExecutionContext.cs | 2 +- .../Logging}/ExceptionModel.cs | 5 +- .../Logging}/HttpLog.cs | 2 +- .../Logging/HttpRequestLog.cs | 16 +++ .../Logging/HttpResponseLog.cs | 9 ++ .../Models/HeaderDictionaryExtensions.cs | 18 ---- .../Logging/Models/HttpRequestLog.cs | 38 ------- .../Logging/Models/HttpResponseLog.cs | 20 ---- .../Middleware/HttpLoggingMiddleware.cs | 101 ++++++++++++++++++ .../IHttpLogRepository.cs | 8 ++ .../HttpLogRepository.cs | 21 ++++ 11 files changed, 159 insertions(+), 81 deletions(-) rename src/Backend/{Repositories/FluentCMS.Repositories.Abstractions => FluentCMS.Entities}/IApiExecutionContext.cs (98%) rename src/Backend/{FluentCMS.Web.Api/Logging/Models => FluentCMS.Entities/Logging}/ExceptionModel.cs (87%) rename src/Backend/{FluentCMS.Web.Api/Logging/Models => FluentCMS.Entities/Logging}/HttpLog.cs (94%) create mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs create mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs delete mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs delete mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs delete mode 100644 src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs diff --git a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IApiExecutionContext.cs b/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs similarity index 98% rename from src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IApiExecutionContext.cs rename to src/Backend/FluentCMS.Entities/IApiExecutionContext.cs index fc5717759..f158332b9 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IApiExecutionContext.cs +++ b/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs @@ -1,4 +1,4 @@ -namespace FluentCMS; +namespace FluentCMS.Entities; // TODO: move this to a project which include server side shared resources which will be accessible in any other server projects diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs b/src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs similarity index 87% rename from src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs rename to src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs index c59a5ae23..73ff8c3aa 100644 --- a/src/Backend/FluentCMS.Web.Api/Logging/Models/ExceptionModel.cs +++ b/src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs @@ -1,7 +1,6 @@ -using Microsoft.AspNetCore.Http; -using System.Collections; +using System.Collections; -namespace FluentCMS.Web.Api.Logging.Models; +namespace FluentCMS.Entities.Logging; public class ExceptionModel { diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs similarity index 94% rename from src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs rename to src/Backend/FluentCMS.Entities/Logging/HttpLog.cs index e49cadca0..6e7c4ac38 100644 --- a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs @@ -1,4 +1,4 @@ -namespace FluentCMS.Web.Api.Logging.Models; +namespace FluentCMS.Entities.Logging; public sealed class HttpLog : Entity { diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs new file mode 100644 index 000000000..92ebc9da3 --- /dev/null +++ b/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs @@ -0,0 +1,16 @@ +namespace FluentCMS.Entities.Logging; + +public class HttpRequestLog +{ + public string DisplayUrl { get; set; } = string.Empty; + public string Protocol { get; set; } = string.Empty; + public string Method { get; set; } = string.Empty; + public string Scheme { get; set; } = string.Empty; + public string PathBase { get; set; } = string.Empty; + public string Path { get; set; } = string.Empty; + public string QueryString { get; set; } = string.Empty; + public string ContentType { get; set; } = string.Empty; + public long? ContentLength { get; set; } + public string Body { get; set; } = string.Empty; + public Dictionary Headers { get; set; } = []; +} diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs new file mode 100644 index 000000000..a1404cf59 --- /dev/null +++ b/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs @@ -0,0 +1,9 @@ +namespace FluentCMS.Entities.Logging; + +public class HttpResponseLog +{ + public string ContentType { get; set; } = string.Empty; + public long? ContentLength { get; set; } + public string Body { get; set; } = string.Empty; + public Dictionary Headers { get; set; } = []; +} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs deleted file mode 100644 index 33cb7df88..000000000 --- a/src/Backend/FluentCMS.Web.Api/Logging/Models/HeaderDictionaryExtensions.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace FluentCMS.Web.Api.Logging.Models; - -internal static class HeaderDictionaryExtensions -{ - public static Dictionary ToStringDictionary(this IHeaderDictionary dictionary) - { - var result = new Dictionary(); - - foreach (var (key, value) in dictionary) - { - result.Add(key, value.ToString()); - } - - return result; - } -} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs deleted file mode 100644 index 8a2bf979c..000000000 --- a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpRequestLog.cs +++ /dev/null @@ -1,38 +0,0 @@ -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; - -namespace FluentCMS.Web.Api.Logging.Models; - -public class HttpRequestLog -{ - public string DisplayUrl { get; set; } = string.Empty; - public string Protocol { get; set; } = string.Empty; - public string Method { get; set; } = string.Empty; - public string Scheme { get; set; } = string.Empty; - public string PathBase { get; set; } = string.Empty; - public string Path { get; set; } = string.Empty; - public string QueryString { get; set; } = string.Empty; - public string ContentType { get; set; } = string.Empty; - public long? ContentLength { get; set; } - public string Body { get; set; } = string.Empty; - public Dictionary Headers { get; set; } = []; - - public HttpRequestLog() - { - } - - public HttpRequestLog(HttpRequest request, string requestBody) - { - DisplayUrl = request.GetDisplayUrl(); - Protocol = request.Protocol; - Method = request.Method; - Scheme = request.Scheme; - PathBase = request.PathBase; - Path = request.Path; - QueryString = request.QueryString.Value ?? string.Empty; - ContentType = request.ContentType ?? string.Empty; - ContentLength = request.ContentLength; - Headers = request.Headers.ToStringDictionary(); - Body = requestBody; - } -} diff --git a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs b/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs deleted file mode 100644 index 5c3d6ec31..000000000 --- a/src/Backend/FluentCMS.Web.Api/Logging/Models/HttpResponseLog.cs +++ /dev/null @@ -1,20 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace FluentCMS.Web.Api.Logging.Models; - -public class HttpResponseLog -{ public string ContentType { get; set; } = string.Empty; - public long? ContentLength { get; set; } - public string Body { get; set; } = string.Empty; - public Dictionary Headers { get; set; } = []; - - public HttpResponseLog() { } - - public HttpResponseLog(HttpResponse response, string responseBody) - { - ContentType = response.ContentType ?? string.Empty; - ContentLength = response.ContentLength; - Body = responseBody; - Headers = response.Headers.ToStringDictionary(); - } -} diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs new file mode 100644 index 000000000..5fafcf33c --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -0,0 +1,101 @@ +using FluentCMS.Entities.Logging; +using FluentCMS.Repositories.Abstractions; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using System.Diagnostics; +using System.IO; +using System.Text; + +namespace FluentCMS.Web.Api.Middleware; + +internal sealed class HttpLoggingMiddleware +{ + private readonly RequestDelegate _next; + + public HttpLoggingMiddleware(RequestDelegate next) + { + _next = next; + } + + public async Task Invoke(HttpContext context, IHttpLogRepository repository, IApiExecutionContext apiContext) + { + try + { + var stopwatch = Stopwatch.StartNew(); + + var requestBody = await ReadRequestBody(context.Request); + + var originalResponseStream = context.Response.Body; + await using var responseMemoryStream = new MemoryStream(); + context.Response.Body = responseMemoryStream; + + Exception exception = null; + + try + { + await _next(context); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + + stopwatch.Stop(); + + var logModel = CreateLogModel(context, apiContext, requestBody, responseBody, stopwatch.ElapsedMilliseconds, exception != null ? 500 : null, exception); + await Log(logModel, repository); + } + } + catch (Exception ex) + { + throw; + } + } + + private async Task ReadRequestBody(HttpRequest request) + { + request.EnableBuffering(); + + using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + var requestBody = await reader.ReadToEndAsync(); + request.Body.Position = 0; + + return requestBody; + } + + private async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) + { + memoryStream.Position = 0; + using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); + var responseBody = await reader.ReadToEndAsync(); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(originalResponseStream); + context.Response.Body = originalResponseStream; + + return responseBody; + } + + private static HttpLog CreateLogModel(HttpContext context, IApiExecutionContext apiContext, string requestBody, string responseBody, long duration, int? statusCode = null, Exception exception = null) + { + exception ??= context.Features.Get()?.Error; + + return new HttpLog + { + Request = new HttpRequestLog(context.Request, requestBody), + Response = new HttpResponseLog(context.Response, responseBody), + StatusCode = statusCode ?? context.Response.StatusCode, + Duration = duration, + Context = apiContext, + Exception = exception == null ? null : new ExceptionModel(exception) + }; + } + + private static async Task Log(HttpLog log, IHttpLogRepository repository) + { + await repository.Create(log); + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs new file mode 100644 index 000000000..1be079905 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs @@ -0,0 +1,8 @@ +using FluentCMS.Entities.Logging; + +namespace FluentCMS.Repositories.Abstractions; + +public interface IHttpLogRepository +{ + Task Create(HttpLog log, CancellationToken cancellationToken = default); +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs new file mode 100644 index 000000000..c0f0147c7 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -0,0 +1,21 @@ +using FluentCMS.Entities.Logging; + +namespace FluentCMS.Repositories.LiteDb; + +public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepository +{ + protected readonly ILiteDatabaseAsync LiteDatabase = liteDbContext.Database; + protected readonly ILiteDBContext LiteDbContext = liteDbContext; + + public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + { + log.Id = Guid.NewGuid(); + + cancellationToken.ThrowIfCancellationRequested(); + + var collectionName = $"{nameof(HttpLog)}s{log.StatusCode.ToString()[..1]}"; + var collection = LiteDbContext.Database.GetCollection < HttpLog>(collectionName); + + await collection.InsertAsync(log); + } +} From 698ad63f0c1929e4dde58f9c09328ae18ec132fb Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 13:02:13 -0500 Subject: [PATCH 03/25] Refactor logging and exception handling Removed ExceptionModel and introduced HttpException. Refactored HttpLoggingMiddleware for better exception handling and logging. Updated HttpLog to use HttpException. Added HttpLogService and IHttpLogService for logging HTTP logs. Updated DI to include IHttpLogRepository service. --- .../Logging/ExceptionModel.cs | 25 ---- .../Logging/HttpException.cs | 13 ++ .../FluentCMS.Entities/Logging/HttpLog.cs | 2 +- .../FluentCMS.Services/HttpLogService.cs | 16 +++ .../Middleware/HttpLoggingMiddleware.cs | 112 ++++++++++++------ .../LiteDbServiceExtensions.cs | 1 + 6 files changed, 104 insertions(+), 65 deletions(-) delete mode 100644 src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs create mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpException.cs create mode 100644 src/Backend/FluentCMS.Services/HttpLogService.cs diff --git a/src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs b/src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs deleted file mode 100644 index 73ff8c3aa..000000000 --- a/src/Backend/FluentCMS.Entities/Logging/ExceptionModel.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System.Collections; - -namespace FluentCMS.Entities.Logging; - -public class ExceptionModel -{ - public IDictionary? Data { get; set; } - public string HelpLink { get; set; } = string.Empty; - public int HResult { get; set; } = 0; - public string Message { get; set; } = string.Empty; - public string Source { get; set; } = string.Empty; - public string StackTrace { get; set; } = string.Empty; - - public ExceptionModel() { } - - public ExceptionModel(Exception exception) - { - Data = exception.Data; - HelpLink = exception.HelpLink ?? string.Empty; - HResult = exception.HResult; - Message = exception.Message; - Source = exception.Source ?? string.Empty; - StackTrace = exception.StackTrace ?? string.Empty; - } -} diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpException.cs b/src/Backend/FluentCMS.Entities/Logging/HttpException.cs new file mode 100644 index 000000000..161fbe392 --- /dev/null +++ b/src/Backend/FluentCMS.Entities/Logging/HttpException.cs @@ -0,0 +1,13 @@ +using System.Collections; + +namespace FluentCMS.Entities.Logging; + +public class HttpException +{ + public IDictionary? Data { get; set; } + public string HelpLink { get; set; } = string.Empty; + public int HResult { get; set; } = 0; + public string Message { get; set; } = string.Empty; + public string Source { get; set; } = string.Empty; + public string StackTrace { get; set; } = string.Empty; +} diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs index 6e7c4ac38..8d5d3b8c3 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs @@ -17,5 +17,5 @@ public sealed class HttpLog : Entity public string EnvironmentName { get; set; } = string.Empty; public string EnvironmentUserName { get; set; } = string.Empty; public IApiExecutionContext? Context { get; set; } - public ExceptionModel? Exception { get; set; } + public HttpException? Exception { get; set; } } diff --git a/src/Backend/FluentCMS.Services/HttpLogService.cs b/src/Backend/FluentCMS.Services/HttpLogService.cs new file mode 100644 index 000000000..12a26ece8 --- /dev/null +++ b/src/Backend/FluentCMS.Services/HttpLogService.cs @@ -0,0 +1,16 @@ +using FluentCMS.Entities.Logging; + +namespace FluentCMS.Services; + +public interface IHttpLogService : IAutoRegisterService +{ + Task Log(HttpLog httpLog, CancellationToken cancellationToken = default); +} + +public class HttpLogService (IHttpLogRepository httpLogRepository) : IHttpLogService +{ + public async Task Log(HttpLog httpLog, CancellationToken cancellationToken = default) + { + await httpLogRepository.Create(httpLog, cancellationToken); + } +} diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 5fafcf33c..e4ddd919e 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -2,6 +2,7 @@ using FluentCMS.Repositories.Abstractions; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; using System.Diagnostics; using System.IO; using System.Text; @@ -12,51 +13,59 @@ internal sealed class HttpLoggingMiddleware { private readonly RequestDelegate _next; +#pragma warning disable IDE0290 // Use primary constructor public HttpLoggingMiddleware(RequestDelegate next) +#pragma warning restore IDE0290 // Use primary constructor { _next = next; } public async Task Invoke(HttpContext context, IHttpLogRepository repository, IApiExecutionContext apiContext) { - try - { - var stopwatch = Stopwatch.StartNew(); - - var requestBody = await ReadRequestBody(context.Request); - - var originalResponseStream = context.Response.Body; - await using var responseMemoryStream = new MemoryStream(); - context.Response.Body = responseMemoryStream; + var stopwatch = Stopwatch.StartNew(); - Exception exception = null; + var requestBody = await ReadRequestBody(context.Request); - try - { - await _next(context); - } - catch (Exception ex) - { - exception = ex; - throw; - } - finally - { - var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + var originalResponseStream = context.Response.Body; + await using var responseMemoryStream = new MemoryStream(); + context.Response.Body = responseMemoryStream; - stopwatch.Stop(); + Exception? exception = null; - var logModel = CreateLogModel(context, apiContext, requestBody, responseBody, stopwatch.ElapsedMilliseconds, exception != null ? 500 : null, exception); - await Log(logModel, repository); - } + try + { + await _next(context); } catch (Exception ex) { + exception = ex; throw; } + finally + { + stopwatch.Stop(); + + var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + + exception ??= context.Features.Get()?.Error; + + var httpLog = new HttpLog + { + Request = GetHttpRequestLog(context.Request, requestBody), + Response = GetHttpResponseLog(context.Response, responseBody), + StatusCode = context.Response.StatusCode, + Duration = stopwatch.ElapsedMilliseconds, + Context = apiContext, + Exception = GetHttpException(exception) + + }; + + await repository.Create(httpLog); + + } } - private async Task ReadRequestBody(HttpRequest request) + private static async Task ReadRequestBody(HttpRequest request) { request.EnableBuffering(); @@ -67,7 +76,7 @@ private async Task ReadRequestBody(HttpRequest request) return requestBody; } - private async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) + private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) { memoryStream.Position = 0; using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); @@ -79,23 +88,48 @@ private async Task ReadResponseBody(HttpContext context, Stream original return responseBody; } - private static HttpLog CreateLogModel(HttpContext context, IApiExecutionContext apiContext, string requestBody, string responseBody, long duration, int? statusCode = null, Exception exception = null) + private static HttpRequestLog GetHttpRequestLog(HttpRequest request, string requestBody) { - exception ??= context.Features.Get()?.Error; + return new HttpRequestLog + { + DisplayUrl = request.GetDisplayUrl(), + Protocol = request.Protocol, + Method = request.Method, + Scheme = request.Scheme, + PathBase = request.PathBase, + Path = request.Path, + QueryString = request.QueryString.Value ?? string.Empty, + ContentType = request.ContentType ?? string.Empty, + ContentLength = request.ContentLength, + Headers = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? [], + Body = requestBody + }; + } - return new HttpLog + private static HttpResponseLog GetHttpResponseLog(HttpResponse response, string responseBody) + { + return new HttpResponseLog { - Request = new HttpRequestLog(context.Request, requestBody), - Response = new HttpResponseLog(context.Response, responseBody), - StatusCode = statusCode ?? context.Response.StatusCode, - Duration = duration, - Context = apiContext, - Exception = exception == null ? null : new ExceptionModel(exception) + ContentType = response.ContentType ?? string.Empty, + ContentLength = response.ContentLength, + Body = responseBody, + Headers = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; }; } - private static async Task Log(HttpLog log, IHttpLogRepository repository) + private static HttpException? GetHttpException(Exception? exception) { - await repository.Create(log); + if (exception == null) + return null; + + return new HttpException + { + Data = exception.Data, + HelpLink = exception.HelpLink ?? string.Empty, + HResult = exception.HResult, + Message = exception.Message ?? string.Empty, + Source = exception.Source ?? string.Empty, + StackTrace = exception.StackTrace ?? string.Empty , + }; } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/LiteDbServiceExtensions.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/LiteDbServiceExtensions.cs index 534e00d6f..d7ede2440 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/LiteDbServiceExtensions.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/LiteDbServiceExtensions.cs @@ -19,6 +19,7 @@ public static IServiceCollection AddLiteDbRepositories(this IServiceCollection s services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); From 6807cb966b461ff90f0a27ef40877b2869c6c9f1 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 13:27:53 -0500 Subject: [PATCH 04/25] Refactor logging and add ApiTokenKey support Added ApiTokenKey to IApiExecutionContext and ApiExecutionContext. Refactored HttpLog class to include new properties and removed Context and Exception properties. Introduced HttpLoggingMiddleware to log HTTP requests and responses. Enhanced HttpLoggingMiddleware with additional logging details. Simplified HttpLogRepository and added it to MongoDBServiceExtensions for dependency injection. --- .../IApiExecutionContext.cs | 6 ++++ .../FluentCMS.Entities/Logging/HttpLog.cs | 14 ++++++-- .../FluentCMS.Web.Api/ApiExecutionContext.cs | 4 +++ .../Extentions/ApiServiceExtensions.cs | 1 + .../Middleware/HttpLoggingMiddleware.cs | 32 ++++++++++++++++--- .../HttpLogRepository.cs | 16 +++++++--- .../HttpLogRepository.cs | 24 ++++++++++++++ .../MongoDBServiceExtensions.cs | 1 + 8 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs diff --git a/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs b/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs index f158332b9..035cf156a 100644 --- a/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs +++ b/src/Backend/FluentCMS.Entities/IApiExecutionContext.cs @@ -62,4 +62,10 @@ public interface IApiExecutionContext /// Returns an empty string if the user is not authenticated. /// string Username { get; } + + + /// + /// Gets the API token key associated with the current request, which can be used for + /// + string ApiTokenKey { get; } } diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs index 8d5d3b8c3..2b15647db 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs @@ -2,9 +2,9 @@ public sealed class HttpLog : Entity { - public DateTime Time { get; set; } public HttpRequestLog? Request { get; set; } public HttpResponseLog? Response { get; set; } + public HttpException? Exception { get; set; } public int StatusCode { get; set; } public long Duration { get; set; } public string AssemblyName { get; set; } = string.Empty; @@ -16,6 +16,14 @@ public sealed class HttpLog : Entity public string MachineName { get; set; } = string.Empty; public string EnvironmentName { get; set; } = string.Empty; public string EnvironmentUserName { get; set; } = string.Empty; - public IApiExecutionContext? Context { get; set; } - public HttpException? Exception { get; set; } + public bool IsAuthenticated { get; set; } + public string Language { get; set; } = string.Empty; + public string SessionId { get; set; } = string.Empty; + public DateTime StartDate { get; set; } + public string TraceId { get; set; } = string.Empty; + public string UniqueId { get; set; } = string.Empty; + public Guid UserId { get; set; } + public string UserIp { get; set; } = string.Empty; + public string Username { get; set; } = string.Empty; + public string ApiTokenKey { get; set; } = string.Empty; } diff --git a/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs b/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs index 7fdf47e30..cfc98f94a 100644 --- a/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs +++ b/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs @@ -26,6 +26,7 @@ public class ApiExecutionContext : IApiExecutionContext public Guid UserId { get; } = Guid.Empty; // User ID extracted from the user's claims, default is Guid.Empty public string Username { get; } = string.Empty; // Username extracted from the user's claims, default is empty string public bool IsAuthenticated { get; } = false; // Indicates if the user is authenticated, default is false + public string ApiTokenKey { get; } = string.Empty; // API token key extracted from the request headers /// /// Constructor initializes the ApiExecutionContext with the current HTTP context information. @@ -60,6 +61,9 @@ public ApiExecutionContext(IHttpContextAccessor accessor) // Determine if the user is authenticated IsAuthenticated = user.Identity?.IsAuthenticated ?? false; + + // extract the API token key from the request headers + ApiTokenKey = context.Request?.Headers?.FirstOrDefault(_ => _.Key.Equals("X-API-AUTH", StringComparison.OrdinalIgnoreCase)).Value.ToString() ?? string.Empty; } } } diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index 0d15572d7..b9ca55954 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -105,6 +105,7 @@ public static WebApplication UseApiService(this WebApplication app) { // this will be executed only when the path starts with "/api" app.UseMiddleware(); + app.UseMiddleware(); }); app.UseAuthorization(); diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index e4ddd919e..62ca2f30e 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Http.Extensions; using System.Diagnostics; using System.IO; +using System.Reflection; using System.Text; namespace FluentCMS.Web.Api.Middleware; @@ -48,6 +49,10 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); exception ??= context.Features.Get()?.Error; + var assembly = Assembly.GetEntryAssembly(); + var assemblyName = assembly?.GetName(); + var process = Process.GetCurrentProcess(); + var thread = Thread.CurrentThread; var httpLog = new HttpLog { @@ -55,9 +60,26 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp Response = GetHttpResponseLog(context.Response, responseBody), StatusCode = context.Response.StatusCode, Duration = stopwatch.ElapsedMilliseconds, - Context = apiContext, - Exception = GetHttpException(exception) - + Exception = GetHttpException(exception), + AssemblyName = assemblyName?.Name ?? string.Empty, + AssemblyVersion = assemblyName?.Version?.ToString() ?? string.Empty, + ProcessId = process.Id, + ProcessName = process.ProcessName, + ThreadId = thread.ManagedThreadId, + MemoryUsage = process.PrivateMemorySize64, + MachineName = Environment.MachineName, + EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty, + EnvironmentUserName = Environment.UserName, + ApiTokenKey = apiContext.ApiTokenKey, + IsAuthenticated = apiContext.IsAuthenticated, + Language = apiContext.Language, + SessionId = apiContext.SessionId, + StartDate = apiContext.StartDate, + TraceId = apiContext.TraceId, + UniqueId = apiContext.UniqueId, + UserId = apiContext.UserId, + UserIp = apiContext.UserIp, + Username = apiContext.Username }; await repository.Create(httpLog); @@ -113,7 +135,7 @@ private static HttpResponseLog GetHttpResponseLog(HttpResponse response, string ContentType = response.ContentType ?? string.Empty, ContentLength = response.ContentLength, Body = responseBody, - Headers = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; + Headers = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? [] }; } @@ -129,7 +151,7 @@ private static HttpResponseLog GetHttpResponseLog(HttpResponse response, string HResult = exception.HResult, Message = exception.Message ?? string.Empty, Source = exception.Source ?? string.Empty, - StackTrace = exception.StackTrace ?? string.Empty , + StackTrace = exception.StackTrace ?? string.Empty, }; } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index c0f0147c7..d925f5287 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -9,13 +9,19 @@ public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepositor public async Task Create(HttpLog log, CancellationToken cancellationToken = default) { - log.Id = Guid.NewGuid(); - cancellationToken.ThrowIfCancellationRequested(); - var collectionName = $"{nameof(HttpLog)}s{log.StatusCode.ToString()[..1]}"; - var collection = LiteDbContext.Database.GetCollection < HttpLog>(collectionName); - + log.Id = Guid.NewGuid(); + var collection = LiteDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); + await collection.InsertAsync(log); } + + private static string GetCollectionName(int statusCode) => statusCode switch + { + < 500 and >= 400 => "HttpLog400", + < 400 and >= 300 => "HttpLog300", + >= 500 => "HttpLog500", + _ => "HttpLog200" + }; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs new file mode 100644 index 000000000..805e5623a --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs @@ -0,0 +1,24 @@ +using FluentCMS.Entities.Logging; + +namespace FluentCMS.Repositories.MongoDB; + +public class HttpLogRepository(IMongoDBContext mongoDbContext) : IHttpLogRepository +{ + public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + { + cancellationToken.ThrowIfCancellationRequested(); + + var collection = mongoDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); + var options = new InsertOneOptions { BypassDocumentValidation = false }; + + await collection.InsertOneAsync(log, options, cancellationToken); + + } + private static string GetCollectionName(int statusCode) => statusCode switch + { + < 500 and >= 400 => "HttpLog400", + < 400 and >= 300 => "HttpLog300", + >= 500 => "HttpLog500", + _ => "HttpLog200" + }; +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/MongoDBServiceExtensions.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/MongoDBServiceExtensions.cs index 0223285e0..aa3a00a1f 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/MongoDBServiceExtensions.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/MongoDBServiceExtensions.cs @@ -25,6 +25,7 @@ public static IServiceCollection AddMongoDbRepositories(this IServiceCollection services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); From 3fe77ca4130e017ffb256ca8dfb21eecae444d6d Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 18:23:17 -0500 Subject: [PATCH 05/25] Ensure HttpLogRepository initializes database before logging - Modify constructor to initialize `_liteDatabase` and `_liteDbContext` fields using `liteDbContext` parameter. - Introduce static boolean `_isInitialized` to track database initialization. - Add check in constructor to set `_isInitialized` if database has at least one collection. - Update `Create` method to check `_isInitialized` before log creation. - Rename `LiteDatabase` and `LiteDbContext` fields to `_liteDatabase` and `_liteDbContext`, and change access modifiers from `protected` to `private`. - Ensure `Create` method only inserts log if `_isInitialized` is `true`. --- .../HttpLogRepository.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index d925f5287..e24de4045 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -2,19 +2,36 @@ namespace FluentCMS.Repositories.LiteDb; -public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepository +public class HttpLogRepository: IHttpLogRepository { - protected readonly ILiteDatabaseAsync LiteDatabase = liteDbContext.Database; - protected readonly ILiteDBContext LiteDbContext = liteDbContext; + private readonly ILiteDatabaseAsync _liteDatabase; + private readonly ILiteDBContext _liteDbContext; + + private static bool _isInitialized = false; + + public HttpLogRepository(ILiteDBContext liteDbContext) + { + _liteDatabase = liteDbContext.Database; + _liteDbContext = liteDbContext; + + if (!_isInitialized) + { + // check if DB exists and has at least one collection (any collection) + _isInitialized = _liteDatabase.GetCollectionNamesAsync().GetAwaiter().GetResult().Any(); + } + } public async Task Create(HttpLog log, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); + if (_isInitialized) + { + cancellationToken.ThrowIfCancellationRequested(); - log.Id = Guid.NewGuid(); - var collection = LiteDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); + log.Id = Guid.NewGuid(); + var collection = _liteDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); - await collection.InsertAsync(log); + await collection.InsertAsync(log); + } } private static string GetCollectionName(int statusCode) => statusCode switch From c813d3cb91748984c51fb4c3c59605d027a75fe1 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 18:24:45 -0500 Subject: [PATCH 06/25] Suppress IDE0290 warning in JwtAuthorizationMiddleware Added pragma directives to disable and restore the IDE0290 warning around the constructor of the JwtAuthorizationMiddleware class. This ensures the existing constructor format is maintained without triggering the IDE warning. --- .../FluentCMS.Web.Api/Middleware/JwtAuthorizationMiddleware.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/JwtAuthorizationMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/JwtAuthorizationMiddleware.cs index e63c75020..fce6e707e 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/JwtAuthorizationMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/JwtAuthorizationMiddleware.cs @@ -17,7 +17,9 @@ public class JwtAuthorizationMiddleware /// Constructor to initialize the middleware with the next delegate in the pipeline. /// /// The next delegate/middleware in the request pipeline. +#pragma warning disable IDE0290 // Use primary constructor public JwtAuthorizationMiddleware(RequestDelegate next) +#pragma warning restore IDE0290 // Use primary constructor { _next = next; // Store the next middleware delegate } From 515add3ebebb470d987cddceb1d8d276277944cc Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 18:46:47 -0500 Subject: [PATCH 07/25] Refactor logging and repository initialization Added a new `Url` property to `HttpLog` and renamed `DisplayUrl` to `Url` in `HttpRequestLog`. Updated `HttpLoggingMiddleware` to use the new `Url` property. Modified `HttpLogRepository` to log only if initialized. Refactored `GlobalSettingsRepository` for constructor initialization and updated methods to use the new `apiExecutionContext`. --- .../FluentCMS.Entities/Logging/HttpLog.cs | 1 + .../Logging/HttpRequestLog.cs | 2 +- .../Middleware/HttpLoggingMiddleware.cs | 3 ++- .../HttpLogRepository.cs | 5 +++-- .../GlobalSettingsRepository.cs | 17 ++++------------- 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs index 2b15647db..973bae584 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs @@ -2,6 +2,7 @@ public sealed class HttpLog : Entity { + public string Url { get; set; } = string.Empty; public HttpRequestLog? Request { get; set; } public HttpResponseLog? Response { get; set; } public HttpException? Exception { get; set; } diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs index 92ebc9da3..ba5b6c184 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs @@ -2,7 +2,7 @@ public class HttpRequestLog { - public string DisplayUrl { get; set; } = string.Empty; + public string Url { get; set; } = string.Empty; public string Protocol { get; set; } = string.Empty; public string Method { get; set; } = string.Empty; public string Scheme { get; set; } = string.Empty; diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 62ca2f30e..1665ec59b 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -56,6 +56,7 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp var httpLog = new HttpLog { + Url = context.Request.GetDisplayUrl(), Request = GetHttpRequestLog(context.Request, requestBody), Response = GetHttpResponseLog(context.Response, responseBody), StatusCode = context.Response.StatusCode, @@ -114,7 +115,7 @@ private static HttpRequestLog GetHttpRequestLog(HttpRequest request, string requ { return new HttpRequestLog { - DisplayUrl = request.GetDisplayUrl(), + Url = request.GetDisplayUrl(), Protocol = request.Protocol, Method = request.Method, Scheme = request.Scheme, diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index e24de4045..a9cb52f45 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -2,7 +2,7 @@ namespace FluentCMS.Repositories.LiteDb; -public class HttpLogRepository: IHttpLogRepository +public class HttpLogRepository : IHttpLogRepository { private readonly ILiteDatabaseAsync _liteDatabase; private readonly ILiteDBContext _liteDbContext; @@ -13,7 +13,7 @@ public HttpLogRepository(ILiteDBContext liteDbContext) { _liteDatabase = liteDbContext.Database; _liteDbContext = liteDbContext; - + if (!_isInitialized) { // check if DB exists and has at least one collection (any collection) @@ -23,6 +23,7 @@ public HttpLogRepository(ILiteDBContext liteDbContext) public async Task Create(HttpLog log, CancellationToken cancellationToken = default) { + // log into DB only if it is initialized if (_isInitialized) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/GlobalSettingsRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/GlobalSettingsRepository.cs index df2d7fd23..0eccd8684 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/GlobalSettingsRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/GlobalSettingsRepository.cs @@ -1,17 +1,8 @@ namespace FluentCMS.Repositories.MongoDB; -public class GlobalSettingsRepository : IGlobalSettingsRepository +public class GlobalSettingsRepository(IMongoDBContext mongoDbContext, IApiExecutionContext apiExecutionContext) : IGlobalSettingsRepository { - private readonly IMongoCollection _collection; - private readonly IApiExecutionContext _apiExecutionContext; - private readonly IMongoDBContext _mongoDbContext; - - public GlobalSettingsRepository(IMongoDBContext mongoDbContext, IApiExecutionContext apiExecutionContext) - { - _collection = mongoDbContext.Database.GetCollection(nameof(GlobalSettings).ToLowerInvariant()); - _apiExecutionContext = apiExecutionContext; - _mongoDbContext = mongoDbContext; - } + private readonly IMongoCollection _collection = mongoDbContext.Database.GetCollection(nameof(GlobalSettings).ToLowerInvariant()); public async Task Get(CancellationToken cancellationToken = default) { @@ -47,12 +38,12 @@ private void SetAuditableFieldsForCreate(GlobalSettings settings) { settings.Id = Guid.NewGuid(); settings.CreatedAt = DateTime.UtcNow; - settings.CreatedBy = _apiExecutionContext.Username; + settings.CreatedBy = apiExecutionContext.Username; } private void SetAuditableFieldsForUpdate(GlobalSettings settings) { settings.ModifiedAt = DateTime.UtcNow; - settings.ModifiedBy = _apiExecutionContext.Username; + settings.ModifiedBy = apiExecutionContext.Username; } } From 1e95ace98d3259f7f3fa291600cbb8f2af8ceefd Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 19:25:24 -0500 Subject: [PATCH 08/25] Temporarily disable request/response body logging The `HttpLoggingMiddleware` class has been modified to temporarily disable the logging of request and response bodies. The `ReadRequestBody` and `ReadResponseBody` methods have been commented out, and their invocations within the `Invoke` method have been replaced with placeholder strings. The original response stream and memory stream handling code has also been commented out. A `TODO` comment has been added to reconsider when to log the request and response bodies. --- .../Middleware/HttpLoggingMiddleware.cs | 57 +++++++++---------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 1665ec59b..3161eae2d 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -4,9 +4,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using System.Diagnostics; -using System.IO; using System.Reflection; -using System.Text; namespace FluentCMS.Web.Api.Middleware; @@ -25,11 +23,11 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp { var stopwatch = Stopwatch.StartNew(); - var requestBody = await ReadRequestBody(context.Request); + var requestBody = string.Empty; // await ReadRequestBody(context.Request); - var originalResponseStream = context.Response.Body; - await using var responseMemoryStream = new MemoryStream(); - context.Response.Body = responseMemoryStream; + //var originalResponseStream = context.Response.Body; + //await using var responseMemoryStream = new MemoryStream(); + //context.Response.Body = responseMemoryStream; Exception? exception = null; @@ -46,7 +44,7 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp { stopwatch.Stop(); - var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + var responseBody = string.Empty; // await ReadResponseBody(context, originalResponseStream, responseMemoryStream); exception ??= context.Features.Get()?.Error; var assembly = Assembly.GetEntryAssembly(); @@ -88,28 +86,29 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp } } - private static async Task ReadRequestBody(HttpRequest request) - { - request.EnableBuffering(); - - using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); - var requestBody = await reader.ReadToEndAsync(); - request.Body.Position = 0; - - return requestBody; - } - - private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) - { - memoryStream.Position = 0; - using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); - var responseBody = await reader.ReadToEndAsync(); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(originalResponseStream); - context.Response.Body = originalResponseStream; - - return responseBody; - } + // TODO: think of when to log the request and response body + //private static async Task ReadRequestBody(HttpRequest request) + //{ + // request.EnableBuffering(); + + // using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + // var requestBody = await reader.ReadToEndAsync(); + // request.Body.Position = 0; + + // return requestBody; + //} + + //private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) + //{ + // memoryStream.Position = 0; + // using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); + // var responseBody = await reader.ReadToEndAsync(); + // memoryStream.Position = 0; + // await memoryStream.CopyToAsync(originalResponseStream); + // context.Response.Body = originalResponseStream; + + // return responseBody; + //} private static HttpRequestLog GetHttpRequestLog(HttpRequest request, string requestBody) { From bcd098192f50b8fa9a80cc58c594e071ca0baaba Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 19:27:48 -0500 Subject: [PATCH 09/25] Fix collection name typo in HttpLogRepository.cs Modified the GetCollectionName method in HttpLogRepository.cs to update collection names based on status codes. Changed "HttpLog400", "HttpLog300", "HttpLog500", and "HttpLog200" to "httplog400", "httplog300", "httplog500", and "httplog200" respectively. Corrected a typo where "httphog400" should be "httplog400". --- .../FluentCMS.Repositories.LiteDb/HttpLogRepository.cs | 8 ++++---- .../FluentCMS.Repositories.MongoDB/HttpLogRepository.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index a9cb52f45..637f1b238 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -37,9 +37,9 @@ public async Task Create(HttpLog log, CancellationToken cancellationToken = defa private static string GetCollectionName(int statusCode) => statusCode switch { - < 500 and >= 400 => "HttpLog400", - < 400 and >= 300 => "HttpLog300", - >= 500 => "HttpLog500", - _ => "HttpLog200" + < 500 and >= 400 => "httphog400", + < 400 and >= 300 => "httplog300", + >= 500 => "httplog500", + _ => "httplog200" }; } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs index 805e5623a..66c6cee2a 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs @@ -16,9 +16,9 @@ public async Task Create(HttpLog log, CancellationToken cancellationToken = defa } private static string GetCollectionName(int statusCode) => statusCode switch { - < 500 and >= 400 => "HttpLog400", - < 400 and >= 300 => "HttpLog300", - >= 500 => "HttpLog500", - _ => "HttpLog200" + < 500 and >= 400 => "httphog400", + < 400 and >= 300 => "httplog300", + >= 500 => "httplog500", + _ => "httplog200" }; } From 30992981ab8e237ccc6c056909d334cdd8d5d8ad Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Fri, 29 Nov 2024 19:28:37 -0500 Subject: [PATCH 10/25] Add _isInitialized flag to HttpLogRepository Added a static boolean field `_isInitialized` to the `HttpLogRepository` class to check if the database is initialized. This is a temporary workaround, and comments indicate uncertainty about its long-term validity. --- .../FluentCMS.Repositories.LiteDb/HttpLogRepository.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index 637f1b238..d0ffec3cb 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -7,6 +7,8 @@ public class HttpLogRepository : IHttpLogRepository private readonly ILiteDatabaseAsync _liteDatabase; private readonly ILiteDBContext _liteDbContext; + // TODO: This is a workaround to check if DB is initialized + // TODO: Not sure it is a valid approach private static bool _isInitialized = false; public HttpLogRepository(ILiteDBContext liteDbContext) From 6c2df24b6bfaee4f6cb9bd8f3584e6181a746f4a Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 17:51:00 -0500 Subject: [PATCH 11/25] Refactor HTTP logging and configuration Removed HttpException, HttpRequestLog, and HttpResponseLog classes. Refactored HttpLog class to handle request, response, and exception details directly. Updated HttpLoggingMiddleware to use IOptions for HttpLogConfig and refactored logging logic. Updated ApiServiceExtensions to bind HttpLogConfig. Added new HttpLogConfig class and updated appsettings.json for HTTP logging settings. --- .../Logging/HttpException.cs | 13 - .../FluentCMS.Entities/Logging/HttpLog.cs | 44 ++- .../Logging/HttpRequestLog.cs | 16 - .../Logging/HttpResponseLog.cs | 9 - .../Extentions/ApiServiceExtensions.cs | 3 + .../Middleware/HttpLogConfig.cs | 8 + .../Middleware/HttpLoggingMiddleware.cs | 273 +++++++++++------- src/FluentCMS/appsettings.json | 5 + 8 files changed, 222 insertions(+), 149 deletions(-) delete mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpException.cs delete mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs delete mode 100644 src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs create mode 100644 src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpException.cs b/src/Backend/FluentCMS.Entities/Logging/HttpException.cs deleted file mode 100644 index 161fbe392..000000000 --- a/src/Backend/FluentCMS.Entities/Logging/HttpException.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Collections; - -namespace FluentCMS.Entities.Logging; - -public class HttpException -{ - public IDictionary? Data { get; set; } - public string HelpLink { get; set; } = string.Empty; - public int HResult { get; set; } = 0; - public string Message { get; set; } = string.Empty; - public string Source { get; set; } = string.Empty; - public string StackTrace { get; set; } = string.Empty; -} diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs index 973bae584..0894e043b 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs @@ -1,11 +1,9 @@ -namespace FluentCMS.Entities.Logging; +using System.Collections; + +namespace FluentCMS.Entities.Logging; public sealed class HttpLog : Entity { - public string Url { get; set; } = string.Empty; - public HttpRequestLog? Request { get; set; } - public HttpResponseLog? Response { get; set; } - public HttpException? Exception { get; set; } public int StatusCode { get; set; } public long Duration { get; set; } public string AssemblyName { get; set; } = string.Empty; @@ -27,4 +25,40 @@ public sealed class HttpLog : Entity public string UserIp { get; set; } = string.Empty; public string Username { get; set; } = string.Empty; public string ApiTokenKey { get; set; } = string.Empty; + + #region Reguest + + public string ReqUrl { get; set; } = string.Empty; + public string ReqProtocol { get; set; } = string.Empty; + public string ReqMethod { get; set; } = string.Empty; + public string ReqScheme { get; set; } = string.Empty; + public string ReqPathBase { get; set; } = string.Empty; + public string ReqPath { get; set; } = string.Empty; + public string QueryString { get; set; } = string.Empty; + public string ReqContentType { get; set; } = string.Empty; + public long? ReqContentLength { get; set; } + public string? ReqBody { get; set; } + public Dictionary ReqHeaders { get; set; } = []; + + #endregion + + #region Response + + public string ResContentType { get; set; } = string.Empty; + public long? ResContentLength { get; set; } + public string? ResBody { get; set; } + public Dictionary ResHeaders { get; set; } = []; + + #endregion + + #region Exception + + public IDictionary? ExData { get; set; } + public string? ExHelpLink { get; set; } + public int? ExHResult { get; set; } + public string? ExMessage { get; set; } + public string? ExSource { get; set; } + public string? ExStackTrace { get; set; } + + #endregion } diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs deleted file mode 100644 index ba5b6c184..000000000 --- a/src/Backend/FluentCMS.Entities/Logging/HttpRequestLog.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace FluentCMS.Entities.Logging; - -public class HttpRequestLog -{ - public string Url { get; set; } = string.Empty; - public string Protocol { get; set; } = string.Empty; - public string Method { get; set; } = string.Empty; - public string Scheme { get; set; } = string.Empty; - public string PathBase { get; set; } = string.Empty; - public string Path { get; set; } = string.Empty; - public string QueryString { get; set; } = string.Empty; - public string ContentType { get; set; } = string.Empty; - public long? ContentLength { get; set; } - public string Body { get; set; } = string.Empty; - public Dictionary Headers { get; set; } = []; -} diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs b/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs deleted file mode 100644 index a1404cf59..000000000 --- a/src/Backend/FluentCMS.Entities/Logging/HttpResponseLog.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace FluentCMS.Entities.Logging; - -public class HttpResponseLog -{ - public string ContentType { get; set; } = string.Empty; - public long? ContentLength { get; set; } - public string Body { get; set; } = string.Empty; - public Dictionary Headers { get; set; } = []; -} diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index b9ca55954..e555dceda 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -18,6 +18,9 @@ public static class ApiServiceExtensions { public static IServiceCollection AddApiServices(this IServiceCollection services) { + services.AddOptions() + .BindConfiguration("HttpLogging"); + services.AddApplicationServices(); services diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs new file mode 100644 index 000000000..7177c5989 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs @@ -0,0 +1,8 @@ +namespace FluentCMS.Web.Api.Middleware; + +public class HttpLogConfig +{ + public bool Enable { get; set; } = false; + public bool EnableRequestBody { get; set; } = false; + public bool EnableResponseBody { get; set; } = false; +} diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 3161eae2d..3ec62f010 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -3,155 +3,216 @@ using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System.Diagnostics; +using System.IO; using System.Reflection; +using System.Text; namespace FluentCMS.Web.Api.Middleware; internal sealed class HttpLoggingMiddleware { private readonly RequestDelegate _next; + private readonly HttpLogConfig _httpLogConfig; + private readonly Assembly? _assembly; + private readonly Process _process; + private readonly AssemblyName? _assemblyName; #pragma warning disable IDE0290 // Use primary constructor - public HttpLoggingMiddleware(RequestDelegate next) + public HttpLoggingMiddleware(RequestDelegate next, IOptions options) #pragma warning restore IDE0290 // Use primary constructor { _next = next; + _httpLogConfig = options.Value ?? new HttpLogConfig(); + _assembly = Assembly.GetEntryAssembly(); + _process = Process.GetCurrentProcess(); + _assemblyName = _assembly?.GetName(); } - public async Task Invoke(HttpContext context, IHttpLogRepository repository, IApiExecutionContext apiContext) + public async Task Invoke(HttpContext context, IHttpLogRepository repository) { - var stopwatch = Stopwatch.StartNew(); - - var requestBody = string.Empty; // await ReadRequestBody(context.Request); - - //var originalResponseStream = context.Response.Body; - //await using var responseMemoryStream = new MemoryStream(); - //context.Response.Body = responseMemoryStream; - - Exception? exception = null; - - try + if (!_httpLogConfig.Enable) { await _next(context); + return; } - catch (Exception ex) + + var stopwatch = Stopwatch.StartNew(); + Exception? exception = null; + + if (!_httpLogConfig.EnableResponseBody) { - exception = ex; - throw; + try + { + await _next(context); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + stopwatch.Stop(); + + var httpLog = new HttpLog(); + + // Create a list of tasks to run simultaneously + var tasks = new[] + { + FillHttpLog(httpLog, context, stopwatch), + FillRequest(httpLog, context.Request), + FillResponse(httpLog, context.Response, null), + FillException(httpLog, context, exception) + }; + + // Wait for all tasks to complete + await Task.WhenAll(tasks); + await repository.Create(httpLog); + } } - finally + + if (_httpLogConfig.EnableResponseBody) { - stopwatch.Stop(); - var responseBody = string.Empty; // await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + var originalResponseStream = context.Response.Body; - exception ??= context.Features.Get()?.Error; - var assembly = Assembly.GetEntryAssembly(); - var assemblyName = assembly?.GetName(); - var process = Process.GetCurrentProcess(); - var thread = Thread.CurrentThread; + await using var responseMemoryStream = new MemoryStream(); + context.Response.Body = responseMemoryStream; - var httpLog = new HttpLog + try + { + await _next(context); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally { - Url = context.Request.GetDisplayUrl(), - Request = GetHttpRequestLog(context.Request, requestBody), - Response = GetHttpResponseLog(context.Response, responseBody), - StatusCode = context.Response.StatusCode, - Duration = stopwatch.ElapsedMilliseconds, - Exception = GetHttpException(exception), - AssemblyName = assemblyName?.Name ?? string.Empty, - AssemblyVersion = assemblyName?.Version?.ToString() ?? string.Empty, - ProcessId = process.Id, - ProcessName = process.ProcessName, - ThreadId = thread.ManagedThreadId, - MemoryUsage = process.PrivateMemorySize64, - MachineName = Environment.MachineName, - EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty, - EnvironmentUserName = Environment.UserName, - ApiTokenKey = apiContext.ApiTokenKey, - IsAuthenticated = apiContext.IsAuthenticated, - Language = apiContext.Language, - SessionId = apiContext.SessionId, - StartDate = apiContext.StartDate, - TraceId = apiContext.TraceId, - UniqueId = apiContext.UniqueId, - UserId = apiContext.UserId, - UserIp = apiContext.UserIp, - Username = apiContext.Username - }; - - await repository.Create(httpLog); + stopwatch.Stop(); + var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + + var httpLog = new HttpLog(); + + // Create a list of tasks to run simultaneously + var tasks = new[] + { + FillHttpLog(httpLog, context, stopwatch), + FillRequest(httpLog, context.Request), + FillResponse(httpLog, context.Response, responseBody), + FillException(httpLog, context, exception) + }; + + // Wait for all tasks to complete + await Task.WhenAll(tasks); + await repository.Create(httpLog); + } } } - // TODO: think of when to log the request and response body - //private static async Task ReadRequestBody(HttpRequest request) - //{ - // request.EnableBuffering(); + private async Task ReadRequestBody(HttpRequest request) + { + if (!_httpLogConfig.EnableRequestBody) + return string.Empty; + + request.EnableBuffering(); + + using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + var requestBody = await reader.ReadToEndAsync(); + request.Body.Position = 0; - // using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); - // var requestBody = await reader.ReadToEndAsync(); - // request.Body.Position = 0; + return requestBody; + } - // return requestBody; - //} + private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream? memoryStream) + { + if (memoryStream == null) + return string.Empty; - //private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream memoryStream) - //{ - // memoryStream.Position = 0; - // using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); - // var responseBody = await reader.ReadToEndAsync(); - // memoryStream.Position = 0; - // await memoryStream.CopyToAsync(originalResponseStream); - // context.Response.Body = originalResponseStream; + memoryStream.Position = 0; + using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); + var responseBody = await reader.ReadToEndAsync(); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(originalResponseStream); + context.Response.Body = originalResponseStream; - // return responseBody; - //} + return responseBody; + } - private static HttpRequestLog GetHttpRequestLog(HttpRequest request, string requestBody) + private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) { - return new HttpRequestLog - { - Url = request.GetDisplayUrl(), - Protocol = request.Protocol, - Method = request.Method, - Scheme = request.Scheme, - PathBase = request.PathBase, - Path = request.Path, - QueryString = request.QueryString.Value ?? string.Empty, - ContentType = request.ContentType ?? string.Empty, - ContentLength = request.ContentLength, - Headers = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? [], - Body = requestBody - }; + var thread = Thread.CurrentThread; + var apiContext = context.RequestServices.GetRequiredService(); + + httpLog.StatusCode = context.Response.StatusCode; + httpLog.Duration = stopwatch.ElapsedMilliseconds; + httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; + httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; + httpLog.ProcessId = _process.Id; + httpLog.ProcessName = _process.ProcessName; + httpLog.ThreadId = thread.ManagedThreadId; + httpLog.MemoryUsage = _process.PrivateMemorySize64; + httpLog.MachineName = Environment.MachineName; + httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; + httpLog.EnvironmentUserName = Environment.UserName; + httpLog.ApiTokenKey = apiContext.ApiTokenKey; + httpLog.IsAuthenticated = apiContext.IsAuthenticated; + httpLog.Language = apiContext.Language; + httpLog.SessionId = apiContext.SessionId; + httpLog.StartDate = apiContext.StartDate; + httpLog.TraceId = apiContext.TraceId; + httpLog.UniqueId = apiContext.UniqueId; + httpLog.UserId = apiContext.UserId; + httpLog.UserIp = apiContext.UserIp; + httpLog.Username = apiContext.Username; + + await Task.CompletedTask; } - private static HttpResponseLog GetHttpResponseLog(HttpResponse response, string responseBody) + private async Task FillRequest(HttpLog httpLog, HttpRequest request) { - return new HttpResponseLog - { - ContentType = response.ContentType ?? string.Empty, - ContentLength = response.ContentLength, - Body = responseBody, - Headers = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? [] - }; + var requestBody = _httpLogConfig.EnableRequestBody ? await ReadRequestBody(request) : null; + + httpLog.ReqUrl = request.GetDisplayUrl(); + httpLog.ReqProtocol = request.Protocol; + httpLog.ReqMethod = request.Method; + httpLog.ReqScheme = request.Scheme; + httpLog.ReqPathBase = request.PathBase; + httpLog.ReqPath = request.Path; + httpLog.QueryString = request.QueryString.Value ?? string.Empty; + httpLog.ReqContentType = request.ContentType ?? string.Empty; + httpLog.ReqContentLength = request.ContentLength; + httpLog.ReqBody = requestBody; + httpLog.ReqHeaders = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; } - private static HttpException? GetHttpException(Exception? exception) + private static async Task FillResponse(HttpLog httpLog, HttpResponse response, string? responseBody) + { + httpLog.ResContentType = response.ContentType ?? string.Empty; + httpLog.ResContentLength = response.ContentLength; + httpLog.ResBody = responseBody; + httpLog.ResHeaders = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; + await Task.CompletedTask; + } + private static async Task FillException(HttpLog httpLog, HttpContext context, Exception? exception) { + exception ??= context.Features.Get()?.Error; + if (exception == null) - return null; + return; - return new HttpException - { - Data = exception.Data, - HelpLink = exception.HelpLink ?? string.Empty, - HResult = exception.HResult, - Message = exception.Message ?? string.Empty, - Source = exception.Source ?? string.Empty, - StackTrace = exception.StackTrace ?? string.Empty, - }; + httpLog.ExData = exception.Data; + httpLog.ExHelpLink = exception.HelpLink; + httpLog.ExHResult = exception.HResult; + httpLog.ExMessage = exception.Message; + httpLog.ExSource = exception.Source; + httpLog.ExStackTrace = exception.StackTrace; + + await Task.CompletedTask; } } diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index cbc929209..ca18ec7ba 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -35,5 +35,10 @@ "Secret": "YOUR_SECRET_KEY_SHOULD_BE_HERE!" } } + }, + "HttpLogging": { + "Enable": true, + "EnableRequestBody": false, + "EnableResponseBody": false } } From 77ac97909c6589a82c0cbe4e97afd309cc15ebef Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 18:23:24 -0500 Subject: [PATCH 12/25] Refactor HttpLog namespace and clean up directives Moved HttpLog class to FluentCMS.Entities namespace. Updated HttpLogService, HttpLoggingMiddleware, IHttpLogRepository, and HttpLogRepository files to remove obsolete using directives. Refactored HttpLoggingMiddleware to use local process variable. Added using System.Collections directive to HttpLog.cs. --- .../FluentCMS.Entities/{Logging => }/HttpLog.cs | 2 +- src/Backend/FluentCMS.Services/HttpLogService.cs | 4 +--- .../Middleware/HttpLoggingMiddleware.cs | 12 +++++------- .../IHttpLogRepository.cs | 4 +--- .../HttpLogRepository.cs | 4 +--- .../HttpLogRepository.cs | 4 +--- 6 files changed, 10 insertions(+), 20 deletions(-) rename src/Backend/FluentCMS.Entities/{Logging => }/HttpLog.cs (98%) diff --git a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs b/src/Backend/FluentCMS.Entities/HttpLog.cs similarity index 98% rename from src/Backend/FluentCMS.Entities/Logging/HttpLog.cs rename to src/Backend/FluentCMS.Entities/HttpLog.cs index 0894e043b..26c5aa8a3 100644 --- a/src/Backend/FluentCMS.Entities/Logging/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/HttpLog.cs @@ -1,6 +1,6 @@ using System.Collections; -namespace FluentCMS.Entities.Logging; +namespace FluentCMS.Entities; public sealed class HttpLog : Entity { diff --git a/src/Backend/FluentCMS.Services/HttpLogService.cs b/src/Backend/FluentCMS.Services/HttpLogService.cs index 12a26ece8..f89ea85d6 100644 --- a/src/Backend/FluentCMS.Services/HttpLogService.cs +++ b/src/Backend/FluentCMS.Services/HttpLogService.cs @@ -1,6 +1,4 @@ -using FluentCMS.Entities.Logging; - -namespace FluentCMS.Services; +namespace FluentCMS.Services; public interface IHttpLogService : IAutoRegisterService { diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 3ec62f010..9b6dd5232 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -1,5 +1,4 @@ -using FluentCMS.Entities.Logging; -using FluentCMS.Repositories.Abstractions; +using FluentCMS.Repositories.Abstractions; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; @@ -17,7 +16,6 @@ internal sealed class HttpLoggingMiddleware private readonly RequestDelegate _next; private readonly HttpLogConfig _httpLogConfig; private readonly Assembly? _assembly; - private readonly Process _process; private readonly AssemblyName? _assemblyName; #pragma warning disable IDE0290 // Use primary constructor @@ -27,7 +25,6 @@ public HttpLoggingMiddleware(RequestDelegate next, IOptions optio _next = next; _httpLogConfig = options.Value ?? new HttpLogConfig(); _assembly = Assembly.GetEntryAssembly(); - _process = Process.GetCurrentProcess(); _assemblyName = _assembly?.GetName(); } @@ -147,16 +144,17 @@ private static async Task ReadResponseBody(HttpContext context, Stream o private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) { var thread = Thread.CurrentThread; + var process = Process.GetCurrentProcess(); var apiContext = context.RequestServices.GetRequiredService(); httpLog.StatusCode = context.Response.StatusCode; httpLog.Duration = stopwatch.ElapsedMilliseconds; httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; - httpLog.ProcessId = _process.Id; - httpLog.ProcessName = _process.ProcessName; + httpLog.ProcessId = process.Id; + httpLog.ProcessName = process.ProcessName; httpLog.ThreadId = thread.ManagedThreadId; - httpLog.MemoryUsage = _process.PrivateMemorySize64; + httpLog.MemoryUsage = process.PrivateMemorySize64; httpLog.MachineName = Environment.MachineName; httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; httpLog.EnvironmentUserName = Environment.UserName; diff --git a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs index 1be079905..453e93f94 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs @@ -1,6 +1,4 @@ -using FluentCMS.Entities.Logging; - -namespace FluentCMS.Repositories.Abstractions; +namespace FluentCMS.Repositories.Abstractions; public interface IHttpLogRepository { diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index d0ffec3cb..c94617a44 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -1,6 +1,4 @@ -using FluentCMS.Entities.Logging; - -namespace FluentCMS.Repositories.LiteDb; +namespace FluentCMS.Repositories.LiteDb; public class HttpLogRepository : IHttpLogRepository { diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs index 66c6cee2a..6a490e1ca 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs @@ -1,6 +1,4 @@ -using FluentCMS.Entities.Logging; - -namespace FluentCMS.Repositories.MongoDB; +namespace FluentCMS.Repositories.MongoDB; public class HttpLogRepository(IMongoDBContext mongoDbContext) : IHttpLogRepository { From 1741d7fd7c517331dad28ca5f8544d1efba3f5cc Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 18:38:49 -0500 Subject: [PATCH 13/25] Add detailed logging documentation and expand HttpLog class Updated README.md to include sections for "Documentation" and "Logging" with a link to the new logging documentation. Expanded HttpLog.cs with XML documentation comments for each property, capturing extensive HTTP request, response, and exception details. Added Logging.md to provide comprehensive documentation on HTTP logging, including middleware overview, configuration options, and usage examples. --- docs/Logging.md | 127 ++++++++++++++++ docs/README.md | 9 +- src/Backend/FluentCMS.Entities/HttpLog.cs | 173 +++++++++++++++++++++- 3 files changed, 304 insertions(+), 5 deletions(-) create mode 100644 docs/Logging.md diff --git a/docs/Logging.md b/docs/Logging.md new file mode 100644 index 000000000..3cdb91f6d --- /dev/null +++ b/docs/Logging.md @@ -0,0 +1,127 @@ +# Logging + +## HTTP Logging for API + +The HTTP Logging Middleware `HttpLoggingMiddleware.cs` is responsible for capturing detailed information about every HTTP request and response passing through the application. This includes logging the request and response metadata, body (if enabled), and any exceptions that occur during processing. The logged data can be stored in a database for auditing, debugging, or analytical purposes. + +### Key Features + +- Logs HTTP request and response metadata such as headers, method, URL, and body. +- Measures the duration of request processing. +- Captures exception details if an error occurs during processing. +- Configurable options to enable or disable logging of request and response bodies. + +### How It Works + +1. The middleware intercepts all HTTP requests and responses. +2. If configured, it reads the request and response bodies. +3. Exceptions encountered during request processing are captured and logged. +4. Asynchronous logging ensures minimal performance impact. +5. The collected data is structured into an `HttpLog` object and saved to the database. + +--- + +## Configuring `HttpLogConfig` + +The `HttpLogConfig` class allows you to customize the logging behavior. It can be configured via the application's `appsettings.json` or other configuration providers. + +### Configuration Properties + +- `Enable` (bool): Toggles the logging functionality. Default is `false`. +- `EnableRequestBody` (bool): Enables or disables logging of the HTTP request body. Default is `false`. +- `EnableResponseBody` (bool): Enables or disables logging of the HTTP response body. Default is `false`. + +### Example `appsettings.json` Configuration + +```json +{ + "HttpLogConfig": { + "Enable": true, + "EnableRequestBody": true, + "EnableResponseBody": true + } +} +``` + +## HttpLog Details + +The `HttpLog` class is used to store detailed information about HTTP requests, responses, and exceptions encountered during processing. This data can be used for debugging, auditing, or performance analysis. + +### General Information + +| Property | Type | Description | +|-----------------------|--------|---------------------------------------------------------------| +| `StatusCode` | `int` | The HTTP status code returned in the response. | +| `Duration` | `long` | The total time taken to process the request, in milliseconds. | +| `AssemblyName` | `string` | The name of the assembly handling the request. | +| `AssemblyVersion` | `string` | The version of the assembly handling the request. | +| `ProcessId` | `int` | The process ID of the application handling the request. | +| `ProcessName` | `string` | The name of the process handling the request. | +| `ThreadId` | `int` | The thread ID of the thread handling the request. | +| `MemoryUsage` | `long` | The memory usage (in bytes) at the time of logging. | +| `MachineName` | `string` | The name of the machine handling the request. | +| `EnvironmentName` | `string` | The environment name (e.g., Development, Production). | +| `EnvironmentUserName` | `string` | The username of the user running the application. | +| `IsAuthenticated` | `bool` | Indicates whether the request is authenticated. | +| `Language` | `string` | The language preference of the user making the request. | +| `SessionId` | `string` | The session ID associated with the request. | +| `StartDate` | `DateTime` | The start date and time of the request. | +| `TraceId` | `string` | A unique identifier for tracing the request. | +| `UniqueId` | `string` | A unique identifier associated with the request. | +| `UserId` | `Guid` | The ID of the user making the request. | +| `UserIp` | `string` | The IP address of the user making the request. | +| `Username` | `string` | The username of the authenticated user. | +| `ApiTokenKey` | `string` | The API token key used for authentication (if applicable). | + +--- + +### Request Details + +| Property | Type | Description | +|--------------------|-------------------------|--------------------------------------------------------------| +| `ReqUrl` | `string` | The full URL of the request. | +| `ReqProtocol` | `string` | The protocol used in the request (e.g., HTTP/1.1). | +| `ReqMethod` | `string` | The HTTP method of the request (e.g., GET, POST). | +| `ReqScheme` | `string` | The scheme used in the request (e.g., http, https). | +| `ReqPathBase` | `string` | The base path of the request. | +| `ReqPath` | `string` | The path of the request. | +| `QueryString` | `string` | The query string of the request. | +| `ReqContentType` | `string` | The content type of the request. | +| `ReqContentLength` | `long?` | The content length of the request body (if available). | +| `ReqBody` | `string?` | The body of the request (if enabled). | +| `ReqHeaders` | `Dictionary` | The HTTP headers of the request. | + +--- + +### Response Details + +| Property | Type | Description | +|--------------------|-------------------------|--------------------------------------------------------------| +| `ResContentType` | `string` | The content type of the response. | +| `ResContentLength` | `long?` | The content length of the response body (if available). | +| `ResBody` | `string?` | The body of the response (if enabled). | +| `ResHeaders` | `Dictionary` | The HTTP headers of the response. | + +--- + +### Exception Details + +| Property | Type | Description | +|----------------|-------------|---------------------------------------------------------------| +| `ExData` | `IDictionary?` | Additional data associated with the exception. | +| `ExHelpLink` | `string?` | A link to documentation or support for the exception. | +| `ExHResult` | `int?` | The HRESULT code for the exception. | +| `ExMessage` | `string?` | The message describing the exception. | +| `ExSource` | `string?` | The source of the exception (e.g., assembly or method name). | +| `ExStackTrace` | `string?` | The stack trace for the exception. | + +--- + +### Usage + +The `HttpLog` class provides a structured format for capturing request, response, and exception data. It is typically populated by the HTTP Logging Middleware and stored in a database or other logging infrastructure for further analysis. + +This data is useful for: +- Debugging and resolving issues in the application. +- Auditing HTTP interactions for compliance or monitoring. +- Analyzing application performance and identifying bottlenecks. diff --git a/docs/README.md b/docs/README.md index a838ba72a..133da6499 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,2 +1,7 @@ -# FluentCMS.Docs -FluentCMS documentation +# FluentCMS + +## Documentation + +## Logging + +For detailed information about HTTP Logging functionality, configuration, and usage, refer to the [Logging Documentation](Logging.md). diff --git a/src/Backend/FluentCMS.Entities/HttpLog.cs b/src/Backend/FluentCMS.Entities/HttpLog.cs index 26c5aa8a3..979c0a8f2 100644 --- a/src/Backend/FluentCMS.Entities/HttpLog.cs +++ b/src/Backend/FluentCMS.Entities/HttpLog.cs @@ -2,62 +2,229 @@ namespace FluentCMS.Entities; +/// +/// Represents an HTTP log entry that captures detailed information about an HTTP request, response, and any associated exceptions. +/// public sealed class HttpLog : Entity { + /// + /// Gets or sets the HTTP status code of the response. + /// public int StatusCode { get; set; } + + /// + /// Gets or sets the duration of the request processing in milliseconds. + /// public long Duration { get; set; } + + /// + /// Gets or sets the name of the assembly handling the request. + /// public string AssemblyName { get; set; } = string.Empty; + + /// + /// Gets or sets the version of the assembly handling the request. + /// public string AssemblyVersion { get; set; } = string.Empty; + + /// + /// Gets or sets the process ID of the application handling the request. + /// public int ProcessId { get; set; } + + /// + /// Gets or sets the name of the process handling the request. + /// public string ProcessName { get; set; } = string.Empty; + + /// + /// Gets or sets the thread ID where the request is being handled. + /// public int ThreadId { get; set; } + + /// + /// Gets or sets the memory usage in bytes at the time of logging. + /// public long MemoryUsage { get; set; } + + /// + /// Gets or sets the name of the machine handling the request. + /// public string MachineName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the environment (e.g., Development, Production). + /// public string EnvironmentName { get; set; } = string.Empty; + + /// + /// Gets or sets the name of the user running the environment. + /// public string EnvironmentUserName { get; set; } = string.Empty; + + /// + /// Gets or sets a value indicating whether the request is authenticated. + /// public bool IsAuthenticated { get; set; } + + /// + /// Gets or sets the language of the request. + /// public string Language { get; set; } = string.Empty; + + /// + /// Gets or sets the session ID associated with the request. + /// public string SessionId { get; set; } = string.Empty; + + /// + /// Gets or sets the start date and time of the request. + /// public DateTime StartDate { get; set; } + + /// + /// Gets or sets the trace ID for distributed tracing. + /// public string TraceId { get; set; } = string.Empty; + + /// + /// Gets or sets the unique ID for the request. + /// public string UniqueId { get; set; } = string.Empty; + + /// + /// Gets or sets the user ID associated with the request. + /// public Guid UserId { get; set; } + + /// + /// Gets or sets the IP address of the user making the request. + /// public string UserIp { get; set; } = string.Empty; + + /// + /// Gets or sets the username of the user making the request. + /// public string Username { get; set; } = string.Empty; + + /// + /// Gets or sets the API token key used for authentication. + /// public string ApiTokenKey { get; set; } = string.Empty; - #region Reguest + #region Request + /// + /// Gets or sets the URL of the request. + /// public string ReqUrl { get; set; } = string.Empty; + + /// + /// Gets or sets the protocol of the request (e.g., HTTP/1.1). + /// public string ReqProtocol { get; set; } = string.Empty; + + /// + /// Gets or sets the HTTP method of the request (e.g., GET, POST). + /// public string ReqMethod { get; set; } = string.Empty; + + /// + /// Gets or sets the scheme of the request (e.g., http, https). + /// public string ReqScheme { get; set; } = string.Empty; + + /// + /// Gets or sets the base path of the request. + /// public string ReqPathBase { get; set; } = string.Empty; + + /// + /// Gets or sets the path of the request. + /// public string ReqPath { get; set; } = string.Empty; + + /// + /// Gets or sets the query string of the request. + /// public string QueryString { get; set; } = string.Empty; + + /// + /// Gets or sets the content type of the request. + /// public string ReqContentType { get; set; } = string.Empty; + + /// + /// Gets or sets the content length of the request body. + /// public long? ReqContentLength { get; set; } + + /// + /// Gets or sets the body of the request. + /// public string? ReqBody { get; set; } - public Dictionary ReqHeaders { get; set; } = []; + + /// + /// Gets or sets the headers of the request. + /// + public Dictionary ReqHeaders { get; set; } = new(); #endregion #region Response + /// + /// Gets or sets the content type of the response. + /// public string ResContentType { get; set; } = string.Empty; + + /// + /// Gets or sets the content length of the response body. + /// public long? ResContentLength { get; set; } + + /// + /// Gets or sets the body of the response. + /// public string? ResBody { get; set; } - public Dictionary ResHeaders { get; set; } = []; + + /// + /// Gets or sets the headers of the response. + /// + public Dictionary ResHeaders { get; set; } = new(); #endregion #region Exception + /// + /// Gets or sets additional data associated with an exception. + /// public IDictionary? ExData { get; set; } + + /// + /// Gets or sets the help link for the exception. + /// public string? ExHelpLink { get; set; } + + /// + /// Gets or sets the HResult code for the exception. + /// public int? ExHResult { get; set; } + + /// + /// Gets or sets the message of the exception. + /// public string? ExMessage { get; set; } + + /// + /// Gets or sets the source of the exception. + /// public string? ExSource { get; set; } + + /// + /// Gets or sets the stack trace of the exception. + /// public string? ExStackTrace { get; set; } #endregion From ec4edcec6b0f4517372ff4c576b3a1cf2e243901 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 18:56:33 -0500 Subject: [PATCH 14/25] Add HttpLogs table for detailed HTTP request/response logging A new table named `HttpLogs` has been created to store detailed logs of HTTP requests and responses. This includes metadata such as status code, duration, assembly details, process and thread information, memory usage, machine and environment details, user information, request and response details, and exception details. This addition will aid in tracking and analyzing HTTP interactions within the system. --- .../Setup.sql | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore.SqlServer/Setup.sql b/src/Backend/Repositories/FluentCMS.Repositories.EFCore.SqlServer/Setup.sql index ee19382a8..20584fcd0 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore.SqlServer/Setup.sql +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore.SqlServer/Setup.sql @@ -1,4 +1,57 @@ -/****** Object: Table [dbo].[ApiTokenPolicies] Script Date: 2024-11-24 5:32:26 PM ******/ +CREATE TABLE HttpLogs ( + Id UNIQUEIDENTIFIER NOT NULL PRIMARY KEY, -- Assuming inherited from Entity + StatusCode INT NOT NULL, + Duration BIGINT NOT NULL, + AssemblyName NVARCHAR(255) NOT NULL, + AssemblyVersion NVARCHAR(50) NOT NULL, + ProcessId INT NOT NULL, + ProcessName NVARCHAR(255) NOT NULL, + ThreadId INT NOT NULL, + MemoryUsage BIGINT NOT NULL, + MachineName NVARCHAR(255) NOT NULL, + EnvironmentName NVARCHAR(100) NOT NULL, + EnvironmentUserName NVARCHAR(255) NOT NULL, + IsAuthenticated BIT NOT NULL, + Language NVARCHAR(50) NOT NULL, + SessionId NVARCHAR(255) NOT NULL, + StartDate DATETIME2 NOT NULL, + TraceId NVARCHAR(255) NOT NULL, + UniqueId NVARCHAR(255) NOT NULL, + UserId UNIQUEIDENTIFIER NOT NULL, + UserIp NVARCHAR(50) NOT NULL, + Username NVARCHAR(255) NOT NULL, + ApiTokenKey NVARCHAR(255) NOT NULL, + + -- Request details + ReqUrl NVARCHAR(MAX) NOT NULL, + ReqProtocol NVARCHAR(50) NOT NULL, + ReqMethod NVARCHAR(50) NOT NULL, + ReqScheme NVARCHAR(50) NOT NULL, + ReqPathBase NVARCHAR(255) NOT NULL, + ReqPath NVARCHAR(255) NOT NULL, + QueryString NVARCHAR(MAX) NOT NULL, + ReqContentType NVARCHAR(255) NOT NULL, + ReqContentLength BIGINT NULL, + ReqBody NVARCHAR(MAX) NULL, + ReqHeaders NVARCHAR(MAX) NOT NULL, -- Store headers as JSON + + -- Response details + ResContentType NVARCHAR(255) NOT NULL, + ResContentLength BIGINT NULL, + ResBody NVARCHAR(MAX) NULL, + ResHeaders NVARCHAR(MAX) NOT NULL, -- Store headers as JSON + + -- Exception details + ExData NVARCHAR(MAX) NULL, -- Store exception data as JSON or serialized string + ExHelpLink NVARCHAR(MAX) NULL, + ExHResult INT NULL, + ExMessage NVARCHAR(MAX) NULL, + ExSource NVARCHAR(MAX) NULL, + ExStackTrace NVARCHAR(MAX) NULL +); + + +/****** Object: Table [dbo].[ApiTokenPolicies] Script Date: 2024-11-24 5:32:26 PM ******/ SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON From 40d875161309a6e1b9edffb178adb74c808725f1 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 18:58:03 -0500 Subject: [PATCH 15/25] Add HttpLogs table to Setup.sql for detailed HTTP logging A new table named `HttpLogs` has been created in the `Setup.sql` file to store detailed HTTP log information. This table includes columns for request and response details, user and environment information, and exception details. Additionally, the comment `-- Table: ApiTokenPolicies` was moved to a different position in the file, but the `ApiTokenPolicies` table definition remains unchanged. --- .../Setup.sql | 55 ++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore.Sqlite/Setup.sql b/src/Backend/Repositories/FluentCMS.Repositories.EFCore.Sqlite/Setup.sql index 5d701f843..9222ac35a 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore.Sqlite/Setup.sql +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore.Sqlite/Setup.sql @@ -1,4 +1,57 @@ --- Table: ApiTokenPolicies +-- Table: HttpLogs +CREATE TABLE HttpLogs ( + Id TEXT NOT NULL PRIMARY KEY, -- Assuming Id is a GUID stored as TEXT + StatusCode INTEGER NOT NULL, + Duration INTEGER NOT NULL, + AssemblyName TEXT NOT NULL, + AssemblyVersion TEXT NOT NULL, + ProcessId INTEGER NOT NULL, + ProcessName TEXT NOT NULL, + ThreadId INTEGER NOT NULL, + MemoryUsage INTEGER NOT NULL, + MachineName TEXT NOT NULL, + EnvironmentName TEXT NOT NULL, + EnvironmentUserName TEXT NOT NULL, + IsAuthenticated INTEGER NOT NULL, -- SQLite uses 0 (false) and 1 (true) for boolean + Language TEXT NOT NULL, + SessionId TEXT NOT NULL, + StartDate TEXT NOT NULL, -- ISO 8601 format for DATETIME + TraceId TEXT NOT NULL, + UniqueId TEXT NOT NULL, + UserId TEXT NOT NULL, -- GUID stored as TEXT + UserIp TEXT NOT NULL, + Username TEXT NOT NULL, + ApiTokenKey TEXT NOT NULL, + + -- Request details + ReqUrl TEXT NOT NULL, + ReqProtocol TEXT NOT NULL, + ReqMethod TEXT NOT NULL, + ReqScheme TEXT NOT NULL, + ReqPathBase TEXT NOT NULL, + ReqPath TEXT NOT NULL, + QueryString TEXT NOT NULL, + ReqContentType TEXT NOT NULL, + ReqContentLength INTEGER NULL, + ReqBody TEXT NULL, + ReqHeaders TEXT NOT NULL, -- Store headers as JSON + + -- Response details + ResContentType TEXT NOT NULL, + ResContentLength INTEGER NULL, + ResBody TEXT NULL, + ResHeaders TEXT NOT NULL, -- Store headers as JSON + + -- Exception details + ExData TEXT NULL, -- Store exception data as JSON or serialized string + ExHelpLink TEXT NULL, + ExHResult INTEGER NULL, + ExMessage TEXT NULL, + ExSource TEXT NULL, + ExStackTrace TEXT NULL +); + +-- Table: ApiTokenPolicies CREATE TABLE ApiTokenPolicies ( Id TEXT NOT NULL PRIMARY KEY, ApiTokenId TEXT NOT NULL, From 95c890c5d17471aa508a43fa704fa6a02afdb1bf Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 19:03:49 -0500 Subject: [PATCH 16/25] Add JSON conversion for HttpLog properties in DbContext Updated FluentCmsDbContext.cs to include JSON serialization and deserialization for HttpLog entity properties (ReqHeaders, ResHeaders, ExData) using JsonSerializer. Added necessary using directives for System.Collections and System.Text.Json. Ensured base class OnModelCreating method is called. --- .../FluentCmsDbContext.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs index 295571192..ad7762a48 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Identity; +using System.Collections; using System.Text.Json; namespace FluentCMS.Repositories.EFCore; @@ -126,6 +127,22 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) claims => JsonSerializer.Deserialize>>(claims, jsonSerializerOptions) ?? new List>()); }); + modelBuilder.Entity(entity => + { + entity.Property(h => h.ReqHeaders) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerOptions), + v => JsonSerializer.Deserialize>(v, jsonSerializerOptions) ?? new Dictionary()); + entity.Property(h => h.ResHeaders) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerOptions), + v => JsonSerializer.Deserialize>(v, jsonSerializerOptions) ?? new Dictionary()); + entity.Property(h => h.ExData) + .HasConversion( + v => JsonSerializer.Serialize(v, jsonSerializerOptions), + v => JsonSerializer.Deserialize(v, jsonSerializerOptions) ?? new Dictionary()); + }); + base.OnModelCreating(modelBuilder); } From 1b8efcc07a5bf767417a2c2fc66a34f956453ebb Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 19:10:14 -0500 Subject: [PATCH 17/25] Switch to SQLite and add HTTP logging support - Register `IHttpLogRepository` as a scoped service in `EFCoreServiceExtensions`. - Add `DbSet` to `FluentCmsDbContext` for managing HTTP logs. - Update `appsettings.json` to use `sqlite` instead of `LiteDb` and adjust connection string. - Implement `HttpLogRepository` in `FluentCMS.Repositories.EFCore` to handle HTTP log entries. --- .../EFCoreServiceExtensions.cs | 1 + .../FluentCmsDbContext.cs | 1 + .../Repositories/HttpLogRepository.cs | 14 ++++++++++++++ src/FluentCMS/appsettings.json | 4 ++-- 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/EFCoreServiceExtensions.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/EFCoreServiceExtensions.cs index dc1a6f141..464e0c86c 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/EFCoreServiceExtensions.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/EFCoreServiceExtensions.cs @@ -16,6 +16,7 @@ public static IServiceCollection AddEFCoreRepositories(this IServiceCollection s services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs index ad7762a48..f2dd84c3f 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/FluentCmsDbContext.cs @@ -19,6 +19,7 @@ public class FluentCmsDbContext(DbContextOptions options) : public DbSet Files { get; set; } = default!; public DbSet Folders { get; set; } = default!; public DbSet GlobalSettings { get; set; } = default!; + public DbSet HttpLogs { get; set; } = default!; public DbSet Layouts { get; set; } = default!; public DbSet Pages { get; set; } = default!; public DbSet Permissions { get; set; } = default!; diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs new file mode 100644 index 000000000..a00fe3440 --- /dev/null +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs @@ -0,0 +1,14 @@ +namespace FluentCMS.Repositories.EFCore; + +public class HttpLogRepository(FluentCmsDbContext dbContext) : IHttpLogRepository +{ + public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + { + if (log.Id == Guid.Empty) + log.Id = Guid.NewGuid(); + + await dbContext.HttpLogs.AddAsync(log, cancellationToken); + + await dbContext.SaveChangesAsync(cancellationToken); + } +} diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index ca18ec7ba..77ed38985 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -1,7 +1,7 @@ { - "Database": "LiteDb", + "Database": "sqlite", "ConnectionStrings": { - "DefaultConnection": "Filename=./fluentcms.db" + "DefaultConnection": "Datasource=./fluentcms.db" }, "ClientSettings": { "Url": "http://localhost:5000", From fd258907436a8befd79cf6acf867198e28053434 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 19:16:07 -0500 Subject: [PATCH 18/25] Refactor logging and update database configuration Refactored `HttpLoggingMiddleware` to use `IHttpLogService` instead of `IHttpLogRepository` for logging HTTP requests. Updated `appsettings.json` to change the database configuration from SQLite to LiteDb, including modifying the connection string to match LiteDb's format. --- .../FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs | 6 +++--- src/FluentCMS/appsettings.json | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 9b6dd5232..b07a7fb55 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -28,7 +28,7 @@ public HttpLoggingMiddleware(RequestDelegate next, IOptions optio _assemblyName = _assembly?.GetName(); } - public async Task Invoke(HttpContext context, IHttpLogRepository repository) + public async Task Invoke(HttpContext context, IHttpLogService httpLogService) { if (!_httpLogConfig.Enable) { @@ -67,7 +67,7 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository) // Wait for all tasks to complete await Task.WhenAll(tasks); - await repository.Create(httpLog); + await httpLogService.Log(httpLog); } } @@ -107,7 +107,7 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository) // Wait for all tasks to complete await Task.WhenAll(tasks); - await repository.Create(httpLog); + await httpLogService.Log(httpLog); } } } diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index 77ed38985..ca18ec7ba 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -1,7 +1,7 @@ { - "Database": "sqlite", + "Database": "LiteDb", "ConnectionStrings": { - "DefaultConnection": "Datasource=./fluentcms.db" + "DefaultConnection": "Filename=./fluentcms.db" }, "ClientSettings": { "Url": "http://localhost:5000", From c98aeaef13ec49fd7fc45068c4a8207c106c24a5 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sat, 30 Nov 2024 19:36:02 -0500 Subject: [PATCH 19/25] Refactor HttpLogService to use background processing queue Refactored HttpLogService to use a background processing queue for logging HTTP logs. Updated IHttpLogService interface to reflect non-blocking logging. Modified HttpLoggingMiddleware to call Log method without awaiting. Updated IHttpLogRepository interface and repository implementations to support batch processing of log entries, improving performance and efficiency. --- .../FluentCMS.Services/HttpLogService.cs | 131 +++++++++++++++++- .../Middleware/HttpLoggingMiddleware.cs | 4 +- .../IHttpLogRepository.cs | 2 +- .../Repositories/HttpLogRepository.cs | 16 ++- .../HttpLogRepository.cs | 55 +++----- .../HttpLogRepository.cs | 24 +++- 6 files changed, 180 insertions(+), 52 deletions(-) diff --git a/src/Backend/FluentCMS.Services/HttpLogService.cs b/src/Backend/FluentCMS.Services/HttpLogService.cs index f89ea85d6..5291949c4 100644 --- a/src/Backend/FluentCMS.Services/HttpLogService.cs +++ b/src/Backend/FluentCMS.Services/HttpLogService.cs @@ -1,14 +1,135 @@ -namespace FluentCMS.Services; +using Microsoft.Extensions.Hosting; +using System.Threading.Channels; +namespace FluentCMS.Services; + +/// +/// Interface for logging HTTP logs. +/// public interface IHttpLogService : IAutoRegisterService { - Task Log(HttpLog httpLog, CancellationToken cancellationToken = default); + /// + /// Enqueues a log entry for background processing. + /// + /// The HTTP log entry. + /// The cancellation token. + void Log(HttpLog httpLog, CancellationToken cancellationToken = default); } -public class HttpLogService (IHttpLogRepository httpLogRepository) : IHttpLogService +/// +/// Implementation of the HTTP log service using a background queue for logging. +/// +public class HttpLogService : IHttpLogService, IHostedService { - public async Task Log(HttpLog httpLog, CancellationToken cancellationToken = default) + private readonly IHttpLogRepository _httpLogRepository; + private readonly Channel _logChannel; + private Task? _processingTask; + private readonly CancellationTokenSource _cts; + private readonly int _batchSize = 10; // Batch size for processing logs + + /// + /// Initializes a new instance of the class. + /// + /// The HTTP log repository. + public HttpLogService(IHttpLogRepository httpLogRepository) + { + _httpLogRepository = httpLogRepository; + _logChannel = Channel.CreateUnbounded(); + _cts = new CancellationTokenSource(); + } + + /// + /// Enqueues a log entry for background processing. + /// + /// The HTTP log entry. + /// The cancellation token. + public void Log(HttpLog httpLog, CancellationToken cancellationToken = default) + { + if (!_logChannel.Writer.TryWrite(httpLog)) + { + // Handle overflow or fallback logic if needed + Console.WriteLine("Log channel is full. Log entry dropped."); + } + } + + /// + /// Starts the background processing task. + /// + /// The cancellation token. + public Task StartAsync(CancellationToken cancellationToken) + { + _processingTask = ProcessLogsAsync(_cts.Token); + return Task.CompletedTask; + } + + /// + /// Stops the background processing task. + /// + /// The cancellation token. + public async Task StopAsync(CancellationToken cancellationToken) + { + _cts.Cancel(); + + if (_processingTask != null) + { + await _processingTask; + } + } + + /// + /// Processes log entries in the background in batches. + /// + /// The cancellation token. + private async Task ProcessLogsAsync(CancellationToken cancellationToken) + { + var batch = new List(); + + try + { + await foreach (var log in _logChannel.Reader.ReadAllAsync(cancellationToken)) + { + batch.Add(log); + + // Process batch when the size reaches the configured limit + if (batch.Count >= _batchSize) + { + await ProcessBatchAsync(batch, cancellationToken); + batch.Clear(); + } + } + } + catch (OperationCanceledException) + { + // Gracefully exit on cancellation + } + catch (Exception ex) + { + // Handle unexpected exceptions + Console.WriteLine($"Error in log processing: {ex.Message}"); + } + + // Process any remaining logs in the batch + if (batch.Count > 0) + { + await ProcessBatchAsync(batch, cancellationToken); + } + } + + /// + /// Processes a batch of log entries. + /// + /// The batch of log entries. + /// The cancellation token. + private async Task ProcessBatchAsync(IEnumerable batch, CancellationToken cancellationToken) { - await httpLogRepository.Create(httpLog, cancellationToken); + try + { + await _httpLogRepository.CreateMany(batch, cancellationToken); + } + catch (Exception ex) + { + // Handle exceptions during batch processing + Console.WriteLine($"Failed to process batch: {ex.Message}"); + } } } diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index b07a7fb55..b0ecacedb 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -67,7 +67,7 @@ public async Task Invoke(HttpContext context, IHttpLogService httpLogService) // Wait for all tasks to complete await Task.WhenAll(tasks); - await httpLogService.Log(httpLog); + httpLogService.Log(httpLog); } } @@ -107,7 +107,7 @@ public async Task Invoke(HttpContext context, IHttpLogService httpLogService) // Wait for all tasks to complete await Task.WhenAll(tasks); - await httpLogService.Log(httpLog); + httpLogService.Log(httpLog); } } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs index 453e93f94..917e28f8e 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.Abstractions/IHttpLogRepository.cs @@ -2,5 +2,5 @@ public interface IHttpLogRepository { - Task Create(HttpLog log, CancellationToken cancellationToken = default); + Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default); } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs index a00fe3440..9e0055fec 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs @@ -1,14 +1,16 @@ -namespace FluentCMS.Repositories.EFCore; + +namespace FluentCMS.Repositories.EFCore; public class HttpLogRepository(FluentCmsDbContext dbContext) : IHttpLogRepository { - public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + public async Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default) { - if (log.Id == Guid.Empty) - log.Id = Guid.NewGuid(); - - await dbContext.HttpLogs.AddAsync(log, cancellationToken); - + foreach (var http in httpLogs) + { + if (http.Id == Guid.Empty) + http.Id = Guid.NewGuid(); + } + await dbContext.HttpLogs.AddRangeAsync(httpLogs, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); } } diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index c94617a44..cd13b7534 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -1,38 +1,29 @@ -namespace FluentCMS.Repositories.LiteDb; + +namespace FluentCMS.Repositories.LiteDb; -public class HttpLogRepository : IHttpLogRepository +public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepository { - private readonly ILiteDatabaseAsync _liteDatabase; - private readonly ILiteDBContext _liteDbContext; - - // TODO: This is a workaround to check if DB is initialized - // TODO: Not sure it is a valid approach - private static bool _isInitialized = false; - - public HttpLogRepository(ILiteDBContext liteDbContext) - { - _liteDatabase = liteDbContext.Database; - _liteDbContext = liteDbContext; - - if (!_isInitialized) - { - // check if DB exists and has at least one collection (any collection) - _isInitialized = _liteDatabase.GetCollectionNamesAsync().GetAwaiter().GetResult().Any(); - } - } - - public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + public async Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default) { - // log into DB only if it is initialized - if (_isInitialized) - { - cancellationToken.ThrowIfCancellationRequested(); - - log.Id = Guid.NewGuid(); - var collection = _liteDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); - - await collection.InsertAsync(log); - } + cancellationToken.ThrowIfCancellationRequested(); + + if (httpLogs == null) + return; + + // Create a list of tasks to insert the HTTP logs into the appropriate collection. + var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) + .Select(async group => + { + // Get the collection name from the group. + var collectionName = group.Key; + // Get the collection from the database. + var collection = liteDbContext.Database.GetCollection(collectionName); + // Insert the HTTP logs into the collection. + await collection.InsertBulkAsync(group); + }); + + // Wait for all tasks to complete. + await Task.WhenAll(tasks); } private static string GetCollectionName(int statusCode) => statusCode switch diff --git a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs index 6a490e1ca..a855462d2 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.MongoDB/HttpLogRepository.cs @@ -1,17 +1,31 @@ -namespace FluentCMS.Repositories.MongoDB; + +namespace FluentCMS.Repositories.MongoDB; public class HttpLogRepository(IMongoDBContext mongoDbContext) : IHttpLogRepository { - public async Task Create(HttpLog log, CancellationToken cancellationToken = default) + public async Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); - var collection = mongoDbContext.Database.GetCollection(GetCollectionName(log.StatusCode)); - var options = new InsertOneOptions { BypassDocumentValidation = false }; + if (httpLogs == null) + return; - await collection.InsertOneAsync(log, options, cancellationToken); + // Create a list of tasks to insert the HTTP logs into the appropriate collection. + var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) + .Select(async group => + { + // Get the collection name from the group. + var collectionName = group.Key; + // Get the collection from the database. + var collection = mongoDbContext.Database.GetCollection(collectionName); + // Insert the HTTP logs into the collection. + await collection.InsertManyAsync(group, cancellationToken: cancellationToken); + }); + // Wait for all tasks to complete. + await Task.WhenAll(tasks); } + private static string GetCollectionName(int statusCode) => statusCode switch { < 500 and >= 400 => "httphog400", From a1a1f3742735cada0339175eec202adab7254e87 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sun, 1 Dec 2024 09:59:38 -0500 Subject: [PATCH 20/25] Refactor HTTP logging and add background processing Refactored `HttpLogService` to use `IHttpLogChannel` and removed the original implementation and interface. Commented out HTTP logging middleware in `ApiServiceExtensions.cs` and `HttpLoggingMiddleware.cs`. Updated `ServiceExtensions.cs` to comment out `BackgroundHttpLogProcessor` and `HttpLogChannel` registrations. Commented out cancellation token checks and log insertion logic in `HttpLogRepository.cs` for EFCore and LiteDb. Added `BackgroundHttpLogProcessor.cs` for batch processing of HTTP logs and `HttpLogChannel.cs` for HTTP log channel implementation. --- .../FluentCMS.Services/HttpLogService.cs | 149 +----- .../Logging/BackgroundHttpLogProcessor.cs | 61 +++ .../Logging/HttpLogChannel.cs | 43 ++ .../FluentCMS.Services/ServiceExtensions.cs | 4 +- .../Extentions/ApiServiceExtensions.cs | 2 +- .../Middleware/HttpLoggingMiddleware.cs | 431 +++++++++--------- .../Repositories/HttpLogRepository.cs | 3 +- .../HttpLogRepository.cs | 35 +- 8 files changed, 355 insertions(+), 373 deletions(-) create mode 100644 src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs create mode 100644 src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs diff --git a/src/Backend/FluentCMS.Services/HttpLogService.cs b/src/Backend/FluentCMS.Services/HttpLogService.cs index 5291949c4..253edda92 100644 --- a/src/Backend/FluentCMS.Services/HttpLogService.cs +++ b/src/Backend/FluentCMS.Services/HttpLogService.cs @@ -1,135 +1,14 @@ -using Microsoft.Extensions.Hosting; -using System.Threading.Channels; - -namespace FluentCMS.Services; - -/// -/// Interface for logging HTTP logs. -/// -public interface IHttpLogService : IAutoRegisterService -{ - /// - /// Enqueues a log entry for background processing. - /// - /// The HTTP log entry. - /// The cancellation token. - void Log(HttpLog httpLog, CancellationToken cancellationToken = default); -} - -/// -/// Implementation of the HTTP log service using a background queue for logging. -/// -public class HttpLogService : IHttpLogService, IHostedService -{ - private readonly IHttpLogRepository _httpLogRepository; - private readonly Channel _logChannel; - private Task? _processingTask; - private readonly CancellationTokenSource _cts; - private readonly int _batchSize = 10; // Batch size for processing logs - - /// - /// Initializes a new instance of the class. - /// - /// The HTTP log repository. - public HttpLogService(IHttpLogRepository httpLogRepository) - { - _httpLogRepository = httpLogRepository; - _logChannel = Channel.CreateUnbounded(); - _cts = new CancellationTokenSource(); - } - - /// - /// Enqueues a log entry for background processing. - /// - /// The HTTP log entry. - /// The cancellation token. - public void Log(HttpLog httpLog, CancellationToken cancellationToken = default) - { - if (!_logChannel.Writer.TryWrite(httpLog)) - { - // Handle overflow or fallback logic if needed - Console.WriteLine("Log channel is full. Log entry dropped."); - } - } - - /// - /// Starts the background processing task. - /// - /// The cancellation token. - public Task StartAsync(CancellationToken cancellationToken) - { - _processingTask = ProcessLogsAsync(_cts.Token); - return Task.CompletedTask; - } - - /// - /// Stops the background processing task. - /// - /// The cancellation token. - public async Task StopAsync(CancellationToken cancellationToken) - { - _cts.Cancel(); - - if (_processingTask != null) - { - await _processingTask; - } - } - - /// - /// Processes log entries in the background in batches. - /// - /// The cancellation token. - private async Task ProcessLogsAsync(CancellationToken cancellationToken) - { - var batch = new List(); - - try - { - await foreach (var log in _logChannel.Reader.ReadAllAsync(cancellationToken)) - { - batch.Add(log); - - // Process batch when the size reaches the configured limit - if (batch.Count >= _batchSize) - { - await ProcessBatchAsync(batch, cancellationToken); - batch.Clear(); - } - } - } - catch (OperationCanceledException) - { - // Gracefully exit on cancellation - } - catch (Exception ex) - { - // Handle unexpected exceptions - Console.WriteLine($"Error in log processing: {ex.Message}"); - } - - // Process any remaining logs in the batch - if (batch.Count > 0) - { - await ProcessBatchAsync(batch, cancellationToken); - } - } - - /// - /// Processes a batch of log entries. - /// - /// The batch of log entries. - /// The cancellation token. - private async Task ProcessBatchAsync(IEnumerable batch, CancellationToken cancellationToken) - { - try - { - await _httpLogRepository.CreateMany(batch, cancellationToken); - } - catch (Exception ex) - { - // Handle exceptions during batch processing - Console.WriteLine($"Failed to process batch: {ex.Message}"); - } - } -} +//namespace FluentCMS.Services; + +//public interface IHttpLogService : IAutoRegisterService +//{ +// void Log(HttpLog httpLog); +//} + +//public class HttpLogService(IHttpLogChannel httpLogChannel) : IHttpLogService +//{ +// public void Log(HttpLog httpLog) +// { +// httpLogChannel.Write(httpLog); +// } +//} diff --git a/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs b/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs new file mode 100644 index 000000000..e63abd32f --- /dev/null +++ b/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace FluentCMS.Services; + +public class BackgroundHttpLogProcessor(IHttpLogChannel logChannel, IServiceProvider serviceProvider) : BackgroundService +{ + // TODO: move this to HttpLogConfig being read from appsettings.json + private readonly int _batchSize = 50; // Number of logs per batch + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var batch = new List(); + + try + { + await foreach (var log in logChannel.ReadAllAsync(stoppingToken)) + { + batch.Add(log); + + // Process the batch when the size reaches the limit + if (batch.Count >= _batchSize) + { + await ProcessBatchAsync(batch, stoppingToken); + batch.Clear(); + } + } + } + catch (OperationCanceledException) + { + // Graceful exit on cancellation + } + catch (Exception) + { + // Log or handle unexpected errors + // TODO: Add logging + } + + // Process remaining logs before shutdown + if (batch.Count > 0) + { + await ProcessBatchAsync(batch, stoppingToken); + } + } + + private async Task ProcessBatchAsync(IEnumerable batch, CancellationToken cancellationToken) + { + try + { + // Create a new scope for each log entry + using var scope = serviceProvider.CreateScope(); + var httpLogRepository = scope.ServiceProvider.GetRequiredService(); + await httpLogRepository.CreateMany(batch, cancellationToken); + } + catch (Exception) + { + // Handle errors during batch processing, e.g., log or retry logic + // TODO: Add logging + } + } +} diff --git a/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs new file mode 100644 index 000000000..9804d6272 --- /dev/null +++ b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs @@ -0,0 +1,43 @@ +using System.Threading.Channels; + +namespace FluentCMS.Services; + +public interface IHttpLogChannel +{ + void Write(HttpLog httpLog); + IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default); +} + +public class HttpLogChannel : IHttpLogChannel +{ + private readonly Channel _logChannel; + + public HttpLogChannel() + { + _logChannel = Channel.CreateUnbounded(); + } + + public void Write(HttpLog httpLog) + { + if (!_logChannel.Writer.TryWrite(httpLog)) + { + // Handle overflow if needed + // TODO: For now, just ignore the log entry + } + } + + public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) + { + try + { + return _logChannel.Reader.ReadAllAsync(cancellationToken); + } + catch (Exception ex) + { + + throw; + } + + } +} + diff --git a/src/Backend/FluentCMS.Services/ServiceExtensions.cs b/src/Backend/FluentCMS.Services/ServiceExtensions.cs index 1d8ba6f51..c60e3a1dc 100644 --- a/src/Backend/FluentCMS.Services/ServiceExtensions.cs +++ b/src/Backend/FluentCMS.Services/ServiceExtensions.cs @@ -11,9 +11,11 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddAutoMapper(typeof(MappingProfile)); services.AddScoped(); - services.AddScoped(); + //services.AddHostedService(); + //services.AddSingleton(); + AddIdentity(services); RegisterServices(services); diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index e555dceda..5d91abc11 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -108,7 +108,7 @@ public static WebApplication UseApiService(this WebApplication app) { // this will be executed only when the path starts with "/api" app.UseMiddleware(); - app.UseMiddleware(); + //app.UseMiddleware(); }); app.UseAuthorization(); diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index b0ecacedb..55e83ded1 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -1,216 +1,215 @@ -using FluentCMS.Repositories.Abstractions; -using Microsoft.AspNetCore.Diagnostics; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Extensions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; -using System.Diagnostics; -using System.IO; -using System.Reflection; -using System.Text; - -namespace FluentCMS.Web.Api.Middleware; - -internal sealed class HttpLoggingMiddleware -{ - private readonly RequestDelegate _next; - private readonly HttpLogConfig _httpLogConfig; - private readonly Assembly? _assembly; - private readonly AssemblyName? _assemblyName; - -#pragma warning disable IDE0290 // Use primary constructor - public HttpLoggingMiddleware(RequestDelegate next, IOptions options) -#pragma warning restore IDE0290 // Use primary constructor - { - _next = next; - _httpLogConfig = options.Value ?? new HttpLogConfig(); - _assembly = Assembly.GetEntryAssembly(); - _assemblyName = _assembly?.GetName(); - } - - public async Task Invoke(HttpContext context, IHttpLogService httpLogService) - { - if (!_httpLogConfig.Enable) - { - await _next(context); - return; - } - - var stopwatch = Stopwatch.StartNew(); - Exception? exception = null; - - if (!_httpLogConfig.EnableResponseBody) - { - try - { - await _next(context); - } - catch (Exception ex) - { - exception = ex; - throw; - } - finally - { - stopwatch.Stop(); - - var httpLog = new HttpLog(); - - // Create a list of tasks to run simultaneously - var tasks = new[] - { - FillHttpLog(httpLog, context, stopwatch), - FillRequest(httpLog, context.Request), - FillResponse(httpLog, context.Response, null), - FillException(httpLog, context, exception) - }; - - // Wait for all tasks to complete - await Task.WhenAll(tasks); - httpLogService.Log(httpLog); - } - } - - if (_httpLogConfig.EnableResponseBody) - { - - var originalResponseStream = context.Response.Body; - - await using var responseMemoryStream = new MemoryStream(); - context.Response.Body = responseMemoryStream; - - try - { - await _next(context); - } - catch (Exception ex) - { - exception = ex; - throw; - } - finally - { - stopwatch.Stop(); - - var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); - - var httpLog = new HttpLog(); - - // Create a list of tasks to run simultaneously - var tasks = new[] - { - FillHttpLog(httpLog, context, stopwatch), - FillRequest(httpLog, context.Request), - FillResponse(httpLog, context.Response, responseBody), - FillException(httpLog, context, exception) - }; - - // Wait for all tasks to complete - await Task.WhenAll(tasks); - httpLogService.Log(httpLog); - } - } - } - - private async Task ReadRequestBody(HttpRequest request) - { - if (!_httpLogConfig.EnableRequestBody) - return string.Empty; - - request.EnableBuffering(); - - using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); - var requestBody = await reader.ReadToEndAsync(); - request.Body.Position = 0; - - return requestBody; - } - - private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream? memoryStream) - { - if (memoryStream == null) - return string.Empty; - - memoryStream.Position = 0; - using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); - var responseBody = await reader.ReadToEndAsync(); - memoryStream.Position = 0; - await memoryStream.CopyToAsync(originalResponseStream); - context.Response.Body = originalResponseStream; - - return responseBody; - } - - private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) - { - var thread = Thread.CurrentThread; - var process = Process.GetCurrentProcess(); - var apiContext = context.RequestServices.GetRequiredService(); - - httpLog.StatusCode = context.Response.StatusCode; - httpLog.Duration = stopwatch.ElapsedMilliseconds; - httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; - httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; - httpLog.ProcessId = process.Id; - httpLog.ProcessName = process.ProcessName; - httpLog.ThreadId = thread.ManagedThreadId; - httpLog.MemoryUsage = process.PrivateMemorySize64; - httpLog.MachineName = Environment.MachineName; - httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; - httpLog.EnvironmentUserName = Environment.UserName; - httpLog.ApiTokenKey = apiContext.ApiTokenKey; - httpLog.IsAuthenticated = apiContext.IsAuthenticated; - httpLog.Language = apiContext.Language; - httpLog.SessionId = apiContext.SessionId; - httpLog.StartDate = apiContext.StartDate; - httpLog.TraceId = apiContext.TraceId; - httpLog.UniqueId = apiContext.UniqueId; - httpLog.UserId = apiContext.UserId; - httpLog.UserIp = apiContext.UserIp; - httpLog.Username = apiContext.Username; - - await Task.CompletedTask; - } - - private async Task FillRequest(HttpLog httpLog, HttpRequest request) - { - var requestBody = _httpLogConfig.EnableRequestBody ? await ReadRequestBody(request) : null; - - httpLog.ReqUrl = request.GetDisplayUrl(); - httpLog.ReqProtocol = request.Protocol; - httpLog.ReqMethod = request.Method; - httpLog.ReqScheme = request.Scheme; - httpLog.ReqPathBase = request.PathBase; - httpLog.ReqPath = request.Path; - httpLog.QueryString = request.QueryString.Value ?? string.Empty; - httpLog.ReqContentType = request.ContentType ?? string.Empty; - httpLog.ReqContentLength = request.ContentLength; - httpLog.ReqBody = requestBody; - httpLog.ReqHeaders = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; - } - - private static async Task FillResponse(HttpLog httpLog, HttpResponse response, string? responseBody) - { - httpLog.ResContentType = response.ContentType ?? string.Empty; - httpLog.ResContentLength = response.ContentLength; - httpLog.ResBody = responseBody; - httpLog.ResHeaders = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; - await Task.CompletedTask; - } - private static async Task FillException(HttpLog httpLog, HttpContext context, Exception? exception) - { - exception ??= context.Features.Get()?.Error; - - if (exception == null) - return; - - httpLog.ExData = exception.Data; - httpLog.ExHelpLink = exception.HelpLink; - httpLog.ExHResult = exception.HResult; - httpLog.ExMessage = exception.Message; - httpLog.ExSource = exception.Source; - httpLog.ExStackTrace = exception.StackTrace; - - await Task.CompletedTask; - } -} +//using Microsoft.AspNetCore.Diagnostics; +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Http.Extensions; +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Options; +//using System.Diagnostics; +//using System.IO; +//using System.Reflection; +//using System.Text; + +//namespace FluentCMS.Web.Api.Middleware; + +//internal sealed class HttpLoggingMiddleware +//{ +// private readonly RequestDelegate _next; +// private readonly HttpLogConfig _httpLogConfig; +// private readonly Assembly? _assembly; +// private readonly AssemblyName? _assemblyName; + +//#pragma warning disable IDE0290 // Use primary constructor +// public HttpLoggingMiddleware(RequestDelegate next, IOptions options) +//#pragma warning restore IDE0290 // Use primary constructor +// { +// _next = next; +// _httpLogConfig = options.Value ?? new HttpLogConfig(); +// _assembly = Assembly.GetEntryAssembly(); +// _assemblyName = _assembly?.GetName(); +// } + +// public async Task Invoke(HttpContext context, IHttpLogService httpLogService, ISetupService setupService) +// { +// if (!_httpLogConfig.Enable || !await setupService.IsInitialized()) +// { +// await _next(context); +// return; +// } + +// var stopwatch = Stopwatch.StartNew(); +// Exception? exception = null; + +// if (!_httpLogConfig.EnableResponseBody) +// { +// try +// { +// await _next(context); +// } +// catch (Exception ex) +// { +// exception = ex; +// throw; +// } +// finally +// { +// stopwatch.Stop(); + +// var httpLog = new HttpLog(); + +// // Create a list of tasks to run simultaneously +// var tasks = new[] +// { +// FillHttpLog(httpLog, context, stopwatch), +// FillRequest(httpLog, context.Request), +// FillResponse(httpLog, context.Response, null), +// FillException(httpLog, context, exception) +// }; + +// // Wait for all tasks to complete +// await Task.WhenAll(tasks); +// httpLogService.Log(httpLog); +// } +// } + +// if (_httpLogConfig.EnableResponseBody) +// { + +// var originalResponseStream = context.Response.Body; + +// await using var responseMemoryStream = new MemoryStream(); +// context.Response.Body = responseMemoryStream; + +// try +// { +// await _next(context); +// } +// catch (Exception ex) +// { +// exception = ex; +// throw; +// } +// finally +// { +// stopwatch.Stop(); + +// var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + +// var httpLog = new HttpLog(); + +// // Create a list of tasks to run simultaneously +// var tasks = new[] +// { +// FillHttpLog(httpLog, context, stopwatch), +// FillRequest(httpLog, context.Request), +// FillResponse(httpLog, context.Response, responseBody), +// FillException(httpLog, context, exception) +// }; + +// // Wait for all tasks to complete +// await Task.WhenAll(tasks); +// httpLogService.Log(httpLog); +// } +// } +// } + +// private async Task ReadRequestBody(HttpRequest request) +// { +// if (!_httpLogConfig.EnableRequestBody) +// return string.Empty; + +// request.EnableBuffering(); + +// using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); +// var requestBody = await reader.ReadToEndAsync(); +// request.Body.Position = 0; + +// return requestBody; +// } + +// private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream? memoryStream) +// { +// if (memoryStream == null) +// return string.Empty; + +// memoryStream.Position = 0; +// using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); +// var responseBody = await reader.ReadToEndAsync(); +// memoryStream.Position = 0; +// await memoryStream.CopyToAsync(originalResponseStream); +// context.Response.Body = originalResponseStream; + +// return responseBody; +// } + +// private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) +// { +// var thread = Thread.CurrentThread; +// var process = Process.GetCurrentProcess(); +// var apiContext = context.RequestServices.GetRequiredService(); + +// httpLog.StatusCode = context.Response.StatusCode; +// httpLog.Duration = stopwatch.ElapsedMilliseconds; +// httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; +// httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; +// httpLog.ProcessId = process.Id; +// httpLog.ProcessName = process.ProcessName; +// httpLog.ThreadId = thread.ManagedThreadId; +// httpLog.MemoryUsage = process.PrivateMemorySize64; +// httpLog.MachineName = Environment.MachineName; +// httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; +// httpLog.EnvironmentUserName = Environment.UserName; +// httpLog.ApiTokenKey = apiContext.ApiTokenKey; +// httpLog.IsAuthenticated = apiContext.IsAuthenticated; +// httpLog.Language = apiContext.Language; +// httpLog.SessionId = apiContext.SessionId; +// httpLog.StartDate = apiContext.StartDate; +// httpLog.TraceId = apiContext.TraceId; +// httpLog.UniqueId = apiContext.UniqueId; +// httpLog.UserId = apiContext.UserId; +// httpLog.UserIp = apiContext.UserIp; +// httpLog.Username = apiContext.Username; + +// await Task.CompletedTask; +// } + +// private async Task FillRequest(HttpLog httpLog, HttpRequest request) +// { +// var requestBody = _httpLogConfig.EnableRequestBody ? await ReadRequestBody(request) : null; + +// httpLog.ReqUrl = request.GetDisplayUrl(); +// httpLog.ReqProtocol = request.Protocol; +// httpLog.ReqMethod = request.Method; +// httpLog.ReqScheme = request.Scheme; +// httpLog.ReqPathBase = request.PathBase; +// httpLog.ReqPath = request.Path; +// httpLog.QueryString = request.QueryString.Value ?? string.Empty; +// httpLog.ReqContentType = request.ContentType ?? string.Empty; +// httpLog.ReqContentLength = request.ContentLength; +// httpLog.ReqBody = requestBody; +// httpLog.ReqHeaders = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; +// } + +// private static async Task FillResponse(HttpLog httpLog, HttpResponse response, string? responseBody) +// { +// httpLog.ResContentType = response.ContentType ?? string.Empty; +// httpLog.ResContentLength = response.ContentLength; +// httpLog.ResBody = responseBody; +// httpLog.ResHeaders = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; +// await Task.CompletedTask; +// } +// private static async Task FillException(HttpLog httpLog, HttpContext context, Exception? exception) +// { +// exception ??= context.Features.Get()?.Error; + +// if (exception == null) +// return; + +// httpLog.ExData = exception.Data; +// httpLog.ExHelpLink = exception.HelpLink; +// httpLog.ExHResult = exception.HResult; +// httpLog.ExMessage = exception.Message; +// httpLog.ExSource = exception.Source; +// httpLog.ExStackTrace = exception.StackTrace; + +// await Task.CompletedTask; +// } +//} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs index 9e0055fec..a55e2332b 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.EFCore/Repositories/HttpLogRepository.cs @@ -1,5 +1,4 @@ - -namespace FluentCMS.Repositories.EFCore; +namespace FluentCMS.Repositories.EFCore; public class HttpLogRepository(FluentCmsDbContext dbContext) : IHttpLogRepository { diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index cd13b7534..0defafdab 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -1,29 +1,28 @@ - -namespace FluentCMS.Repositories.LiteDb; +namespace FluentCMS.Repositories.LiteDb; public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepository { public async Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default) { - cancellationToken.ThrowIfCancellationRequested(); + //cancellationToken.ThrowIfCancellationRequested(); - if (httpLogs == null) - return; + //if (httpLogs == null) + // return; - // Create a list of tasks to insert the HTTP logs into the appropriate collection. - var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) - .Select(async group => - { - // Get the collection name from the group. - var collectionName = group.Key; - // Get the collection from the database. - var collection = liteDbContext.Database.GetCollection(collectionName); - // Insert the HTTP logs into the collection. - await collection.InsertBulkAsync(group); - }); + //// Create a list of tasks to insert the HTTP logs into the appropriate collection. + //var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) + // .Select(async group => + // { + // // Get the collection name from the group. + // var collectionName = group.Key; + // // Get the collection from the database. + // var collection = liteDbContext.Database.GetCollection(collectionName); + // // Insert the HTTP logs into the collection. + // await collection.InsertBulkAsync(group); + // }); - // Wait for all tasks to complete. - await Task.WhenAll(tasks); + //// Wait for all tasks to complete. + //await Task.WhenAll(tasks); } private static string GetCollectionName(int statusCode) => statusCode switch From c1763409f1bb5a7834f5af48d3ad23e23fc18b12 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sun, 1 Dec 2024 10:07:49 -0500 Subject: [PATCH 21/25] Restore HTTP logging functionality Uncommented and restored the `HttpLogService`, `HttpLoggingMiddleware`, and `HttpLogRepository` across multiple files. Re-enabled the registration of related services and middleware in the service collection and middleware pipeline. Simplified the `ReadAllAsync` method in `HttpLogChannel.cs` by removing the try-catch block. --- .../FluentCMS.Services/HttpLogService.cs | 24 +- .../Logging/HttpLogChannel.cs | 11 +- .../FluentCMS.Services/ServiceExtensions.cs | 4 +- .../Extentions/ApiServiceExtensions.cs | 2 +- .../Middleware/HttpLoggingMiddleware.cs | 430 +++++++++--------- .../HttpLogRepository.cs | 32 +- 6 files changed, 247 insertions(+), 256 deletions(-) diff --git a/src/Backend/FluentCMS.Services/HttpLogService.cs b/src/Backend/FluentCMS.Services/HttpLogService.cs index 253edda92..b45adafb8 100644 --- a/src/Backend/FluentCMS.Services/HttpLogService.cs +++ b/src/Backend/FluentCMS.Services/HttpLogService.cs @@ -1,14 +1,14 @@ -//namespace FluentCMS.Services; +namespace FluentCMS.Services; -//public interface IHttpLogService : IAutoRegisterService -//{ -// void Log(HttpLog httpLog); -//} +public interface IHttpLogService : IAutoRegisterService +{ + void Log(HttpLog httpLog); +} -//public class HttpLogService(IHttpLogChannel httpLogChannel) : IHttpLogService -//{ -// public void Log(HttpLog httpLog) -// { -// httpLogChannel.Write(httpLog); -// } -//} +public class HttpLogService(IHttpLogChannel httpLogChannel) : IHttpLogService +{ + public void Log(HttpLog httpLog) + { + httpLogChannel.Write(httpLog); + } +} diff --git a/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs index 9804d6272..ccc4e7798 100644 --- a/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs +++ b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs @@ -28,16 +28,7 @@ public void Write(HttpLog httpLog) public IAsyncEnumerable ReadAllAsync(CancellationToken cancellationToken = default) { - try - { - return _logChannel.Reader.ReadAllAsync(cancellationToken); - } - catch (Exception ex) - { - - throw; - } - + return _logChannel.Reader.ReadAllAsync(cancellationToken); } } diff --git a/src/Backend/FluentCMS.Services/ServiceExtensions.cs b/src/Backend/FluentCMS.Services/ServiceExtensions.cs index c60e3a1dc..347044cab 100644 --- a/src/Backend/FluentCMS.Services/ServiceExtensions.cs +++ b/src/Backend/FluentCMS.Services/ServiceExtensions.cs @@ -13,8 +13,8 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); - //services.AddHostedService(); - //services.AddSingleton(); + services.AddSingleton(); + services.AddHostedService(); AddIdentity(services); diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index 5d91abc11..e555dceda 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -108,7 +108,7 @@ public static WebApplication UseApiService(this WebApplication app) { // this will be executed only when the path starts with "/api" app.UseMiddleware(); - //app.UseMiddleware(); + app.UseMiddleware(); }); app.UseAuthorization(); diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs index 55e83ded1..67eee8b0a 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs +++ b/src/Backend/FluentCMS.Web.Api/Middleware/HttpLoggingMiddleware.cs @@ -1,215 +1,215 @@ -//using Microsoft.AspNetCore.Diagnostics; -//using Microsoft.AspNetCore.Http; -//using Microsoft.AspNetCore.Http.Extensions; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Options; -//using System.Diagnostics; -//using System.IO; -//using System.Reflection; -//using System.Text; - -//namespace FluentCMS.Web.Api.Middleware; - -//internal sealed class HttpLoggingMiddleware -//{ -// private readonly RequestDelegate _next; -// private readonly HttpLogConfig _httpLogConfig; -// private readonly Assembly? _assembly; -// private readonly AssemblyName? _assemblyName; - -//#pragma warning disable IDE0290 // Use primary constructor -// public HttpLoggingMiddleware(RequestDelegate next, IOptions options) -//#pragma warning restore IDE0290 // Use primary constructor -// { -// _next = next; -// _httpLogConfig = options.Value ?? new HttpLogConfig(); -// _assembly = Assembly.GetEntryAssembly(); -// _assemblyName = _assembly?.GetName(); -// } - -// public async Task Invoke(HttpContext context, IHttpLogService httpLogService, ISetupService setupService) -// { -// if (!_httpLogConfig.Enable || !await setupService.IsInitialized()) -// { -// await _next(context); -// return; -// } - -// var stopwatch = Stopwatch.StartNew(); -// Exception? exception = null; - -// if (!_httpLogConfig.EnableResponseBody) -// { -// try -// { -// await _next(context); -// } -// catch (Exception ex) -// { -// exception = ex; -// throw; -// } -// finally -// { -// stopwatch.Stop(); - -// var httpLog = new HttpLog(); - -// // Create a list of tasks to run simultaneously -// var tasks = new[] -// { -// FillHttpLog(httpLog, context, stopwatch), -// FillRequest(httpLog, context.Request), -// FillResponse(httpLog, context.Response, null), -// FillException(httpLog, context, exception) -// }; - -// // Wait for all tasks to complete -// await Task.WhenAll(tasks); -// httpLogService.Log(httpLog); -// } -// } - -// if (_httpLogConfig.EnableResponseBody) -// { - -// var originalResponseStream = context.Response.Body; - -// await using var responseMemoryStream = new MemoryStream(); -// context.Response.Body = responseMemoryStream; - -// try -// { -// await _next(context); -// } -// catch (Exception ex) -// { -// exception = ex; -// throw; -// } -// finally -// { -// stopwatch.Stop(); - -// var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); - -// var httpLog = new HttpLog(); - -// // Create a list of tasks to run simultaneously -// var tasks = new[] -// { -// FillHttpLog(httpLog, context, stopwatch), -// FillRequest(httpLog, context.Request), -// FillResponse(httpLog, context.Response, responseBody), -// FillException(httpLog, context, exception) -// }; - -// // Wait for all tasks to complete -// await Task.WhenAll(tasks); -// httpLogService.Log(httpLog); -// } -// } -// } - -// private async Task ReadRequestBody(HttpRequest request) -// { -// if (!_httpLogConfig.EnableRequestBody) -// return string.Empty; - -// request.EnableBuffering(); - -// using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); -// var requestBody = await reader.ReadToEndAsync(); -// request.Body.Position = 0; - -// return requestBody; -// } - -// private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream? memoryStream) -// { -// if (memoryStream == null) -// return string.Empty; - -// memoryStream.Position = 0; -// using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); -// var responseBody = await reader.ReadToEndAsync(); -// memoryStream.Position = 0; -// await memoryStream.CopyToAsync(originalResponseStream); -// context.Response.Body = originalResponseStream; - -// return responseBody; -// } - -// private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) -// { -// var thread = Thread.CurrentThread; -// var process = Process.GetCurrentProcess(); -// var apiContext = context.RequestServices.GetRequiredService(); - -// httpLog.StatusCode = context.Response.StatusCode; -// httpLog.Duration = stopwatch.ElapsedMilliseconds; -// httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; -// httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; -// httpLog.ProcessId = process.Id; -// httpLog.ProcessName = process.ProcessName; -// httpLog.ThreadId = thread.ManagedThreadId; -// httpLog.MemoryUsage = process.PrivateMemorySize64; -// httpLog.MachineName = Environment.MachineName; -// httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; -// httpLog.EnvironmentUserName = Environment.UserName; -// httpLog.ApiTokenKey = apiContext.ApiTokenKey; -// httpLog.IsAuthenticated = apiContext.IsAuthenticated; -// httpLog.Language = apiContext.Language; -// httpLog.SessionId = apiContext.SessionId; -// httpLog.StartDate = apiContext.StartDate; -// httpLog.TraceId = apiContext.TraceId; -// httpLog.UniqueId = apiContext.UniqueId; -// httpLog.UserId = apiContext.UserId; -// httpLog.UserIp = apiContext.UserIp; -// httpLog.Username = apiContext.Username; - -// await Task.CompletedTask; -// } - -// private async Task FillRequest(HttpLog httpLog, HttpRequest request) -// { -// var requestBody = _httpLogConfig.EnableRequestBody ? await ReadRequestBody(request) : null; - -// httpLog.ReqUrl = request.GetDisplayUrl(); -// httpLog.ReqProtocol = request.Protocol; -// httpLog.ReqMethod = request.Method; -// httpLog.ReqScheme = request.Scheme; -// httpLog.ReqPathBase = request.PathBase; -// httpLog.ReqPath = request.Path; -// httpLog.QueryString = request.QueryString.Value ?? string.Empty; -// httpLog.ReqContentType = request.ContentType ?? string.Empty; -// httpLog.ReqContentLength = request.ContentLength; -// httpLog.ReqBody = requestBody; -// httpLog.ReqHeaders = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; -// } - -// private static async Task FillResponse(HttpLog httpLog, HttpResponse response, string? responseBody) -// { -// httpLog.ResContentType = response.ContentType ?? string.Empty; -// httpLog.ResContentLength = response.ContentLength; -// httpLog.ResBody = responseBody; -// httpLog.ResHeaders = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; -// await Task.CompletedTask; -// } -// private static async Task FillException(HttpLog httpLog, HttpContext context, Exception? exception) -// { -// exception ??= context.Features.Get()?.Error; - -// if (exception == null) -// return; - -// httpLog.ExData = exception.Data; -// httpLog.ExHelpLink = exception.HelpLink; -// httpLog.ExHResult = exception.HResult; -// httpLog.ExMessage = exception.Message; -// httpLog.ExSource = exception.Source; -// httpLog.ExStackTrace = exception.StackTrace; - -// await Task.CompletedTask; -// } -//} +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using System.Diagnostics; +using System.IO; +using System.Reflection; +using System.Text; + +namespace FluentCMS.Web.Api.Middleware; + +internal sealed class HttpLoggingMiddleware +{ + private readonly RequestDelegate _next; + private readonly HttpLogConfig _httpLogConfig; + private readonly Assembly? _assembly; + private readonly AssemblyName? _assemblyName; + +#pragma warning disable IDE0290 // Use primary constructor + public HttpLoggingMiddleware(RequestDelegate next, IOptions options) +#pragma warning restore IDE0290 // Use primary constructor + { + _next = next; + _httpLogConfig = options.Value ?? new HttpLogConfig(); + _assembly = Assembly.GetEntryAssembly(); + _assemblyName = _assembly?.GetName(); + } + + public async Task Invoke(HttpContext context, IHttpLogService httpLogService, ISetupService setupService) + { + if (!_httpLogConfig.Enable || !await setupService.IsInitialized()) + { + await _next(context); + return; + } + + var stopwatch = Stopwatch.StartNew(); + Exception? exception = null; + + if (!_httpLogConfig.EnableResponseBody) + { + try + { + await _next(context); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + stopwatch.Stop(); + + var httpLog = new HttpLog(); + + // Create a list of tasks to run simultaneously + var tasks = new[] + { + FillHttpLog(httpLog, context, stopwatch), + FillRequest(httpLog, context.Request), + FillResponse(httpLog, context.Response, null), + FillException(httpLog, context, exception) + }; + + // Wait for all tasks to complete + await Task.WhenAll(tasks); + httpLogService.Log(httpLog); + } + } + + if (_httpLogConfig.EnableResponseBody) + { + + var originalResponseStream = context.Response.Body; + + await using var responseMemoryStream = new MemoryStream(); + context.Response.Body = responseMemoryStream; + + try + { + await _next(context); + } + catch (Exception ex) + { + exception = ex; + throw; + } + finally + { + stopwatch.Stop(); + + var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); + + var httpLog = new HttpLog(); + + // Create a list of tasks to run simultaneously + var tasks = new[] + { + FillHttpLog(httpLog, context, stopwatch), + FillRequest(httpLog, context.Request), + FillResponse(httpLog, context.Response, responseBody), + FillException(httpLog, context, exception) + }; + + // Wait for all tasks to complete + await Task.WhenAll(tasks); + httpLogService.Log(httpLog); + } + } + } + + private async Task ReadRequestBody(HttpRequest request) + { + if (!_httpLogConfig.EnableRequestBody) + return string.Empty; + + request.EnableBuffering(); + + using var reader = new StreamReader(request.Body, encoding: Encoding.UTF8, detectEncodingFromByteOrderMarks: false, leaveOpen: true); + var requestBody = await reader.ReadToEndAsync(); + request.Body.Position = 0; + + return requestBody; + } + + private static async Task ReadResponseBody(HttpContext context, Stream originalResponseStream, Stream? memoryStream) + { + if (memoryStream == null) + return string.Empty; + + memoryStream.Position = 0; + using var reader = new StreamReader(memoryStream, encoding: Encoding.UTF8); + var responseBody = await reader.ReadToEndAsync(); + memoryStream.Position = 0; + await memoryStream.CopyToAsync(originalResponseStream); + context.Response.Body = originalResponseStream; + + return responseBody; + } + + private async Task FillHttpLog(HttpLog httpLog, HttpContext context, Stopwatch stopwatch) + { + var thread = Thread.CurrentThread; + var process = Process.GetCurrentProcess(); + var apiContext = context.RequestServices.GetRequiredService(); + + httpLog.StatusCode = context.Response.StatusCode; + httpLog.Duration = stopwatch.ElapsedMilliseconds; + httpLog.AssemblyName = _assemblyName?.Name ?? string.Empty; + httpLog.AssemblyVersion = _assemblyName?.Version?.ToString() ?? string.Empty; + httpLog.ProcessId = process.Id; + httpLog.ProcessName = process.ProcessName; + httpLog.ThreadId = thread.ManagedThreadId; + httpLog.MemoryUsage = process.PrivateMemorySize64; + httpLog.MachineName = Environment.MachineName; + httpLog.EnvironmentName = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT") ?? string.Empty; + httpLog.EnvironmentUserName = Environment.UserName; + httpLog.ApiTokenKey = apiContext.ApiTokenKey; + httpLog.IsAuthenticated = apiContext.IsAuthenticated; + httpLog.Language = apiContext.Language; + httpLog.SessionId = apiContext.SessionId; + httpLog.StartDate = apiContext.StartDate; + httpLog.TraceId = apiContext.TraceId; + httpLog.UniqueId = apiContext.UniqueId; + httpLog.UserId = apiContext.UserId; + httpLog.UserIp = apiContext.UserIp; + httpLog.Username = apiContext.Username; + + await Task.CompletedTask; + } + + private async Task FillRequest(HttpLog httpLog, HttpRequest request) + { + var requestBody = _httpLogConfig.EnableRequestBody ? await ReadRequestBody(request) : null; + + httpLog.ReqUrl = request.GetDisplayUrl(); + httpLog.ReqProtocol = request.Protocol; + httpLog.ReqMethod = request.Method; + httpLog.ReqScheme = request.Scheme; + httpLog.ReqPathBase = request.PathBase; + httpLog.ReqPath = request.Path; + httpLog.QueryString = request.QueryString.Value ?? string.Empty; + httpLog.ReqContentType = request.ContentType ?? string.Empty; + httpLog.ReqContentLength = request.ContentLength; + httpLog.ReqBody = requestBody; + httpLog.ReqHeaders = request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; + } + + private static async Task FillResponse(HttpLog httpLog, HttpResponse response, string? responseBody) + { + httpLog.ResContentType = response.ContentType ?? string.Empty; + httpLog.ResContentLength = response.ContentLength; + httpLog.ResBody = responseBody; + httpLog.ResHeaders = response.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()) ?? []; + await Task.CompletedTask; + } + private static async Task FillException(HttpLog httpLog, HttpContext context, Exception? exception) + { + exception ??= context.Features.Get()?.Error; + + if (exception == null) + return; + + httpLog.ExData = exception.Data; + httpLog.ExHelpLink = exception.HelpLink; + httpLog.ExHResult = exception.HResult; + httpLog.ExMessage = exception.Message; + httpLog.ExSource = exception.Source; + httpLog.ExStackTrace = exception.StackTrace; + + await Task.CompletedTask; + } +} diff --git a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs index 0defafdab..731aeed48 100644 --- a/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs +++ b/src/Backend/Repositories/FluentCMS.Repositories.LiteDb/HttpLogRepository.cs @@ -4,25 +4,25 @@ public class HttpLogRepository(ILiteDBContext liteDbContext) : IHttpLogRepositor { public async Task CreateMany(IEnumerable httpLogs, CancellationToken cancellationToken = default) { - //cancellationToken.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); - //if (httpLogs == null) - // return; + if (httpLogs == null) + return; - //// Create a list of tasks to insert the HTTP logs into the appropriate collection. - //var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) - // .Select(async group => - // { - // // Get the collection name from the group. - // var collectionName = group.Key; - // // Get the collection from the database. - // var collection = liteDbContext.Database.GetCollection(collectionName); - // // Insert the HTTP logs into the collection. - // await collection.InsertBulkAsync(group); - // }); + // Create a list of tasks to insert the HTTP logs into the appropriate collection. + var tasks = httpLogs.GroupBy(log => GetCollectionName(log.StatusCode)) + .Select(async group => + { + // Get the collection name from the group. + var collectionName = group.Key; + // Get the collection from the database. + var collection = liteDbContext.Database.GetCollection(collectionName); + // Insert the HTTP logs into the collection. + await collection.InsertBulkAsync(group); + }); - //// Wait for all tasks to complete. - //await Task.WhenAll(tasks); + // Wait for all tasks to complete. + await Task.WhenAll(tasks); } private static string GetCollectionName(int statusCode) => statusCode switch From c07d0780914887a73e0bf87c73427c9f1b6320ea Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sun, 1 Dec 2024 12:22:11 -0500 Subject: [PATCH 22/25] Refactor HttpLogConfig and update logging classes Updated BackgroundHttpLogProcessor and HttpLogChannel to use IOptions for configuration settings from appsettings.json. Modified ServiceExtensions to bind HttpLogConfig settings and removed binding from ApiServiceExtensions. Moved HttpLogConfig to FluentCMS.Services namespace and added BatchSize property. Updated appsettings.json to include BatchSize. --- .../Logging/BackgroundHttpLogProcessor.cs | 11 +++++++---- .../FluentCMS.Services/Logging/HttpLogChannel.cs | 10 ++++++++-- .../Logging}/HttpLogConfig.cs | 3 ++- src/Backend/FluentCMS.Services/ServiceExtensions.cs | 1 + .../Extentions/ApiServiceExtensions.cs | 3 --- src/FluentCMS/appsettings.json | 3 ++- 6 files changed, 20 insertions(+), 11 deletions(-) rename src/Backend/{FluentCMS.Web.Api/Middleware => FluentCMS.Services/Logging}/HttpLogConfig.cs (71%) diff --git a/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs b/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs index e63abd32f..8cec31b1c 100644 --- a/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs +++ b/src/Backend/FluentCMS.Services/Logging/BackgroundHttpLogProcessor.cs @@ -1,15 +1,18 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; namespace FluentCMS.Services; -public class BackgroundHttpLogProcessor(IHttpLogChannel logChannel, IServiceProvider serviceProvider) : BackgroundService +public class BackgroundHttpLogProcessor(IHttpLogChannel logChannel, IServiceProvider serviceProvider, IOptions options) : BackgroundService { - // TODO: move this to HttpLogConfig being read from appsettings.json - private readonly int _batchSize = 50; // Number of logs per batch + private readonly HttpLogConfig _config = options.Value ?? new HttpLogConfig(); protected override async Task ExecuteAsync(CancellationToken stoppingToken) { + if (!_config.Enable) + return; + var batch = new List(); try @@ -19,7 +22,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) batch.Add(log); // Process the batch when the size reaches the limit - if (batch.Count >= _batchSize) + if (batch.Count >= _config.BatchSize) { await ProcessBatchAsync(batch, stoppingToken); batch.Clear(); diff --git a/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs index ccc4e7798..0aad41f38 100644 --- a/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs +++ b/src/Backend/FluentCMS.Services/Logging/HttpLogChannel.cs @@ -1,4 +1,5 @@ -using System.Threading.Channels; +using Microsoft.Extensions.Options; +using System.Threading.Channels; namespace FluentCMS.Services; @@ -11,14 +12,19 @@ public interface IHttpLogChannel public class HttpLogChannel : IHttpLogChannel { private readonly Channel _logChannel; + private readonly HttpLogConfig _config; - public HttpLogChannel() + public HttpLogChannel(IOptions options) { _logChannel = Channel.CreateUnbounded(); + _config = options.Value ?? new HttpLogConfig(); } public void Write(HttpLog httpLog) { + if (!_config.Enable) + return; + if (!_logChannel.Writer.TryWrite(httpLog)) { // Handle overflow if needed diff --git a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs b/src/Backend/FluentCMS.Services/Logging/HttpLogConfig.cs similarity index 71% rename from src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs rename to src/Backend/FluentCMS.Services/Logging/HttpLogConfig.cs index 7177c5989..d0ec79540 100644 --- a/src/Backend/FluentCMS.Web.Api/Middleware/HttpLogConfig.cs +++ b/src/Backend/FluentCMS.Services/Logging/HttpLogConfig.cs @@ -1,8 +1,9 @@ -namespace FluentCMS.Web.Api.Middleware; +namespace FluentCMS.Services; public class HttpLogConfig { public bool Enable { get; set; } = false; public bool EnableRequestBody { get; set; } = false; public bool EnableResponseBody { get; set; } = false; + public int BatchSize { get; set; } = 20; } diff --git a/src/Backend/FluentCMS.Services/ServiceExtensions.cs b/src/Backend/FluentCMS.Services/ServiceExtensions.cs index 347044cab..36fd12de5 100644 --- a/src/Backend/FluentCMS.Services/ServiceExtensions.cs +++ b/src/Backend/FluentCMS.Services/ServiceExtensions.cs @@ -13,6 +13,7 @@ public static IServiceCollection AddApplicationServices(this IServiceCollection services.AddScoped(); services.AddScoped(); + services.AddOptions().BindConfiguration("HttpLogging"); services.AddSingleton(); services.AddHostedService(); diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index e555dceda..b9ca55954 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -18,9 +18,6 @@ public static class ApiServiceExtensions { public static IServiceCollection AddApiServices(this IServiceCollection services) { - services.AddOptions() - .BindConfiguration("HttpLogging"); - services.AddApplicationServices(); services diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index ca18ec7ba..aeee0ed5a 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -39,6 +39,7 @@ "HttpLogging": { "Enable": true, "EnableRequestBody": false, - "EnableResponseBody": false + "EnableResponseBody": false, + "BatchSize": 50 } } From a2e7cf4f23fdd3c2b325e636be900f488ed2cf33 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Sun, 1 Dec 2024 12:26:51 -0500 Subject: [PATCH 23/25] Update HTTP logging configuration documentation Replaced the `HttpLogConfig` class configuration with a new `HttpLogging` section in the `appsettings.json` file. Added a table detailing the new configuration properties, including `Enable`, `EnableRequestBody`, `EnableResponseBody`, and `BatchSize`. Updated the example JSON configuration to reflect these changes. --- docs/Logging.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/Logging.md b/docs/Logging.md index 3cdb91f6d..4bc7b1af7 100644 --- a/docs/Logging.md +++ b/docs/Logging.md @@ -21,25 +21,25 @@ The HTTP Logging Middleware `HttpLoggingMiddleware.cs` is responsible for captur --- -## Configuring `HttpLogConfig` - -The `HttpLogConfig` class allows you to customize the logging behavior. It can be configured via the application's `appsettings.json` or other configuration providers. - ### Configuration Properties -- `Enable` (bool): Toggles the logging functionality. Default is `false`. -- `EnableRequestBody` (bool): Enables or disables logging of the HTTP request body. Default is `false`. -- `EnableResponseBody` (bool): Enables or disables logging of the HTTP response body. Default is `false`. +The `HttpLogging` section in the appsettings.json file allows you to customize the behavior of the HTTP logging middleware and background processor. Below are the available configuration properties: + +| Property | Type | Default Value | Description | +|------------------------|---------|---------------|-----------------------------------------------------------------------------| +| `Enable` | `bool` | `false` | Toggles the logging functionality on or off. | +| `EnableRequestBody` | `bool` | `false` | Enables or disables logging of the HTTP request body. | +| `EnableResponseBody` | `bool` | `false` | Enables or disables logging of the HTTP response body. | +| `BatchSize` | `int` | `50` | Specifies the number of log entries to process in a single batch. | ### Example `appsettings.json` Configuration ```json -{ - "HttpLogConfig": { +"HttpLogging": { "Enable": true, - "EnableRequestBody": true, - "EnableResponseBody": true - } + "EnableRequestBody": false, + "EnableResponseBody": false, + "BatchSize": 50 } ``` From 20fb5343e38810b2885cd17765ca1d1dcf327cce Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Thu, 5 Dec 2024 00:47:28 -0500 Subject: [PATCH 24/25] Refactor API context and add new API result classes Refactored ApiExecutionContext to use a new constant for the API auth header key. Updated ApiResultActionFilter and ApiResultExceptionFilter to use constructor injection for IApiExecutionContext. Introduced new interfaces and classes for API results, including IApiPagingResult, ApiPagingResult, IApiResult, and ApiResult. Updated appsettings.json for database configuration and logging. Added new PersonController and Person entity. Removed AppApiClientException and added AppError and AppException classes. --- .../FluentCMS.Web.Api/ApiExecutionContext.cs | 3 +- .../FluentCMS.Web.Api/Controllers/_Test.cs | 107 ++++++++++++++++++ .../Filters/ApiResultActionFilter.cs | 16 +-- .../Filters/ApiResultExceptionFilter.cs | 17 +-- .../Models/ApiPagingResult.cs | 52 ++++++++- .../FluentCMS.Web.Api/Models/ApiResult.cs | 68 +++++++++++ src/FluentCMS/appsettings.json | 10 +- .../Exceptions/AppApiClientException.cs | 8 -- src/Shared/Exceptions/AppError.cs | 21 ++++ src/Shared/Exceptions/AppException.cs | 35 ++++++ 10 files changed, 298 insertions(+), 39 deletions(-) create mode 100644 src/Backend/FluentCMS.Web.Api/Controllers/_Test.cs delete mode 100644 src/Shared/Exceptions/AppApiClientException.cs diff --git a/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs b/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs index cfc98f94a..1319645a9 100644 --- a/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs +++ b/src/Backend/FluentCMS.Web.Api/ApiExecutionContext.cs @@ -15,6 +15,7 @@ public class ApiExecutionContext : IApiExecutionContext public const string SESSION_ID_HEADER_KEY = "X_Session_Id"; public const string UNIQUE_USER_ID_HEADER_KEY = "X-Unique-Id"; public const string DEFAULT_LANGUAGE = "en-US"; + public const string API_AUTH_HEADER = "X-API-AUTH"; // Properties to store various contextual information public string TraceId { get; } = string.Empty; // Unique identifier for the current request @@ -63,7 +64,7 @@ public ApiExecutionContext(IHttpContextAccessor accessor) IsAuthenticated = user.Identity?.IsAuthenticated ?? false; // extract the API token key from the request headers - ApiTokenKey = context.Request?.Headers?.FirstOrDefault(_ => _.Key.Equals("X-API-AUTH", StringComparison.OrdinalIgnoreCase)).Value.ToString() ?? string.Empty; + ApiTokenKey = context.Request?.Headers?.FirstOrDefault(_ => _.Key.Equals(API_AUTH_HEADER, StringComparison.OrdinalIgnoreCase)).Value.ToString() ?? string.Empty; } } } diff --git a/src/Backend/FluentCMS.Web.Api/Controllers/_Test.cs b/src/Backend/FluentCMS.Web.Api/Controllers/_Test.cs new file mode 100644 index 000000000..32736b4c6 --- /dev/null +++ b/src/Backend/FluentCMS.Web.Api/Controllers/_Test.cs @@ -0,0 +1,107 @@ +namespace FluentCMS.Web.Api.Controllers; + +public class PersonController : BaseGlobalController +{ + private readonly List _persons = + [ + new() { Id=1, FirstName = "John", LastName = "Doe", Age = 30 }, + new() { Id=2, FirstName = "Jane", LastName = "Doe", Age = 25 }, + new() { Id=3, FirstName = "Alice", LastName = "Smith", Age = 35 }, + new() { Id=4, FirstName = "Bob", LastName = "Smith", Age = 40 }, + ]; + + [HttpGet] + public async Task> GetAll(CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + return OkPaged(_persons); + } + + [HttpPost] + public async Task> Create([FromBody] Person person, CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + person.Id = _persons.Max(p => p.Id) + 1; + _persons.Add(person); + return Ok(person); + } + + [HttpPost] + public async Task> Update([FromBody] Person person, CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + var existingPerson = _persons.FirstOrDefault(p => p.Id == person.Id); + if (existingPerson == null) + { + return Ok(person); + } + existingPerson.FirstName = person.FirstName; + existingPerson.LastName = person.LastName; + existingPerson.Age = person.Age; + return Ok(existingPerson); + } + + [HttpGet] + public async Task> GetAllRaw(CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + return _persons; + } + + [HttpPost] + public async Task CreateRaw([FromBody] Person person, CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + person.Id = _persons.Max(p => p.Id) + 1; + _persons.Add(person); + return person; + } + + [HttpPost] + public async Task UpdateRaw([FromBody] Person person, CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + var existingPerson = _persons.FirstOrDefault(p => p.Id == person.Id); + if (existingPerson == null) + { + return person; + } + existingPerson.FirstName = person.FirstName; + existingPerson.LastName = person.LastName; + existingPerson.Age = person.Age; + return existingPerson; + } + + // status 500, example of a method that throw an exception + [HttpGet] + public async Task> GetException500(CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + throw new Exception("This is an exception"); + } + + // status 500, example of a method that throw an exception + [HttpGet] + public async Task GetException500Raw(CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + throw new Exception("This is an exception"); + } + + // status 404, example of a method that throw an exception + [HttpGet] + public async Task GetException404Raw(CancellationToken cancellationToken = default) + { + await Task.Delay(100, cancellationToken); + return new NotFoundResult(); + } +} + +public class Person +{ + [Required] + public int Id { get; set; } + public string FirstName { get; set; } = default!; + public string LastName { get; set; } = default!; + public int Age { get; set; } +} diff --git a/src/Backend/FluentCMS.Web.Api/Filters/ApiResultActionFilter.cs b/src/Backend/FluentCMS.Web.Api/Filters/ApiResultActionFilter.cs index 851bccf3e..fa7338588 100644 --- a/src/Backend/FluentCMS.Web.Api/Filters/ApiResultActionFilter.cs +++ b/src/Backend/FluentCMS.Web.Api/Filters/ApiResultActionFilter.cs @@ -2,14 +2,8 @@ namespace FluentCMS.Web.Api.Filters; -public class ApiResultActionFilter : IAsyncActionFilter +public class ApiResultActionFilter(IApiExecutionContext apiExecutionContext) : IAsyncActionFilter { - private readonly IApiExecutionContext _apiExecutionContext; - - public ApiResultActionFilter(IApiExecutionContext apiExecutionContext) - { - _apiExecutionContext = apiExecutionContext; - } public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) { // Execute the action @@ -25,10 +19,10 @@ public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionE return; var apiResult = (IApiResult)value; - apiResult.Duration = (DateTime.UtcNow - _apiExecutionContext.StartDate).TotalMilliseconds; - apiResult.SessionId = _apiExecutionContext.SessionId; - apiResult.TraceId = _apiExecutionContext.TraceId; - apiResult.UniqueId = _apiExecutionContext.UniqueId; + apiResult.Duration = (DateTime.UtcNow - apiExecutionContext.StartDate).TotalMilliseconds; + apiResult.SessionId = apiExecutionContext.SessionId; + apiResult.TraceId = apiExecutionContext.TraceId; + apiResult.UniqueId = apiExecutionContext.UniqueId; apiResult.Status = 200; apiResult.IsSuccess = true; } diff --git a/src/Backend/FluentCMS.Web.Api/Filters/ApiResultExceptionFilter.cs b/src/Backend/FluentCMS.Web.Api/Filters/ApiResultExceptionFilter.cs index 5ac8a39c9..cccf86d61 100644 --- a/src/Backend/FluentCMS.Web.Api/Filters/ApiResultExceptionFilter.cs +++ b/src/Backend/FluentCMS.Web.Api/Filters/ApiResultExceptionFilter.cs @@ -2,15 +2,8 @@ namespace FluentCMS.Web.Api.Filters; -public class ApiResultExceptionFilter : IExceptionFilter +public class ApiResultExceptionFilter(IApiExecutionContext apiExecutionContext) : IExceptionFilter { - private readonly IApiExecutionContext _apiExecutionContext; - - public ApiResultExceptionFilter(IApiExecutionContext apiExecutionContext) - { - _apiExecutionContext = apiExecutionContext; - } - public void OnException(ExceptionContext context) { if (!context.ActionDescriptor.IsApiResultType()) @@ -18,10 +11,10 @@ public void OnException(ExceptionContext context) var apiResult = new ApiResult { - Duration = (DateTime.UtcNow - _apiExecutionContext.StartDate).TotalMilliseconds, - SessionId = _apiExecutionContext.SessionId, - TraceId = _apiExecutionContext.TraceId, - UniqueId = _apiExecutionContext.UniqueId, + Duration = (DateTime.UtcNow - apiExecutionContext.StartDate).TotalMilliseconds, + SessionId = apiExecutionContext.SessionId, + TraceId = apiExecutionContext.TraceId, + UniqueId = apiExecutionContext.UniqueId, Status = 500, IsSuccess = false, }; diff --git a/src/Backend/FluentCMS.Web.Api/Models/ApiPagingResult.cs b/src/Backend/FluentCMS.Web.Api/Models/ApiPagingResult.cs index 578a9ebf1..d5972ca3d 100644 --- a/src/Backend/FluentCMS.Web.Api/Models/ApiPagingResult.cs +++ b/src/Backend/FluentCMS.Web.Api/Models/ApiPagingResult.cs @@ -1,29 +1,77 @@ namespace FluentCMS.Web.Api.Models; +/// +/// Represents a paginated result of an API call, with metadata about the pagination. +/// +/// The type of the data contained in the paginated result. public interface IApiPagingResult : IApiResult> { + /// + /// Gets the number of items per page. + /// int PageSize { get; } + + /// + /// Gets the total number of pages. + /// int TotalPages { get; } + + /// + /// Gets the current page number. + /// int PageNumber { get; } + + /// + /// Gets the total number of items across all pages. + /// long TotalCount { get; } + + /// + /// Gets a value indicating whether there is a previous page. + /// bool HasPrevious { get; } + + /// + /// Gets a value indicating whether there is a next page. + /// bool HasNext { get; } } +/// +/// Provides a concrete implementation of the interface. +/// +/// The type of the data contained in the paginated result. public class ApiPagingResult : ApiResult>, IApiPagingResult { + /// public int PageSize { get; } + + /// public int TotalPages { get; } + + /// public int PageNumber { get; } + + /// public long TotalCount { get; } + + /// public bool HasPrevious { get; } + + /// public bool HasNext { get; } + + /// + /// Initializes a new instance of the class with the specified data. + /// + /// The paginated data. public ApiPagingResult(IEnumerable data) : base(data) { - // Initialize properties based on the data - PageNumber = 1; + // Initialize properties based on the provided data. + PageNumber = 1; // Assuming default value for demonstration. PageSize = data.Count(); TotalCount = data.Count(); + TotalPages = (int)Math.Ceiling((double)TotalCount / PageSize); HasPrevious = PageNumber > 1; HasNext = PageNumber < TotalPages; } diff --git a/src/Backend/FluentCMS.Web.Api/Models/ApiResult.cs b/src/Backend/FluentCMS.Web.Api/Models/ApiResult.cs index ff4065994..dc25382f3 100644 --- a/src/Backend/FluentCMS.Web.Api/Models/ApiResult.cs +++ b/src/Backend/FluentCMS.Web.Api/Models/ApiResult.cs @@ -1,35 +1,103 @@ namespace FluentCMS.Web.Api.Models; +/// +/// Represents the result of an API call. +/// public interface IApiResult { + /// + /// Gets the list of errors associated with the API result. + /// List Errors { get; } + + /// + /// Gets or sets the unique trace identifier for tracking the request. + /// string TraceId { get; set; } + + /// + /// Gets or sets the session identifier associated with the request. + /// string SessionId { get; set; } + + /// + /// Gets or sets the unique identifier for this specific API result. + /// string UniqueId { get; set; } + + /// + /// Gets or sets the duration of the request in milliseconds. + /// double Duration { get; set; } + + /// + /// Gets or sets the HTTP status code of the API result. + /// int Status { get; set; } + + /// + /// Gets or sets a value indicating whether the API call was successful. + /// bool IsSuccess { get; set; } } + +/// +/// Represents the result of an API call with a specific data type. +/// +/// The type of the data returned by the API call. public interface IApiResult : IApiResult { + /// + /// Gets the data returned by the API call. + /// TData? Data { get; } } +/// +/// Provides a concrete implementation of the interface. +/// +/// The type of the data returned by the API call. public class ApiResult : IApiResult { + /// + /// Gets the list of errors associated with the API result. + /// public List Errors { get; } = []; + + /// public string TraceId { get; set; } = string.Empty; + + /// public string SessionId { get; set; } = string.Empty; + + /// public string UniqueId { get; set; } = string.Empty; + + /// public double Duration { get; set; } + + /// public int Status { get; set; } + + /// + /// Gets the data returned by the API call. + /// public TData? Data { get; } + + /// public bool IsSuccess { get; set; } = true; + /// + /// Initializes a new instance of the class. + /// public ApiResult() { } + /// + /// Initializes a new instance of the class with the specified data. + /// + /// The data returned by the API call. public ApiResult(TData data) { Data = data; diff --git a/src/FluentCMS/appsettings.json b/src/FluentCMS/appsettings.json index aeee0ed5a..4999ff7c6 100644 --- a/src/FluentCMS/appsettings.json +++ b/src/FluentCMS/appsettings.json @@ -1,7 +1,7 @@ { - "Database": "LiteDb", + "Database": "mongodb", "ConnectionStrings": { - "DefaultConnection": "Filename=./fluentcms.db" + "DefaultConnection": "mongodb://localhost:27017/fluentcms" }, "ClientSettings": { "Url": "http://localhost:5000", @@ -38,8 +38,8 @@ }, "HttpLogging": { "Enable": true, - "EnableRequestBody": false, - "EnableResponseBody": false, - "BatchSize": 50 + "EnableRequestBody": true, + "EnableResponseBody": true, + "BatchSize": 1 } } diff --git a/src/Shared/Exceptions/AppApiClientException.cs b/src/Shared/Exceptions/AppApiClientException.cs deleted file mode 100644 index 4393f4587..000000000 --- a/src/Shared/Exceptions/AppApiClientException.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace FluentCMS; - -public class AppApiClientException : AppException -{ - public AppApiClientException() : base("ApiClient") - { - } -} diff --git a/src/Shared/Exceptions/AppError.cs b/src/Shared/Exceptions/AppError.cs index cc70076e8..2e4de3ada 100644 --- a/src/Shared/Exceptions/AppError.cs +++ b/src/Shared/Exceptions/AppError.cs @@ -1,19 +1,40 @@ namespace FluentCMS; +/// +/// Represents an error associated with an API call. +/// public class AppError { + /// + /// Gets or sets the error code. + /// public string Code { get; set; } = string.Empty; + + /// + /// Gets or sets the description of the error. + /// public string Description { get; set; } = string.Empty; + /// + /// Initializes a new instance of the class. + /// public AppError() { } + /// + /// Initializes a new instance of the class with the specified error code. + /// + /// The error code. public AppError(string code) { Code = code; } + /// + /// Returns a string representation of the . + /// + /// A string containing the error code and description. public override string ToString() { return $"{Code}-{Description}"; diff --git a/src/Shared/Exceptions/AppException.cs b/src/Shared/Exceptions/AppException.cs index a1cf174c3..b57972f26 100644 --- a/src/Shared/Exceptions/AppException.cs +++ b/src/Shared/Exceptions/AppException.cs @@ -2,36 +2,71 @@ namespace FluentCMS; +/// +/// Represents a custom application exception with additional context, such as the source method and type, and associated errors. +/// public class AppException : ApplicationException { + /// + /// Gets or sets the name of the type where the exception occurred. + /// public string? TypeName { get; set; } + + /// + /// Gets or sets the name of the method where the exception occurred. + /// public string? MethodName { get; set; } + + /// + /// Gets the list of errors associated with this exception. + /// public List Errors { get; } = []; + /// + /// Initializes a new instance of the class with the specified error code. + /// + /// The error code associated with this exception. public AppException(string code) : base(code) { Errors.Add(new AppError(code)); CaptureExceptionSource(); } + /// + /// Initializes a new instance of the class with the specified error code and inner exception. + /// + /// The error code associated with this exception. + /// The inner exception that caused this exception. public AppException(string code, Exception? innerException) : base(code, innerException) { Errors.Add(new AppError(code)); CaptureExceptionSource(); } + /// + /// Initializes a new instance of the class with a collection of error codes. + /// + /// The error codes associated with this exception. public AppException(IEnumerable codes) : base(string.Empty) { Errors.AddRange(codes.Select(c => new AppError(c))); CaptureExceptionSource(); } + /// + /// Initializes a new instance of the class with a collection of error codes and an inner exception. + /// + /// The error codes associated with this exception. + /// The inner exception that caused this exception. public AppException(IEnumerable codes, Exception? innerException) : base(string.Empty, innerException) { Errors.AddRange(codes.Select(c => new AppError(c))); CaptureExceptionSource(); } + /// + /// Captures the source type and method where the exception occurred. + /// private void CaptureExceptionSource() { var stackTrace = new StackTrace(this, true); From a07b14e7dc0943129705d599a3d93ac7a9b0b834 Mon Sep 17 00:00:00 2001 From: Amir Pournasserian Date: Thu, 5 Dec 2024 00:59:40 -0500 Subject: [PATCH 25/25] Refactor ApiServiceExtensions DI and using directives Added using directive for FluentCMS.Services.Models and removed Microsoft.AspNetCore.Http directive. Simplified IApiExecutionContext registration in the DI container from a lambda expression to a direct type mapping. --- .../FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs index b9ca55954..dae768a3a 100644 --- a/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs +++ b/src/Backend/FluentCMS.Web.Api/Extentions/ApiServiceExtensions.cs @@ -5,7 +5,6 @@ using FluentCMS.Web.Api.Middleware; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.Tokens; using System.Security.Cryptography; @@ -86,7 +85,7 @@ public static IServiceCollection AddApiServices(this IServiceCollection services services.AddHttpContextAccessor(); - services.AddScoped(sp => new ApiExecutionContext(sp.GetRequiredService())); + services.AddScoped(); services.AddAutoMapper(typeof(ApiServiceExtensions));