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

动态日志记录。

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.50/5 (3投票s)

2020年12月30日

CPOL

5分钟阅读

viewsIcon

13122

downloadIcon

135

动态日志的概念、动机和实现 - C++程序员的宝贵辅助工具

背景

调试(有时)在很大程度上依赖于智能的日志消息。这个陈述带来了一个权衡冲突——一方面,我们希望将尽可能多的数据写入日志文件。另一方面,我们又希望避免日志文件泛滥。

这通常的解决方案是

  1. 日志级别分类 - 通常为:致命 (F)、错误 (E)、警告 (W)、信息 (I)、调试 (D)、跟踪 (T)
  2. 一个补充方法(在分类态度之上)是定义一个日志级别 - 通常对 D 实现,很少对 I,几乎从不对其他类别实现。

这种做法带来了一些棘手的问题

  1. 所有日志都需要在编码时决定,并根据 a) 来考虑。这个问题虽然对于 F/E/W 甚至 I 来说是相当合乎逻辑且可接受的,但对于 D 来说是不可接受的。因此,添加或删除 D 日志意味着更改代码并重新编译,这在客户现场通常是无法实现的选项。
  2. 支持 b) 给程序员带来了繁重的任务,需要仔细设计消息层次结构,试图在大多数可能的情况下正确预测运行时实际需求。此外,编码变得乏味,需要将每条消息都包装在检查日志级别的 if 块中。
  3. 任何设置的更改(例如,日志级别)都需要重新运行。
  4. 场景的变化,尤其是意外的变化(这种情况很常见,因为一切正常时 D 日志几乎过时了),可能会(正如经常发生的那样)证明调试级别不准确。这很快就会导致 D 日志的混乱。

需求定义

我们希望设计一个解决方案来弥补上述缺点。基本要求是

  1. 调试粒度 - 能够在运行时选择实际报告哪些 D 日志消息
    1. 每条消息都需要以用户友好的方式识别
    2. 每条消息都应该可以开启/关闭
  2. 编码便捷性
  3. 易于识别 D 日志消息,以支持 a)
  4. 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,不允许区分函数变体。如果创建了新的函数变体(例如:doublefloatstd::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 日:初始版本
© . All rights reserved.