用于控制对请求对象进行解构的选项

时间:2018-07-26 14:31:30

标签: c# asp.net-core serilog

我遇到了一个问题,我一直在努力寻找一个干净的解决方案,而谷歌搜索并没有使我变得更明智。

情况

(1)我们有自己的程序集,用于设置Serilog记录器并将其添加到我们的任何项目(一致的记录输出,主题等),并且该程序集没有引用任何消耗项目(在不同项目中)回购)。我们将其称为CompanySerilog程序集。

(2)消耗项目之一是一个外部可访问的API,为此,在ExternalContracts程序集中定义了“合同”对象。即请求和响应对象,以及用作这些对象一部分的任何枚举。可以将此ExternalContracts程序集提供给与API集成的开发人员。

(3)我们要记录所有请求,并使用IActionFilter使用Serilog结构化记录方法注销每个请求对象。例如遍历上下文中的每个参数并最终执行_logger.LogDebug("With {name} of {@requestObject}", name, value);

问题

某些请求对象具有我们要屏蔽的敏感数据,但是:

  • 我们可以使用标准CompanySerilog扩展名在.Destructure中创建记录器时定义解构方法,但是不知道或不希望知道请求对象的详细信息,因为这些可能来自Api1Api2等,这意味着向每个消耗项目添加引用。
  • 我们可以向请求对象(Destructurama.Attributed中添加属性,但这意味着ExternalContracts程序集现在需要对该NuGet包的引用,而这又需要对所有必需的Serilog包的引用。严格来讲,在ExternalContracts程序集中不需要记录日志:这是我们的问题,而不是我们的API使用者

正如我所说,我一直在努力想办法解决这个问题,并且在使用IDestructuringPolicy等信息的方式上找不到很多,甚至是否合适,或者是否应该进行转换发挥作用。到目前为止,我只能想到以下选项,但我希望其他人遇到此问题,并拥有支持该用例的邪恶聪明和简洁的方法。

解决方案?

  • 停止执行结构化日志记录,并为每个请求对象定义一个ToString(),以掩盖我们不想记录的值。这很简单,不需要讨厌的项目交叉引用或将日志记录问题添加到外部合同中。但这确实意味着不可能进行结构化日志记录。

  • 将所有必需的日志记录引用添加到外部协定中。这将使我们能够继续使用内置销毁功能,但意味着我们的API使用者将拥有一个包含日志记录程序集的ExternalContracts程序集

  • .Destructure中配置日志记录时,通过引用将消耗此程序集的每个项目来设置CompanySerilog值。会发生!

  • 还有别的吗?拜托!

2 个答案:

答案 0 :(得分:2)

我们提出了两个潜在的解决方案,以防万一有人遇到类似问题,我将与大家分享-都涉及使用IDestructuringPolicy

解决方案1 ​​

IDestructuringPolicy程序集中有一个通用的CompanySerilog

public class SensitiveDataDestructuringPolicy : IDestructuringPolicy
    {
        public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
        {
            var props = value.GetType().GetTypeInfo().DeclaredProperties;
            var logEventProperties = new List<LogEventProperty>();

            foreach (var propertyInfo in props)
            {
                switch (propertyInfo.Name.ToLower())
                {
                    case "cardnumber":
                    case "password":
                        logEventProperties.Add(new LogEventProperty(propertyInfo.Name,propertyValueFactory.CreatePropertyValue("***")));
                        break;
                    default:
                        logEventProperties.Add(new LogEventProperty(propertyInfo.Name, propertyValueFactory.CreatePropertyValue(propertyInfo.GetValue(value))));
                        break;
                }

            }
            result = new StructureValue(logEventProperties);
            return true;
        }
    }

并在设置记录器时,使用以下类型的配置:

var logger = new LoggerConfiguration()
// snipped out all the other things that need configuring
// ...
.Destructure.With<SensitiveDataDestructuringPolicy>
.CreateLogger();

这种方法的优点:

  • 一个地方(在日志记录程序集中)负责决定如何记录对象,而不知道这些对象将是什么类型

这种方法的缺点:

  • 这将反映每个对象的每个属性,如果只需要屏蔽一个或两个对象,这是过大的选择

最后,由于第一个解决方案的弊端,我们采用了另一种方法。

解决方案2

CompanySerilog中具有用于在记录器中查找使用IDestructuringPolicies的方法(无论使用哪个程序集)。

public static ILogger Create()
{
    var destructuringPolicies = GetAllDestructuringPolicies();

    var logger = new LoggerConfiguration()
    // snipped out all the other things that need configuring
    // ...
    .Destructure.With(destructuringPolicies)
    .CreateLogger();

    //Set the static instance of Serilog.Log with the same config
    Log.Logger = logger;

    logger.Debug($"Found {destructuringPolicies.Length} destructuring policies");
    return logger;
}

/// <summary>
/// Finds all classes that implement IDestructuringPolicy, in the assembly that is calling this 
/// </summary>
/// <returns></returns>
private static IDestructuringPolicy[] GetAllDestructuringPolicies()
{
    var policies = Assembly.GetEntryAssembly().GetTypes().Where(x => typeof(IDestructuringPolicy).IsAssignableFrom(x));
    var instances = policies.Select(x => (IDestructuringPolicy)Activator.CreateInstance(x));
    return instances.ToArray();
}

现在,该CompanySerilog程序集的所有使用者都可以通过为其关心的每个类定义一个IDestructuringPolicy来定义其希望如何记录敏感数据的方式。例如:

public class RegisterNewUserDestructuringPolicy : IDestructuringPolicy
{
    public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
    {
        var request = value as RegisterNewUserRequest;
        if (request == null)
        {
            result = null;
            return false;
        }

        var logEventProperties = new List<LogEventProperty>
            {
                new LogEventProperty(nameof(request.Claims), propertyValueFactory.CreatePropertyValue(request.Claims)),
                new LogEventProperty(nameof(request.Email), propertyValueFactory.CreatePropertyValue(request.Email)),
                new LogEventProperty(nameof(request.Password), propertyValueFactory.CreatePropertyValue("****")),
                new LogEventProperty(nameof(request.Roles), propertyValueFactory.CreatePropertyValue(request.Roles)),
                new LogEventProperty(nameof(request.UserName),
                    propertyValueFactory.CreatePropertyValue(request.UserName))
            };

        result = new StructureValue(logEventProperties);
        return true;
    }
}

此方法相对于解决方案1的优势在于,我们现在正在处理具体类型,如果该类型没有策略,则不会被反映出来。

答案 1 :(得分:1)

听起来像是适配器模式的情况。您不希望外部API涉及日志记录问题,也不希望CompanySerilog知道API中的特殊情况。最好的选择可能是创建一个包装对象,该包装对象(临时)保存对请求对象的引用。记录包装程序,该包装程序将仅具有您要在日志中显示的属性。

由于包装程序除了被包装的对象之外不会保留任何状态,因此甚至可以通过池重新使用它们,以消除GC开销。

大致:

public class Request {
  public string Username { get; set; } // log this
  public string Password { get; set; } // but not this
}

public class RequestLogWrapper {
  public Request WrappedRequest { private get; set; }
  public String Username { get { return WrappedRequest.Username; }
}

//To use:
var rlw = new RequestLogWrapper { Request = request };
logger.log("Got a request: {0}", rlw);