I am trying to log all of the requests hitting my endpoints using a delegating handler. As there are sensitive information in the request and response body, I would like to mask them in my logs. I have tried using Destructurama.Attributed and it works when I log the object of the request.
public class LoginRequest
{
[LogMasked(ShowFirst = 3)]
public string LoginName { get; set; }
[NotLogged]
public string Password { get; set; }
}
public class UserController : BaseApiController
{
private readonly ILogger _logger;
public UserController (ILogger logger)
{
_logger = logger;
}
[HttpPost, Route("login")]
public async Task<IHttpActionResult> Login(LoginRequest request)
{
// masking works
_logger.Information("Request: {@request}", request)
// some logic
}
}
However, it does not work in the delegating handler as I only have the JSON string of the requests and responses.
In my delegating handler, I am retrieving the request body using:
var stream = await request.Content.ReadAsStreamAsync();
var reader = new StreamReader(stream);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
var requestJsonString = reader.ReadToEnd();
Is there a way to deserialize these JSON strings into their respective objects based on the endpoint?
public class GlobalLoggingHandler : DelegatingHandler
{
private readonly ILogger _logger;
public GlobalLoggingHandler(ILogger logger)
{
_logger = logger;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var endpoint = string.Join(string.Empty, request.RequestUri.Segments);
var operation = _logger.BeginOperation("HTTP {method} {endpoint}", request.Method.Method, endpoint);
try
{
var response = await base.SendAsync(request, cancellationToken);
Log(request, response);
operation.Complete();
return response;
}
catch (Exception)
{
//to-do: log error
operation.Abandon();
return new HttpResponseMessage(HttpStatusCode.InternalServerError);
}
}
private async void Log(HttpRequestMessage request, HttpResponseMessage response)
{
var requestObject = await GetHttpRequestContentAsync(request);
//this is not masking as it is JSON string
_logger.Information("Request: {@request}", requestObject);
var responseObject = await response.Content.ReadAsStringAsync();
//this is not masking as it is JSON string
_logger.Information("Response: {@response}", responseObject);
}
private async Task<string> GetHttpRequestContentAsync(HttpRequestMessage request)
{
var stream = await request.Content.ReadAsStreamAsync();
var reader = new StreamReader(stream);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
return reader.ReadToEnd();
}
}
Is there any way to make it work for my case? Or, should I look into masking my JSON string manually using JObject? I would greatly appreciate any feedback :)
Edited Answer: The request is not logged when exception is thrown for my original implementation.
Instead, I switched to action filters to log all of my requests. The request object is accessible via HttpActionContext.ActionArguments
which will work with Destructurama.Attributed.
public class LoggingFilter : ActionFilterAttribute
{
public override Task OnActionExecutingAsync(HttpActionContext context, CancellationToken cancellationToken)
{
var actionArgs = context.ActionArguments;
if (actionArgs != null && actionArgs.Count > 0)
{
foreach (var kvp in actionArgs)
{
Log.Logger.Information("{@key}: {@value}", kvp.Key, kvp.Value);
}
}
return base.OnActionExecutingAsync(context, cancellationToken);
}
}
Original Answer:
What I ended up doing instead was to log the request and response in my base api controller, not the most elegant way but it is simple and prevents the need of two lines of log in every controller method.
However, I would still greatly appreciate anyone could share an elegant approach to this.
Here is my implementation:
BaseApiController.cs
public abstract class BaseApiController : ApiController
{
private readonly ILogger _logger;
protected BaseApiController(ILogger logger)
{
_logger = logger;
}
protected IHttpActionResult LogAndCreateResponseFromResult<T1, T2, T3>(Result<T1> result, T2 requestObject, T3 responseObject)
{
_logger.Information("Request: {@request}", requestObject);
_logger.Information("Response: {@response}", responseObject);
if (result.IsFailure)
{
ModelState.AddModelError(result.Error.Code, result.Error.Message);
return BadRequest(ModelState);
}
return Ok(responseObject);
}
}
UserController.cs
public class UserController : BaseApiController
{
public UserController (ILogger logger) : base(logger)
{
}
[HttpPost, Route("login")]
public async Task<IHttpActionResult> Login(LoginRequest request)
{
// some logic
var result = await someLogicAsync();
var response = (result.IsSuccess) ? result.Value.ToResponse() : null;
return LogAndCreateResponseFromResult(result, request, response);
}
}