我有一个自定义的构建日志框架,可以记录到数据库。
例如,它可以进行
L.e("Error invalid password", userGuid);
这对于一般用途来说很好,但应用程序相当复杂,并且从主代码中调用了许多不同的部分。例如,登录序列可以发送OTP的SMS,OTP由系统的另一个部分处理,仅仅为了登录目的传递大量值是没有意义的。
我想要实现的是用例如userGuid标记日志,这样我就可以搜索与该特定用户相关的所有内容。我还想标记SMS模块中的任何登录,即使SMS模块对用户概念一无所知。
所以我想的是,是否有可能获得当前的线程ID,并在更高级别上存储一些关于日志记录的内容。我想知道这是否可能。
Psuedo代码:
void Login(UserName, Password) {
User user = UserManager.GetUser(UserName)
using(L.SetUser(user.ID)) { //Here I want to use user.ID later in code that dont know the context
SmsManager.SendOtp(user.Phonenumber)
}
}
public class SmsManager {
public static void SendOtp(string phonenumber) {
if (phonenumber == "") {
L.error("Phone number is empty"); //How can I use the value from L.SetUser above? Could I use a hash table of threadids in L or would that be a bad idea?
}
}
}
亲切问候Jens
你能给我们看一些L
的片段吗?那是static
级吗?SetUser
是否设置了static
变量?您可以按照建议的方式使用using
块。您想要实现IDisposable
并清除Dispose
方法中的UserID
值。但是,如果UserID
是static
变量,则此解决方案将无法在多线程环境中工作(如果没有其他更改(。这个设计在我看来很奇怪。
总的来说,您似乎经常使用static
。这可能会给你带来麻烦。
有很多可能的解决方案。如果没有看到更多的代码,很难说什么是最好的。这里有一种使用依赖项注入的方法,可以根据需要将模块分离。
为您的记录器定义一个接口。
public interface ILogger
{
void Error(string message);
}
使用添加用户信息的类实现:
public class MessageWithUserLogger : ILogger
{
private readonly string _userId;
public MessageWithUserLogger(string userId)
{
_userId = userId;
}
public void Error(string message)
{
L.error(message, _userId);
}
}
将SmsManager
类更改为非static
,并依赖于ILogger
抽象而非L
实现:
public class SmsManager
{
private readonly ILogger _logger;
public SmsManager(ILogger logger)
{
_logger = logger;
}
public void SendOtp(string phonenumber)
{
if (phonenumber == "")
{
_logger.Error("Phone number is empty");
}
}
}
当用户ID信息可用时,为记录器注入该信息:
void Login(UserName, Password)
{
User user = UserManager.GetUser(UserName);
ILogger logger = new MessageWithUserLogger(user.ID);
SmsManager smsManager = new SmsManager(logger);
smsManager.SendOtp(user.Phonenumber);
}
using语句不是这样使用的。引入using语句是为了能够定义有限的范围,同时确保使用IDisposable接口处理对象(另请参阅https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-statement)。
使用using语句的方式使属性看起来像是在启动作用域时发送的,并且在某种程度上是";未设置";之后,但事实并非如此。
当使用记录器并从伪代码开始时,我认为应该扩展日志框架,以便在使用时创建特定于上下文的记录器,然后将日志上下文传递给静态函数。它看起来如下:
void Login(UserName, Password) {
User user = UserManager.GetUser(UserName)
using(var logContext = L.CreateContext(user.ID)) { //Here I want to use user.ID later in code that dont know the context
SmsManager.SendOtp(logContext, user.Phonenumber)
}
}
public class SmsManager {
public static void SendOtp(LogContext logContext, string phonenumber) {
if (phonenumber == "") {
logContext.error("Phone number is empty"); //How can I use the value from L.SetUser above? Could I use a hash table of threadids in L or would that be a bad idea?
}
}
}
理论上可以将上下文存储在L对象中,并将其映射到线程ID,而不是传递日志上下文,稍后在函数中,当您记录某些内容时,检查该线程是否有特定的日志上下文。在LogContext对象的IDisposable接口实现中,您应该移除上下文(对应于using((作用域的末尾(。然而,我不会这么做,因为它";隐藏";一堆逻辑,但更重要的是,它依赖于每个函数都将在同一个线程中执行的事实。这与隐藏这一点相结合,使其成为错误的可能来源(如果代码的用户不知道这与线程链接,并更改了线程,则可能会错过信息,根据日志记录做出错误的假设,等等(。我认为,如果你有像SMS管理器这样的函数,它有很多助手函数,可以传递特定于上下文的对象,这是一种不错的做法。
此外,请注意,这是一个存在于大多数流行的日志库(如Serilog(中的概念,并且在几乎所有情况下,编写自己的日志库并不是最有利可图的业务(因为这些库中的大多数都有扩展,允许您编写自定义接收器,例如,它会将日志输出写入特定场景的数据库中(但其余的都是免费的(。