diff --git a/src/Examples/Identity.MongoDB.API/Controllers/Base/AccountsControllerBase.cs b/src/Examples/Identity.MongoDB.API/Controllers/Base/AccountsControllerBase.cs index 00ee934..ce66bc8 100644 --- a/src/Examples/Identity.MongoDB.API/Controllers/Base/AccountsControllerBase.cs +++ b/src/Examples/Identity.MongoDB.API/Controllers/Base/AccountsControllerBase.cs @@ -21,6 +21,12 @@ public virtual async Task> Register([FromBody] RegisterRequest mod return true.ToResult(); } + [HttpGet] + public virtual async Task> Test(int id, string name) + { + return (await Task.FromResult(true)).ToResult(); + } + [HttpPost] public virtual async Task>> Login([FromBody] LoginRequest model, CancellationToken cancellationToken = default) { diff --git a/src/Examples/Identity.MongoDB.API/Program.cs b/src/Examples/Identity.MongoDB.API/Program.cs index 133eca4..98c6eb9 100644 --- a/src/Examples/Identity.MongoDB.API/Program.cs +++ b/src/Examples/Identity.MongoDB.API/Program.cs @@ -15,10 +15,11 @@ //builder.Services.AddDefaultBsonSerializers(); // Adding http logging +builder.Services.AddHttpLogServices(); builder.Services.AddMongoDbHttpLogging("HttpLoggingConnection", builder.Configuration.GetInstance("HttpLogging")); builder.Services.AddHttpContextAccessor(); -builder.Services.AddControllers(); +builder.Services.AddControllers(options => options.Filters.Add()); builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly()); // Disabling automatic model state validation diff --git a/src/Examples/Identity.MongoDB.API/ViewModels/UserLogin.cs b/src/Examples/Identity.MongoDB.API/ViewModels/UserLogin.cs index 2919a1c..f76b100 100644 --- a/src/Examples/Identity.MongoDB.API/ViewModels/UserLogin.cs +++ b/src/Examples/Identity.MongoDB.API/ViewModels/UserLogin.cs @@ -7,8 +7,7 @@ public class LoginRequest [Required] public string UserName { get; set; } - [Required] - [DataType(DataType.Password)] + [Required, DataType(DataType.Password), LogReplaceValue("***")] public string Password { get; set; } } @@ -20,4 +19,4 @@ public class LoginResponse public string Token { get; set; } public string RefreshToken { get; set; } public DateTime Expiry { get; set; } -} \ No newline at end of file +} diff --git a/src/Examples/Identity.MongoDB.API/ViewModels/UserRegister.cs b/src/Examples/Identity.MongoDB.API/ViewModels/UserRegister.cs index 70eb5df..28e94e3 100644 --- a/src/Examples/Identity.MongoDB.API/ViewModels/UserRegister.cs +++ b/src/Examples/Identity.MongoDB.API/ViewModels/UserRegister.cs @@ -2,6 +2,7 @@ namespace API; +[LogIgnore] public class RegisterRequest { [Required] diff --git a/src/Logging/uBeac.Core.Web.Logging/Attributes/LogIgnoreAttribute.cs b/src/Logging/uBeac.Core.Web.Logging/Attributes/LogIgnoreAttribute.cs new file mode 100644 index 0000000..79cbcf1 --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/Attributes/LogIgnoreAttribute.cs @@ -0,0 +1,6 @@ +namespace uBeac; + +[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class LogIgnoreAttribute : Attribute +{ +} \ No newline at end of file diff --git a/src/Logging/uBeac.Core.Web.Logging/Attributes/LogReplaceValueAttribute.cs b/src/Logging/uBeac.Core.Web.Logging/Attributes/LogReplaceValueAttribute.cs new file mode 100644 index 0000000..04cce6a --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/Attributes/LogReplaceValueAttribute.cs @@ -0,0 +1,12 @@ +namespace uBeac; + +[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)] +public class LogReplaceValueAttribute : Attribute +{ + public LogReplaceValueAttribute(object value) + { + Value = value; + } + + public object Value { get; set; } +} \ No newline at end of file diff --git a/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLogDataHandlingFilter.cs b/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLogDataHandlingFilter.cs new file mode 100644 index 0000000..f295818 --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLogDataHandlingFilter.cs @@ -0,0 +1,92 @@ +using System.Reflection; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Mvc.Filters; +using Newtonsoft.Json; +using Newtonsoft.Json.Serialization; + +namespace uBeac.Web.Logging; + +public class HttpLogDataHandlingFilter : IActionFilter, IOrderedFilter +{ + private readonly IHttpLogChanges _httpLogChanges; + + public HttpLogDataHandlingFilter(IHttpLogChanges httpLogChanges) + { + _httpLogChanges = httpLogChanges; + } + private readonly JsonSerializerSettings _serializationSettings = new() + { + ContractResolver = new JsonLogResolver(), + Formatting = Formatting.Indented + }; + + private bool _logIgnored; + + public void OnActionExecuting(ActionExecutingContext context) + { + var ignoredController = context.Controller.GetType().IsIgnored(); + var ignoredAction = ((ControllerActionDescriptor)context.ActionDescriptor).MethodInfo.IsIgnored(); + _logIgnored = ignoredController || ignoredAction; + _httpLogChanges.Add(LogConstants.LOG_IGNORED, _logIgnored); + if (_logIgnored) return; + + var requestArgs = context.ActionArguments.Where(arg => arg.Value != null && arg.Value.GetType() != typeof(CancellationToken)).ToList(); + var logRequestBody = JsonConvert.SerializeObject(requestArgs, _serializationSettings); + + _httpLogChanges.Add(LogConstants.REQUEST_BODY, logRequestBody); + } + + public void OnActionExecuted(ActionExecutedContext context) + { + if (_logIgnored) return; + + if (context.Result == null || context.Result.GetType() == typeof(EmptyResult)) + { + var emptyResult = JsonConvert.SerializeObject(new { }, _serializationSettings); + _httpLogChanges.Add(LogConstants.RESPONSE_BODY, emptyResult); + return; + } + + var resultValue = ((ObjectResult)context.Result).Value; + var logResponseBody = resultValue != null ? JsonConvert.SerializeObject(resultValue, _serializationSettings) : null; + + _httpLogChanges.Add(LogConstants.RESPONSE_BODY, logResponseBody); + } + + public int Order => int.MaxValue; +} + +internal class JsonLogResolver : CamelCasePropertyNamesContractResolver +{ + protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) + { + var result = base.CreateProperty(member, memberSerialization); + + // Ignore value + result.Ignored = member.IsIgnored(); + if (result.Ignored) return result; + + // Replace value + if (member.HasReplaceValue()) + { + var replaceValue = member.GetReplaceValue(); + result.ValueProvider = new ReplaceValueProvider(replaceValue); + } + + return result; + } +} + +internal class ReplaceValueProvider : IValueProvider +{ + private readonly object _replaceValue; + + public ReplaceValueProvider(object replaceValue) + { + _replaceValue = replaceValue; + } + + public void SetValue(object target, object value) { } + public object GetValue(object target) => _replaceValue; +} \ No newline at end of file diff --git a/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLoggingExtensions.cs b/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLoggingExtensions.cs new file mode 100644 index 0000000..8abe936 --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/Helpers/HttpLoggingExtensions.cs @@ -0,0 +1,28 @@ +using System.Reflection; +using Microsoft.AspNetCore.Diagnostics; +using Microsoft.AspNetCore.Http; + +namespace uBeac.Web.Logging; + +internal static class HttpLoggingExtensions +{ + public static HttpLog CreateLogModel(this HttpContext context, IApplicationContext appContext, 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 = appContext, + Exception = exception == null ? null : new ExceptionModel(exception) + }; + } + + public static bool IsIgnored(this MemberInfo target) => target.GetCustomAttributes(typeof(LogIgnoreAttribute), true).Any(); + + public static bool HasReplaceValue(this MemberInfo target) => target.GetCustomAttributes(typeof(LogReplaceValueAttribute), true).Any(); + public static object GetReplaceValue(this MemberInfo target) => ((LogReplaceValueAttribute)target.GetCustomAttributes(typeof(LogReplaceValueAttribute), true).First()).Value; +} \ No newline at end of file diff --git a/src/Logging/uBeac.Core.Web.Logging/HttpLogChanges.cs b/src/Logging/uBeac.Core.Web.Logging/HttpLogChanges.cs new file mode 100644 index 0000000..7fedd3b --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/HttpLogChanges.cs @@ -0,0 +1,10 @@ +namespace uBeac.Web.Logging +{ + public interface IHttpLogChanges : IDictionary + { + + } + internal class HttpLogChanges : Dictionary, IHttpLogChanges + { + } +} diff --git a/src/Logging/uBeac.Core.Web.Logging/HttpLoggingMiddleware.cs b/src/Logging/uBeac.Core.Web.Logging/HttpLoggingMiddleware.cs index f7198dc..af38419 100644 --- a/src/Logging/uBeac.Core.Web.Logging/HttpLoggingMiddleware.cs +++ b/src/Logging/uBeac.Core.Web.Logging/HttpLoggingMiddleware.cs @@ -1,6 +1,4 @@ using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; namespace uBeac.Web.Logging; @@ -14,18 +12,11 @@ public HttpLoggingMiddleware(RequestDelegate next) _next = next; } - public async Task Invoke(HttpContext context, IHttpLogRepository repository, IApplicationContext appContext, IDebugger debugger) + public async Task Invoke(HttpContext context, IHttpLogRepository repository, IApplicationContext appContext, IDebugger debugger, IHttpLogChanges httpLogChanges) { 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 @@ -39,12 +30,13 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp } finally { - var responseBody = await ReadResponseBody(context, originalResponseStream, responseMemoryStream); - stopwatch.Stop(); - var logModel = CreateLogModel(context, appContext, requestBody, responseBody, stopwatch.ElapsedMilliseconds, exception != null ? 500 : null, exception); - await Log(logModel, repository); + if (!httpLogChanges.ContainsKey(LogConstants.LOG_IGNORED) || httpLogChanges[LogConstants.LOG_IGNORED] is false) + { + var model = context.CreateLogModel(appContext, httpLogChanges[LogConstants.REQUEST_BODY].ToString(), httpLogChanges[LogConstants.RESPONSE_BODY].ToString(), stopwatch.ElapsedMilliseconds, exception != null ? 500 : null, exception); + await Log(model, repository); + } } } catch (Exception ex) @@ -53,46 +45,5 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp } } - 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, IApplicationContext appContext, 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 = appContext, - Exception = exception == null ? null : new ExceptionModel(exception) - }; - } - - private static async Task Log(HttpLog log, IHttpLogRepository repository) - { - await repository.Create(log); - } + private static async Task Log(HttpLog log, IHttpLogRepository repository) => await repository.Create(log); } \ No newline at end of file diff --git a/src/Logging/uBeac.Core.Web.Logging/LogConstants.cs b/src/Logging/uBeac.Core.Web.Logging/LogConstants.cs new file mode 100644 index 0000000..cfc03f1 --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/LogConstants.cs @@ -0,0 +1,8 @@ +namespace uBeac.Web.Logging; + +public class LogConstants +{ + public const string LOG_IGNORED = "LogIgnored"; + public const string REQUEST_BODY = "LogRequestBody"; + public const string RESPONSE_BODY = "LogResponseBody"; +} diff --git a/src/Logging/uBeac.Core.Web.Logging/ServiceExtensions.cs b/src/Logging/uBeac.Core.Web.Logging/ServiceExtensions.cs new file mode 100644 index 0000000..63a6544 --- /dev/null +++ b/src/Logging/uBeac.Core.Web.Logging/ServiceExtensions.cs @@ -0,0 +1,14 @@ + +using uBeac.Web.Logging; + +namespace Microsoft.Extensions.DependencyInjection +{ + public static class ServiceExtensions + { + public static IServiceCollection AddHttpLogServices(this IServiceCollection services) + { + services.AddScoped(); + return services; + } + } +} diff --git a/src/Logging/uBeac.Core.Web.Logging/uBeac.Core.Web.Logging.csproj b/src/Logging/uBeac.Core.Web.Logging/uBeac.Core.Web.Logging.csproj index 67ffff0..4f90eca 100644 --- a/src/Logging/uBeac.Core.Web.Logging/uBeac.Core.Web.Logging.csproj +++ b/src/Logging/uBeac.Core.Web.Logging/uBeac.Core.Web.Logging.csproj @@ -19,10 +19,14 @@ + + + +