C++ 日志封装器设计

6
我想在我的应用程序中添加日志记录。我已经选择了一个日志记录库,但我希望能够轻松切换到不同的日志记录库,而无需更改使用日志记录的任何代码。
因此,我需要一种灵活的日志包装器,可以利用几乎任何底层日志记录库的功能。
有没有关于这样一个包装器设计的建议?
编辑:我必须在这个包装器中拥有一个特性,即组件标记。我希望我的算法类在其日志行之前出现“X:”,我的管理类则出现“Y:”。如何将这些标记传播到底层日志以及如何构建组件标记命名机制是一个重要的设计问题。

2
真的需要一个包装器吗?问问自己。你真的有可能会切换你的记录器(可能多次),或者你将使用多个日志框架吗?编写包装器需要资源(时间),如果你不真的需要包装器,这些资源可以用在其他事情上。 - Xeo
谢谢,你可以相信我真的会做到。为了简单起见,我愿意接受一个非常简化的带有标记功能的日志版本。 - Leo
3个回答

2

您最好尽可能简化界面。完全将登录用户的界面与实际实现登录的方式分开。

横切关注点总是很难维护的,所以让事情变得更加复杂只会让您感到痛苦。

某些库只需要像这样的简单东西:

void logDebug(const std::string &msg);
void logWarning(const std::string &msg);
void logError(const std::string &msg);

他们不应该添加或指定更多的上下文信息。无论如何,没有人能够使用这些信息,所以不要过度设计它。
如果你开始向日志调用中添加更多信息,那么使用它的客户端代码就会变得更加难以重用。通常情况下,当组件在不同的抽象层次上被使用时,你会看到这种情况。特别是当一些低级代码提供的调试信息只与较高层次有关时。
这并不会强制你的日志实现(甚至是日志实现符合的接口!)做出任何改变,因此你可以随时进行更改。
更新:
就标记而言,那是一个高级问题。我猜想它不属于日志记录,但这并不重要。
将其从日志消息规范中排除。低级代码不应该关心你或你的经理是谁。
我不知道你在示例中如何指定X或Y。从我们得到的描述中,你如何做到这一点并不明显。我只是为了演示而使用字符串,但如果可能的话,你应该用一些类型安全的东西来替换它。
如果这总是开启的,那么只拥有一个实例上下文(可能是一个全局变量)可能是适当的。当你登录时,设置上下文并忘记它。如果它从未被设置过,则要极其严格地抛出异常。如果不能在没有设置时抛出异常,则它就没有总是开启。
void setLoggingContext("X:");

如果在不同的抽象级别上发生变化,我会考虑使用基于堆栈的RAII实现。

LoggingTag tag("X:");

我不确定在不同的堆栈帧传递不同值的情况下您的要求是什么。对于不同的用例,我可以看出堆栈的顶部或底部都是合理的选择。

void foo() {
  LoggingTag tag("X:");
  logWarning("foo");
  bar();
  baz();
}

void bar() {
  LoggingTag tag("Y:");
  logWarning("bar");
  baz();
}

void baz() {
  logWarning("baz");
}

无论哪种情况,这都不应影响您如何向日志添加消息。函数baz没有上下文来指定LoggingTag。因此,使用logWarning时不知道标签非常重要。
如果您想基于某种类型进行标记,可以像这样简单处理。
struct LoggingTag {
  LoggingTag(const std::string &tag_) : tag(tag_) {}
  template<typename T>
    static LoggingTag ByType() {
      return LoggingTag(typeid(T).name());
    }
  std::string tag;
};

void foo() {
  LoggingTag tag = LogginTag::ByType<int>();
}

如果有需要,这并不会强制某人使用typeid(T).name(),但它会给你带来方便。


我需要考虑的一个问题是不同组件标记。 - Leo
我脑海中有这样的语法:Log log = GetLogger(MyClassName); log.Warn(...)。我不确定MyClassName应该是什么,如果它是一个字符串,我从哪里获取它呢?如果它是将类名转换为字符串的一些技巧,如何实现? - Leo
@user991339 我完全不明白你在问什么。你是想记录一个类型吗? - Tom Kerr
不,我想将一个标签名称与使用日志的类关联起来。 - Leo
@user991339 更新了我的解决方案。您希望日志记录是声明性的,使用您提供的日志对象会与此相冲突,并且难以维护。 - Tom Kerr
显示剩余2条评论

1

我喜欢这种方法:

class Log {
public:
    virtual logString(const std::string&)=0;
};

template <typename T>
Log& operator<<(Log& logger, const T& object) {
        std::stringstream converter;
        converter << object;
        logger.logString(converter.str());
        return logger;
}

简单快捷!你所需要做的就是重新实现logString方法...

这是一个好主意。你能提供一种实现标记功能的方法吗? - Leo
@user991339 我会给记录器添加一个标签属性和一个布尔值来指示是否为行首,这样在第一次记录时就可以获得标签戳记。您可以针对 endl 情况专门定制 operator<<,以便知道下一个输入是行首。 - André Puel
Boost.Log如何在没有bool或endl的情况下知道何时打印该行? log << "现在肯定"; log << "现在?" << "还是现在?"; - Leo

0

看一下zf_log库。它非常小(约2000行,编译后约10KB),速度很快(请参见README.md中的比较表)。它非常接近您所描述的包装器。它为您提供了一个抽象的API,您可以在项目中使用它,并允许指定要使用的实际日志记录实现。请参见custom_output.c示例,其中syslog用作输出设施。它还可以在库内私下使用,而不会冲突于其他可能使用此库的代码(有关更多信息,请参见ZF_LOG_LIBRARY_PREFIX定义)。 即使它不完全符合您的要求,我想它也可以成为您的包装器的一个很好的例子。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接