动态日志记录。






3.50/5 (3投票s)
动态日志的概念、动机和实现 - C++程序员的宝贵辅助工具
背景
调试(有时)在很大程度上依赖于智能的日志消息。这个陈述带来了一个权衡冲突——一方面,我们希望将尽可能多的数据写入日志文件。另一方面,我们又希望避免日志文件泛滥。
这通常的解决方案是
- 日志级别分类 - 通常为:致命 (F)、错误 (E)、警告 (W)、信息 (I)、调试 (D)、跟踪 (T)
- 一个补充方法(在分类态度之上)是定义一个日志级别 - 通常对 D 实现,很少对 I,几乎从不对其他类别实现。
这种做法带来了一些棘手的问题
- 所有日志都需要在编码时决定,并根据 a) 来考虑。这个问题虽然对于 F/E/W 甚至 I 来说是相当合乎逻辑且可接受的,但对于 D 来说是不可接受的。因此,添加或删除 D 日志意味着更改代码并重新编译,这在客户现场通常是无法实现的选项。
- 支持 b) 给程序员带来了繁重的任务,需要仔细设计消息层次结构,试图在大多数可能的情况下正确预测运行时实际需求。此外,编码变得乏味,需要将每条消息都包装在检查日志级别的
if
块中。 - 任何设置的更改(例如,日志级别)都需要重新运行。
- 场景的变化,尤其是意外的变化(这种情况很常见,因为一切正常时 D 日志几乎过时了),可能会(正如经常发生的那样)证明调试级别不准确。这很快就会导致 D 日志的混乱。
需求定义
我们希望设计一个解决方案来弥补上述缺点。基本要求是
- 调试粒度 - 能够在运行时选择实际报告哪些 D 日志消息
- 每条消息都需要以用户友好的方式识别
- 每条消息都应该可以开启/关闭
- 编码便捷性
- 易于识别 D 日志消息,以支持 a)
- c) 的自动化
建议解决方案
幸运的是,通过一组类和 C 宏,可以满足所有已定义的需求。
以你最喜欢的标准日志工具为例,比如
MyLog(“<the message>”);
它只是写入日志文件
<the message>
假设代码中的每个 D 日志条目都被替换为一个**块**。在块内,根据**唯一**的字符串标识符为该消息分配一个**唯一**的 ID。因此,具有**相同**字符串标识符的 D 日志消息将被分配**相同的** ID。这个唯一 ID 在单例中被检查,以决定该 ID 是否已定义为日志记录(=报告)。如果该 ID 已注册用于日志记录,则使用标准的 MyLog
,它会强制执行内部所有现有逻辑,例如字符串构建和基于从 XML 读取的配置的实际报告到日志文件的决定。否则,什么也不会发生。
{
static int i = -1;
const char* sDynalogId = “<namespace::class::func.location>”;
if (-1 == i)
{
i = AddReportable(sDynalogId);
}
if (IsReportable(i))
{
MyLog(sDynalogId << ": " << “<the message>”);
}
}
现在,虽然这可以工作,但至少可以说非常麻烦。给定一个宏来包装这段代码
#define DYNALOG(_Src, ...) \
do { \
static int i = -1; \
if (-1 == i) \
{ \
i = NDYNALOG::AddReportable(_Src, false); \
} \
if (NDYNALOG::IsReportable(i)) \
{ \
MyLog(_Src << ": " << __VA_ARGS__); \
} \
} while(0)
上面的代码变成
DYNALOG(“<namespace::class::func.location>”, “<the message>”);
例如
void MyClass::DoIt()
{
DYNALOG(“MyClass::DoIt”, “Entered”);
// Do something
DYNALOG(“MyClass::DoIt.Leave”, “Exited”);
}
在配置文件中表示
0 MyClass::DoIt
0 MyClass::DoIt.Leave
要不记录其中任何一个条目,
或者
更改为 1 并重新加载配置文件
0 MyClass::DoIt
1 MyClass::DoIt.Leave
仅记录“已退出”消息。
到目前为止,我们处理了单个消息。
现在,让我们填充缺失的部分。首先,我们需要能够加载/保存动态日志配置集。即
void NDYNALOG::SaveConfiguration();
void NDYNALOG::LoadConfiguration();
预计在应用程序启动时(与 MyLog
初始化一起)调用 NDYNALOG::LoadConfiguration
。当需要更改配置时,也需要调用它。
预计在应用程序终止时调用 NDYNALOG::SaveConfiguration
,以保存下次运行时加载的配置。当用户希望更新配置列表时,也需要调用它。
注意事项和解决方案
场景 a
考虑以下代码片段
void f()
{
for (int i = 1; i < 3; i++)
{
<code>DYNALOG</code>(“f”, i);
}
}
虽然预期的输出是
f: 1
f: 2
f: 3
会生成一个相当令人惊讶的输出,类似如下:
f: 72
f: 72
f: 72
原因是 DYNALOG 中的 i
被宏内部的 i
所遮盖,后者代表消息的唯一 ID。
解决这个问题的实际方法是改变宏内部的 i
为一个真正唯一的名称,例如 __ID_898CC116_D331_4b2e_9E30_952D99CD08D9
,这样,当它与用户的名称冲突时,就会归类为“没有绝对万无一失的系统,因为傻瓜是如此聪明”。在生产代码中定义一个名为 __ID_898CC116_D331_4b2e_9E30_952D99CD08D9
的变量,不亚于恶意破坏。
场景 b
考虑以下代码片段(在解决了**场景 a** 的注意事项之后)
template <T> void f(T v)
{
DYNALOG(“f”, v);
}
void main()
{
int i = 123;
char* s = “Hello”;
f(i);
f(s);
}
输出是:
f: 123
f: Hello
这没有提供关于实际调用的函数(无论是 int
还是 char*
模板函数)的足够信息。更糟糕的是,配置中只有一个条目 - f
,不允许区分函数变体。如果创建了新的函数变体(例如:double
、float
、std::string
),则无法为它们定义不同的行为。要么记录 f
的所有变体,要么都不记录。
为了解决这个问题,提供了 _FUNC_DECORATED_
宏
#ifdef WIN32
#define _FUNC_DECORATED_(_Src) (std::string(__FUNCTION__) + _Src).c_str()
#else // Linux
#define _FUNC_DECORATED_(_Src) (std::string(__PRETTY_FUNCTION__) + _Src).c_str()
#endif
不使用 __func
的原因是 __func
解析为 f
。
之所以提供 Windows/Linux 变体,是因为在 Linux 中 __FUNCTION__
解析为 __func
。
使用建议的宏
template <T> void f(T v)
{
DYNALOG(_FUNC_DECORATED_(“”), v);
}
将在 **Windows** 上的 VC++ 中生成以下内容
f<int>: 123
f<char*>: Hello
以及在 **Linux** 上的 g++ 中生成以下内容
void f(T) [with T = int]: 123
void f(T) [with T = char*]: Hello
实现问题
保存/加载的配置文件是一个文本文件,可以在记事本中编辑。每条消息的唯一日志标签都在单独的一行上,其中第一个字段是一个布尔值,表示是否需要日志记录。行的其余部分是控制日志记录的标签。
尽管可以手动向文件中添加条目,但不推荐这样做,因为出错的可能性很高。建议的做法是运行应用程序,让它将标签收集到缓存中,然后执行“保存动态日志配置”操作。
保存/加载的示例配置文件格式如下(在 Windows 上)
0 f
0 f<char*>
0 f<int>
以及在 Linux 上
0 f
0 void f(T) [with T = char*]
0 void f(T) [with T = int]
为了使 DYNALOG 成为调试过程中有价值的辅助工具——程序员应该大量使用该工具(因为其性能成本微乎其微),并秉持“越多越好”的态度。
历史
- 2020 年 12 月 30 日:初始版本