Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: Don't log everything in HTTP logs #155

Open
wants to merge 9 commits into
base: development
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ public virtual async Task<IResult<bool>> Register([FromBody] RegisterRequest mod
return true.ToResult();
}

[HttpGet]
public virtual async Task<IResult<bool>> Test(int id, string name)
{
return (await Task.FromResult(true)).ToResult();
}

[HttpPost]
public virtual async Task<IResult<SignInResult<TUserKey>>> Login([FromBody] LoginRequest model, CancellationToken cancellationToken = default)
{
Expand Down
3 changes: 2 additions & 1 deletion src/Examples/Identity.MongoDB.API/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@
//builder.Services.AddDefaultBsonSerializers();

// Adding http logging
builder.Services.AddHttpLogServices();
builder.Services.AddMongoDbHttpLogging<HttpLogMongoDBContext>("HttpLoggingConnection", builder.Configuration.GetInstance<MongoDbHttpLogOptions>("HttpLogging"));

builder.Services.AddHttpContextAccessor();
builder.Services.AddControllers();
builder.Services.AddControllers(options => options.Filters.Add<HttpLogDataHandlingFilter>());
builder.Services.AddAutoMapper(Assembly.GetExecutingAssembly());

// Disabling automatic model state validation
Expand Down
5 changes: 2 additions & 3 deletions src/Examples/Identity.MongoDB.API/ViewModels/UserLogin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}

Expand All @@ -20,4 +19,4 @@ public class LoginResponse
public string Token { get; set; }
public string RefreshToken { get; set; }
public DateTime Expiry { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace API;

[LogIgnore]
public class RegisterRequest
{
[Required]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace uBeac;

[AttributeUsage(AttributeTargets.Property | AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
public class LogIgnoreAttribute : Attribute
{
}
Original file line number Diff line number Diff line change
@@ -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; }
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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<IExceptionHandlerFeature>()?.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;
}
10 changes: 10 additions & 0 deletions src/Logging/uBeac.Core.Web.Logging/HttpLogChanges.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace uBeac.Web.Logging
{
public interface IHttpLogChanges : IDictionary<string, object>
{

}
internal class HttpLogChanges : Dictionary<string, object>, IHttpLogChanges
{
}
}
63 changes: 7 additions & 56 deletions src/Logging/uBeac.Core.Web.Logging/HttpLoggingMiddleware.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Diagnostics;
using System.Text;
using Microsoft.AspNetCore.Diagnostics;
using Microsoft.AspNetCore.Http;

namespace uBeac.Web.Logging;
Expand All @@ -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
Expand All @@ -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)
Expand All @@ -53,46 +45,5 @@ public async Task Invoke(HttpContext context, IHttpLogRepository repository, IAp
}
}

private async Task<string> 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<string> 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<IExceptionHandlerFeature>()?.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);
}
8 changes: 8 additions & 0 deletions src/Logging/uBeac.Core.Web.Logging/LogConstants.cs
Original file line number Diff line number Diff line change
@@ -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";
}
14 changes: 14 additions & 0 deletions src/Logging/uBeac.Core.Web.Logging/ServiceExtensions.cs
Original file line number Diff line number Diff line change
@@ -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<IHttpLogChanges, HttpLogChanges>();
return services;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,14 @@
</ItemGroup>

<ItemGroup>
<FrameworkReference Include="Microsoft.AspNetCore.App" />
<PackageReference Include="Microsoft.AspNetCore.Diagnostics.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Http" Version="2.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Abstractions" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="6.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
</ItemGroup>

<ItemGroup>
Expand Down