65.9K
CodeProject 正在变化。 阅读更多。
Home

JLogger6 - 当您希望写出最好的日志时

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2022 年 8 月 21 日

CPOL

11分钟阅读

viewsIcon

9327

描述一个简单、通用且可扩展的日志记录器

引言

JLogger6 是一个单例组件,作为 .NET 6 库组件,可用于支持 .NET 6 的任何 .NET 项目。

JLogger 具有以下特性

  • 多线程使用 – 作为单例,它可以从任何线程访问,并使用锁定技术确保没有冲突。
  • 高吞吐量 – 如果日志被多个线程同时使用,日志写入不会中断调用线程。JLogger 使用先进先出 (FIFO) 队列,将日志写入放入队列并在单独的线程中后台并发写入文件。WriteDebugLog 命令接受参数,创建日志数据,将其放入队列。这些步骤都没有阻塞。
  • 发送电子邮件 – 调试日志写入可以选择性地发送电子邮件(需要 SMTP 配置数据)
  • 多种日志条目类型 – 有几种日志条目类型可供选择。每种类型的含义取决于编写代码的用户。某些日志类型保留给组件使用,在处理日志条目时将被忽略。下面将详细介绍。
  • 每天新日志文件 – 午夜过后,会创建一个新的日志文件,因此日志文件会命名以显示日志活动的日期和时间范围。
  • 日志保留 – 日志将在指定天数后自动删除,如果指定为零,则不删除任何日志文件。
  • 制表符分隔的日志文件 – 日志以制表符分隔的文件形式写入。这使得可以在 Excel 等程序中打开该文件进行分析。
  • 可选地将日志写入数据库 - 当用户倾向于将日志写入数据库时,此选项适用于 SQL Server。提供了脚本来创建 DBLog 表以及用于插入日志记录和执行日志保留的存储过程。
  • 可选地将日志文件存储在 Azure 文件存储中 - 通过指定 Azure 存储,当日志关闭时,日志文件会被复制到那里并在本地删除。

简化日志记录

多年来,我尝试过几种日志记录包。其中一些非常出色,大多数在某些场景下很有用。我无意贬低任何一个。您可能会发现其他包更适合您的需求。当我描述此 NuGet 包在演示应用程序中的使用时,您可能会发现它的简单性、通用性、可扩展性和性能是您想要的。

设置日志记录器

日志记录器中的一个关键项是 LOG_TYPE 枚举。这提供了一种指定条目日志类型的方式,并为调用代码提供了一种简单的方式来选择是否进行日志记录。这使得可以运行时更改记录的内容和不记录的内容。这将在后面的部分中显现。

这些代码行用于说明 JLogger 的用法。有更多
用法比文档所示的要多,但这展示了 JLogger 的完整功能用法
JLogger 的用法。

首先,是引用库的 `using` 语句

using Jeff.Jones.JLogger6;
using Jeff.Jones.JHelpers6;

下面是一个示例,用于设置一个类级别的变量,以配置您想要的调试日志选项。您设置的内容可能因开发、QA、生产和生产故障排除而异。

此程序全局值通常存储在某个配置数据位置。

LOG_TYPE m_DebugLogOptions = LOG_TYPE.Error | LOG_TYPE.Informational |
                             LOG_TYPE.ShowTimeOnly | LOG_TYPE.Warning |
                             LOG_TYPE.HideThreadID | 
                             LOG_TYPE.ShowModuleMethodAndLineNumber |
                             LOG_TYPE.System | LOG_TYPE.SendEmail;

下一步是设置用于配置 Logger 的变量,通常在程序的启动代码中,尽早执行,在需要日志记录器之前。

Boolean response = false;
String filePath = CommonHelpers.CurDir + @"\";
String fileNamePrefix = "MyLog";

// This value applies to both debug files and to DB log entries.
Int32 daysToRetainLogs = 30;

// Setting the Logger data so it knows how to build a log file, and
// how long to keep them. The initial debug log options is set here,
// and can be changed programmatically at anytime in the
// Logger.Instance.DebugLogOptions property.
response = Logger.Instance.SetLogData
           (filePath, fileNamePrefix, daysToRetainLogs, logOptions, "");

如果使用数据库存储日志,则使用此代码作为示例,而不是前面的代码。
注意您可以将其设置为 "useDBLogging = false",它将使用上面 SetLogData 方法中指定的日志文件。

这些行显示了如何设置基于数据库的日志记录。DBLog 表和两个存储过程的 T-SQL 脚本必须在您想要日志条目的数据库上执行。

如果使用 Windows 身份验证访问数据库,请确保 Windows 帐户在 SQL Server 上具有必要的权限,并且您可以将 DBUserNameDBPassword 设置为 ""。内部数据库连接会根据 SetDBConfiguration() 传递的值构建正确的连接字符串。

Boolean response = false;
String serverInstanceName = "MyComputer.SQL2020";
String dbUserName = "";
String dbPassword = "";
Boolean useWindowsAuthentication = true;
Boolean useDBLogging = true;
String databaseName = "myData";

response = Logger.Instance.SetDBConfiguration(serverInstanceName,
                                              dbUserName,
                                              dbPassword,
                                              useWindowsAuthentication,
                                              useDBLogging,
                                              databaseName);

提供了三个数据库脚本,必须在目标 SQL Server 上运行。

  • DBLog.sql - 创建 DBLog 表和主键索引。
  • spDebugLogDelete.sql - 删除早于特定日期的记录。请参阅 DataAccessLayer.ProcessLogRetention() 方法了解其用法。
  • spDebugLogInsert.sql - 插入日志记录。请参阅 DataAccessLayer.WriteDBLog() 方法了解其用法。

如果记录到文件,并且您想使用 Azure 文件存储,则需要添加此配置。出于性能原因(一次写入一个日志行到 Azure 文件会因网络开销而慢得多),日志文件在打开时在本地使用。关闭日志时,日志文件会被复制到指定的 Azure 文件存储。日志保留在 Azure 文件存储上运行,而不是在本地运行,因为本地日志文件在复制到 Azure 文件存储后会被删除。

// Optional configuration for Azure file storage
String resourceID = "<AZURE_CONNECTION_STRING>";
String fileShareName = "<AZURE_FILE_SHARE_NAME>";
String directoryName = "<AZURE_DIRECTORY_NAME>";
response = Logger.Instance.SetAzureConfiguration
           (resourceID, fileShareName, directoryName, true);

其中一个选项,无论日志位置如何,都可以发送电子邮件。这是启用电子邮件用于指定此功能的日志条目的配置。仅启用并不会发送电子邮件。更多信息请参见下面的代码部分。

// Email setup.
// Note that the Debug Log Options must have the LOG_TYPES.SendEmail flag in order for a 
// given log entry to send an email.  
// If that flag is not in the log options bitset, then adding to the flags for a 
// log entry will not send email.  Both must be present in the log options and the 
// log entry for the email to be sent.
Int32 smtpPort = 587;  // Or whatever port your email server uses.
Boolean useSSL = true;

List<String\ sendToAddresses = new List<String>();
sendToAddresses.Add("MyBuddy@somewhere.net");
sendToAddresses.Add("John.Smith@anywhere.net");

response = Logger.Instance.SetEmailData("smtp.mymailserver.net",
                                        "logonEmailAddress@work.net",
                                        "logonEmailPassword",
                                        smtpPort,
                                        sendToAddresses,
                                        "emailFromAddress@work.net",
                                        "emailReplyToAddress@work.net",
                                        useSSL);

// This is an example of how to use the LOG_TYPES.SendMail flag when writing to the log
// so that an email is sent.
if ((m_DebugLogOptions & LOG_TYPES.Error) == LOG_TYPES.Error)
{
   Logger.Instance.WriteDebugLog(LOG_TYPES.Error & LOG_TYPES.SendEmail,
                                 exUnhandled,
                                "Optional Specific message if desired");
}

以及发送电子邮件不被需要时的相同日志条目

if ((m_DebugLogOptions & LOG_TYPES.Error) == LOG_TYPES.Error)
{
   Logger.Instance.WriteDebugLog(LOG_TYPES.Error,
             exUnhandled,
             "Optional Specific message if desired");
}

配置只需执行一次。然而,为了保持动态性,Logger.DebugLogOptions 属性可以在运行时设置为任何所需的位掩码。例如,如果您想在不重新启动系统的情况下增加日志记录,只需更新 config 文件中的调试日志选项值以打开更多日志位,然后让监控配置文件的任何进程更新 Logger.DebugLogOptions 属性。现在将记录更多内容,您可以将日志记录量减少回正常水平。

代码中的日志记录示例

关于性能的一个注意事项是在调用日志记录方法之前进行位掩码比较。如果位未打开,则不会执行记录日志的代码。因此,添加更多日志条目不会影响性能,除非它们被使用。所以性能和流程等方面都可以进行编码,但除非被启用,否则不会影响性能。这种方法允许在不影响性能的情况下,将大量的通用性设计到代码中,以便进行调试和分析。

// Example of use in a method
void SomeMethod()
{
  // Use of the Flow LOG_TYPE shows in the log when a method was entered,
  // and exited. Useful for debugging, QA, and development. The Flow bit
  // mask is usually turned off in production to reduce log size.
  if ((m_DebugLogOptions & LOG_TYPE.Flow) == LOG_EXCEPTION_TYPE.Flow)
  {
    Logger.Instance.WriteToDebugLog(LOG_TYPE.Flow, "1st line in method", “”);
  }
  // This variable notes when the method started.
  DateTime methodStart = DateTime.Now;
  try
  {
    // Do some work here
    // This is an example of logging used during
    // process flow. The bitmask used here does not
    // have to be “Informational”, and may be turned
    // off in production.
    Logger.Instance.WriteToDebugLog(LOG_TYPE.Informational,
                                    "Primary message",
                                    "Optional detail message");
    // Do some more work
  }
  catch (Exception exUnhandled)
  {
    // Capture some runtime data that may be useful in debugging.
    exUnhandled.Data.Add(“SomeName”, “SomeValue”);
    if ((m_DebugLogOptions & LOG_TYPE.Error) == LOG_TYPE.Error)
    {
      Logger.Instance.WriteToDebugLog(LOG_TYPE.Error,
                                      exUnhandled,
                                      "Optional detail message");
    }
  }
  finally
  {
    if ((m_DebugLogOptions & LOG_TYPE.Performance) == LOG_TYPE.Performance)
    {
      TimeSpan elapsedTime = DateTime.Now - methodStart;
      Logger.Instance.WriteToDebugLog(LOG_TYPE.Performance,
                                      String.Format("END; 
                                      elapsed time = [{0:mm} mins, 
                                      {0:ss} secs, {0:fff} msecs].", objElapsedTime));
    }
    // Capture the flow for exiting the method.
    if ((m_DebugLogOptions & LOG_TYPE.Flow) == LOG_EXCEPTION_TYPE.Flow)
    {
      Logger.Instance.WriteToDebugLog(LOG_TYPE.Flow, "Exiting method", “”);
    }
  }
} // END of method 

日志记录代码的许多用法都可以复制粘贴,从而缩短开发时间。

ILogger 选项

为了在利用 ILogger 接口的 .NET 应用程序中使用 JLogger6,只需获取 Logger.Instance 对象 ILogger 接口的引用即可。

LogLevel 转换为 JLogger6 日志类型

  • LogLevel.CriticalLOG_TYPE.Fatal 添加到调试日志选项
  • LogLevel.DebugLOG_TYPE.System 添加到调试日志选项
  • LogLevel.ErrorLOG_TYPE.Error 添加到调试日志选项
  • LogLevel.InformationLOG_TYPE.Informational 添加到调试日志选项
  • LogLevel.WarningLOG_TYPE.Warning 添加到调试日志选项
  • LogLevel.TraceLOG_TYPE.Flow 添加到调试日志选项

这些是 ILogger 方法以及它们如何使用底层 Logger 实例。

void Log<TState>(LogLevel logLevel, EventId eventId, 
     TState state, Exception exception, Func<TState, Exception, string> formatter)

写入日志的方式为

WriteDebugLog(logType, exception, $"EventID = {eventId.ToString()}; 
              State = {state.ToString()}");

bool IsEnabled(LogLevel logLevel)

检查调试日志选项位掩码,以查看转换后的位是否已启用。

IDisposable BeginScope<TState>(TState state)

启动日志并返回 Logger.Instance 对象的 IDisposable 引用。

让我们看看日志

Sample Log Text

这是前几行的示例。当日志启动时,Logger 会自动捕获一些系统值的快照,这些值在后续的诊断和分析中已被证明非常有用。

列包括

  1. 时间(或日期/时间,取决于 LOG_TYPE 标志 ShowTimeOnly)。这提供了精确到毫秒的时间。通常,日志会在午夜关闭并启动新的日志,因此 ShowTimeOnly 标志是常用的。
  2. 日志类型 - 条目的日志类型(与日志条目一起使用的 LOG_TYPE 值的名称)
  3. 消息 - 日志条目的主要消息
  4. 附加信息 - 附加的、通常更详细的信息,用于解释 Message
  5. 异常数据 - 异常 Data 集合的键值对。明智地使用此 Exception 类功能对于节省故障排除和诊断时间至关重要。
  6. 堆栈信息 - 来自异常实例的堆栈信息
  7. 模块 - 发生日志条目的模块名称
  8. 方法 - 发生日志条目的方法名称
  9. 行号 - 发生错误的行号
  10. 线程 ID - .NET 线程 ID(如果提供)

日志文件也可以直接在 Excel(或任何解释制表符分隔列的电子表格程序)中打开。Excel 允许比 Notepad 等文本编辑器更复杂的搜索和分析。

Log file in Excel

如果无法写入日志文件或日志数据库表,Logger 会创建一个“紧急日志文件”,也采用制表符分隔格式。它直接写入(而不是通过队列)并提供基本信息。

无论如何,Logger 都试图确保日志条目被写入。

看看演示应用

演示程序是一个 .NET 6 Windows Forms 应用程序。与 Logger 的正常实现不同,Logger 实际上是在 Run Test 按钮调用的代码中配置、运行和关闭的。此演示的目的是展示 Logger 在各种配置中的用法。

日志文件配置

Azure 文件存储配置

数据库配置

代码可以从 https://github.com/MSBassSinger/LoggingDemo6 下载,以便您可以根据需要进行单步调试和测试。

即将推出的功能

我正在研究下一版本,以支持文件日志和数据库中的用户定义字段。其概念是允许在配置日志时定义和/或创建用户定义字段,因此它们可以随着时间的推移为任何给定的应用程序而改变。

此外,我正在研究数据库日志存储的一个选项,以添加一个审计表,以便在 DBLog 表中删除的记录会在 DBLogAudit 表中注明。对于那些必须保留日志记录及其操作的人来说,可能需要此选项。

结论

我想创建一个设置和使用更简单、更一致的日志记录器。我想提供广泛的日志类型而无需回溯和重新编码,因此打开和关闭位即可满足此需求。而且,我想让日志记录器能够处理多线程和任务,并具有高吞吐量的日志条目,而不会影响性能。

依赖注入 (DI) 的纯粹主义者可能会对使用单例感到厌恶。然而,DI 是一种设计概念,旨在应用于创建在另一个对象中并且影响业务规则的对象。在日志记录器的案例中,它不是在对象内创建的,它也不影响业务规则。因此,使用单例日志记录器不会违反 DI 的最初目的。就像对象可以通过构造函数、方法或属性进行依赖注入一样,引入外部对象(Logger 实例)作为单例同样有效。在这三种情况下,都会注入(推送或拉入)Logger 实例的引用,而不是创建它。

一些开发者已经有了自己喜欢的日志记录器。其他人则选择最容易、工作量最小的。但是,如果您愿意看看 JLogger6 是否有价值,并想最大限度地发挥日志记录的作用,我希望您能认真尝试一下这个日志记录器。

背景

我从事软件开发已有 40 多年,跨越多种操作系统和多种语言。远远早于 Windows。远远早于 Linux。日志记录的几个一致性之一是需要日志记录来衡量性能,记录错误、警告和其他信息,所有这些都使解决生产、QA 和开发应用程序问题的工作不那么痛苦。我编写了这个组件,以便几乎可以在任何场景中使用日志记录,根据我的需求进行配置,而且不会减慢应用程序执行速度。

使用演示代码

从 GitHub 存储库拉取演示项目。代码有详细注释,并展示了如何配置和使用 JLogger6。演示是用 C# 在 Visual Studio 2022 中编写的。演示代码针对 .NET 6。JLogger6 针对 .NET 6。

关注点

我想创建一个易于使用、易于设置的日志记录组件,并通过位比较,在不需要时跳过对日志的方法调用。当我遇到成百上千个任务/线程试图写入日志文件的情况时,我决定使用排队方法来写入日志文件。这大大减慢了 UI 的速度。改为采用该架构消除了这个问题。

历史

作者 什么
2019 年 7 月 26 日 JDJ Genesis
2019 年 10 月 22 日 JDJ 更新以提供演示如何使用 JLogger 的代码的访问权限
2022 年 8 月 21 日 JDJ 更新了功能并定位到 .NET 6
© . All rights reserved.