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