正如Jeff Atwood 所问的:“你的日志记录理念是什么?应该所有代码都被乱扔.logthis()
并.logthat()
调用吗?还是你在事后以某种方式注入日志记录?”
12 回答
我的日志记录哲学很容易总结为四个部分:
审计或业务逻辑日志记录
记录那些需要记录的东西。这来自应用程序要求,可能包括记录对任何数据库所做的每次更改(如在许多金融应用程序中)或记录对数据的访问(如卫生行业可能要求满足行业法规)
由于这是程序要求的一部分,因此许多人不会将其包含在他们对日志记录的一般讨论中,但是这些领域存在重叠,对于某些应用程序,将所有日志记录活动一起考虑是有用的。
程序记录
消息将帮助开发人员测试和调试应用程序,并更轻松地跟踪数据流和程序逻辑以了解可能存在的实施、集成和其他错误。
通常,此日志记录会根据调试会话的需要打开和关闭。
性能记录
根据需要添加以后的日志记录,以查找和解决性能瓶颈和其他不会导致程序失败但会导致更好操作的程序问题。在内存泄漏和一些非关键错误的情况下与程序日志记录重叠。
安全日志记录
记录用户操作以及与需要考虑安全性的外部系统的交互。用于确定攻击者在攻击后如何破坏系统,但也可能与入侵检测系统相关联以检测新的或正在进行的攻击。
我使用对安全至关重要的实时系统,而日志记录通常是捕获罕见错误的唯一方法,这些错误只会在每 53 个星期二的满月时出现,如果你发现我的想法的话。这让你对这个话题很着迷,所以如果我开始吐口水,我现在就道歉。
我设计的系统能够记录几乎所有内容,但默认情况下我不会打开所有内容。调试信息被发送到一个隐藏的调试对话框,该对话框为其添加时间戳并将其输出到列表框(删除前限制为大约 500 行),该对话框允许我停止它,自动将其保存到日志文件,或将其转移到附加的调试器,例如 DBWin32。这种转移让我可以看到来自多个应用程序的调试输出都整齐地序列化,这有时可以挽救生命。日志文件每 N 天自动清除一次。我曾经使用数字日志级别(级别越高,捕获的越多):
- 离开
- 仅错误
- 基本的
- 详细的
- 一切
但这太不灵活了——当你努力解决一个错误时,能够更有效地专注于你需要的登录,而不必费力地处理大量的碎屑,它可能是一种特定类型的事务或操作导致错误。如果这需要你打开一切,你只是让你自己的工作更难了。你需要更细粒度的东西。
所以现在我正在切换到基于标志系统的日志记录。记录的所有内容都有一个标志,详细说明它是哪种操作,并且有一组复选框允许我定义记录的内容。通常,该列表如下所示:
#define DEBUG_ERROR 1
#define DEBUG_BASIC 2
#define DEBUG_DETAIL 4
#define DEBUG_MSG_BASIC 8
#define DEBUG_MSG_POLL 16
#define DEBUG_MSG_STATUS 32
#define DEBUG_METRICS 64
#define DEBUG_EXCEPTION 128
#define DEBUG_STATE_CHANGE 256
#define DEBUG_DB_READ 512
#define DEBUG_DB_WRITE 1024
#define DEBUG_SQL_TEXT 2048
#define DEBUG_MSG_CONTENTS 4096
此日志系统随发布版本一起提供,默认情况下已打开并保存到文件中。如果该错误平均每六个月发生一次并且您无法重现它,那么现在发现您应该在错误发生后记录为时已晚。
该软件通常附带 ERROR、BASIC、STATE_CHANGE 和 EXCEPTION,但可以通过调试对话框(或注册表/ini/cfg 设置,保存这些内容)在现场更改。
哦,还有一件事——我的调试系统每天生成一个文件。您的要求可能不同。但请确保您的调试代码以您正在运行的代码的日期、版本以及可能的客户 ID、系统位置或其他任何标记开始每个文件。您可以从现场获得混杂的日志文件,并且您需要一些记录来自何处以及他们正在运行的系统的哪个版本,而这些系统实际上是在数据本身中,您不能信任客户/现场工程师告诉你他们有什么版本 - 他们可能只是告诉你他们认为他们有什么版本。更糟糕的是,他们可能会报告磁盘上的 exe 版本,但旧版本仍在运行,因为他们更换后忘记重新启动。让您的代码自己告诉您。
那是我脑残了……
我认为总是,总是,总是在出现异常时添加日志记录,包括消息和完整的堆栈跟踪。除此之外,我认为您是否经常使用日志是非常主观的......
我经常尝试只在我所记录的内容很少会命中的关键位置添加日志记录,否则你会遇到像他提到的日志变得太大的问题......这就是为什么记录错误案例总是最理想的事情日志(很高兴能够看到这些错误案例何时真正被击中,以便您可以进一步检查问题)。
其他要记录的好东西是如果您有断言,并且您的断言失败,则记录它...例如,此查询应该在 10 个结果以下,如果它更大可能有问题,所以记录它。当然,如果一条日志语句最终填满了日志,则可能暗示将其置于某种“调试”级别,或者调整或删除日志语句。如果日志变得太大,您通常最终会忽略它们。
我采用我认为的传统方法;一些日志记录,被条件定义包围。对于生产版本,我关闭了定义。
我选择边走边记录,因为这意味着日志数据是有意义的:
- 根据日志框架,您可以添加级别/严重性/类别信息,以便可以过滤日志数据
- 您可以确保提供正确级别的信息,不要太多,也不要太少
- 您知道在编写代码时最重要的事情是什么,因此可以确保它们被记录下来
使用某种形式的代码注入、分析或跟踪工具来生成日志很可能会生成冗长、不太有用的日志,这些日志将更难深入研究。但是,它们可能有助于调试。
System.Diagnostics.Assert
我首先在我的代码中声明很多条件(在 C# 中,使用我的代码没有永久附加调试器。
否则,我更喜欢使用 Visual Studio 将跟踪作为特殊断点放入代码中的功能(即插入一个断点并右键单击它,然后选择“当命中...”并告诉它在这种情况下显示什么)。无需重新编译,并且可以轻松地动态启用/禁用跟踪。
如果您正在编写一个将被许多人使用的程序,那么最好有某种机制来选择要记录的内容和不记录的内容。支持 .logthis() 函数的一个论点是,在某些情况下(如果处理得当),它们可以很好地替代内联注释。
此外,它还可以帮助您缩小发生错误的确切范围。
将它们全部记录下来,然后让 Grep 将它们整理出来。
我同意 Adam 的观点,但我也会考虑记录感兴趣的事情或可以证明为成就的事情,以证明它们正在发生。
我定义了各种级别并通过配置/调用传入设置。
如果你真的需要登录你的系统,那么你的测试就是垃圾,或者至少是不完整的,而且不是很彻底。你系统中的所有东西都应该尽可能地是一个黑匣子。注意像 String 这样的核心类不需要日志记录——主要原因是它们经过了很好的测试并且执行得非常详细。没有惊喜。
我使用日志记录来缩小在我们的单元测试中无法重现的问题,更不用说重复用户提供的相同步骤:那些只出现在一些非常远程的硬件上的罕见故障(有时,尽管很少,甚至由我们无法控制的驱动程序或第三方库故障引起)。
我同意我们的测试程序应该捕捉到这一切的评论,但是很难找到需要非常低级、性能关键的代码才能满足这些要求的百万+ LOC 代码库。我不在关键任务软件领域工作,但我在图形行业工作,我们经常需要做所有事情,从实现内存分配器到利用 GPU 代码再到 SIMD。
即使使用非常模块化、松散耦合甚至完全解耦的代码,系统交互也会导致非常复杂的输入和输出,其行为在平台之间会有所不同,有时我们会遇到无法测试的流氓边缘情况。模块化黑盒可能非常简单,但它们之间的交互可能会变得非常复杂,并导致偶尔出现意想不到的边缘情况。
作为一个日志记录拯救了我的例子的例子,有一次我让一个奇怪的用户使用了一台崩溃的原型英特尔机器。我们列出了应该支持 SSE 4 的机器的最低要求,但是这台特定的机器满足了这些最低要求,并且尽管是 16 核机器,但仍然不支持超过 SSE 3 的 Streaming SIMD 扩展。通过查看他的日志可以快速发现这一点,该日志准确显示了使用 SSE 4 指令的行号。我们团队中的任何人都无法重现该问题,更不用说参与验证报告的其他用户了。理想情况下,我们应该为较旧的 SIMD 版本编写代码,或者至少进行一些分支和检查以确保硬件支持最低要求,但我们希望通过最低硬件要求来传达简单性和经济性的坚定假设。在这里,也许可以说是我们的最低系统要求出现了“故障”。
鉴于我在这里使用日志记录的方式,我们往往会得到相当大的日志。然而,目标不是可读性——通常重要的是当用户遇到某种我们团队中没有人(更不用说世界上其他少数用户)的崩溃时,发送报告的日志的最后一行可以繁殖。
尽管如此,我经常使用的一个技巧来避免过多的日志垃圾邮件是,通常可以合理地假设一段成功执行一次的代码随后也会这样做(不是硬性保证,但通常是一个合理的假设)。所以我经常log_once
为粒度函数使用一种函数来避免每次调用时支付日志记录成本的开销。
我不会到处乱写日志输出(如果我有时间我可能会这样做)。通常,我将它们保留在看起来最危险的领域:调用 GLSL 着色器的代码,例如(GPU 供应商在功能方面甚至他们如何编译代码方面差异很大),使用 SIMD 内在函数的代码,非常低级的代码,代码这不可避免地必须依赖于特定于操作系统的行为,低级代码对 POD 的表示做出假设(例如:假设 8 位到一个字节的代码)——在这种情况下,我们同样会散布很多断言和健全性检查以及编写最多数量的单元测试。通常这已经足够了,并且日志记录已经多次挽救了我的屁股,否则我会遇到无法重现的问题,并且不得不盲目地解决问题,