tkorays: 未经同意,不得转载。
日水一篇。这里分析下webrtc日志模块的实现,方便后续可以借鉴他的实现。
这里主要介绍webrtc的日志模块,从代码角度看看webrtc的log设计。我们平常直接使用RTC_LOG宏,是不是也想知道其背后实现?通过分析WebRTC的日志模块,可以学习其日志设计的思想。
可以先从调用顺序角度,对log模块有一个大致了解:
这里提供了以下的日志级别:
enum LoggingSeverity {
LS_VERBOSE,
LS_INFO,
LS_WARNING,
LS_ERROR,
LS_NONE,
INFO = LS_INFO,
WARNING = LS_WARNING,
LERROR = LS_ERROR
};
LogCall这个类的作用在于控制logging是否开启,日志流通过const引用参数传入,我们完全可以修改这个实现,完全过滤掉所有输出。
class LogCall final {
public:
// This can be any binary operator with precedence lower than <<.
// We return bool here to be able properly remove logging if
// RTC_DISABLE_LOGGING is defined.
template <typename... Ts>
RTC_FORCE_INLINE bool operator&(const LogStreamer<Ts...>& streamer) {
streamer.Call();
return true;
}
};
代码内主要使用RTC_LOG_FILE_LINE宏来输出日志,LogStreamer作为参数输入到LogCall中:
#define RTC_LOG_FILE_LINE(sev, file, line) \
::rtc::webrtc_logging_impl::LogCall() & \
::rtc::webrtc_logging_impl::LogStreamer<>() \
<< ::rtc::webrtc_logging_impl::LogMetadata(file, line, sev)
#define RTC_LOG(sev) \
!rtc::LogMessage::IsNoop<::rtc::sev>() && \
RTC_LOG_FILE_LINE(::rtc::sev, __FILE__, __LINE__)
最终我们使用RTC_LOG这个宏来输出,如RTC_LOG(INFO)<<"1234";
,从RTC_LOG_FILE_LINE
中我们可以知道,如果LogCall()
返回false,则不会执行流式输出。
LogStreamer是一个日志流类,单条输出在这个流中保存。日志输出通过 « 串联。LogStreamer本身是一个模板,有不同的实现, RTC_LOG_FILE_LINE使用了其默认的实现:
// Base case: Before the first << argument.
template <>
class LogStreamer<> final {
public:
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<std::is_arithmetic<U>::value ||
std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V> operator<<(U arg) const {
return LogStreamer<V>(MakeVal(arg), this);
}
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<!std::is_arithmetic<U>::value &&
!std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V> operator<<(const U& arg) const {
return LogStreamer<V>(MakeVal(arg), this);
}
template <typename... Us>
RTC_FORCE_INLINE static void Call(const Us&... args) {
static constexpr LogArgType t[] = {Us::Type()..., LogArgType::kEnd};
Log(t, args.GetVal()...);
}
};
在执行RTC_LOG(INFO)«“1234”; 的开始,先创建一个LogStreamer<>
对象,这是链式调用中的head,也是链式回溯中最后负责组装字符串的tail。链式调用后续输入参数都会创建一个新的对象(根据模板参数实例化一个模板),并将打印的信息以及当前LogStreamer作为参数传入到下一个LogStreamer对象,具体实现见LogStreamer<V>operator<<(U arg) const
成员函数以及LogStreamer的构造函数。
RTC_LOG(INFO)<<"1234"
; 里面的”1234”会通过以下模板生成一个新的对象LogStreamer<std::string>,传入上一个LogStreamer和“1234”参数。模板如下:
// Inductive case: We've already seen at least one << argument. The most recent
// one had type `T`, and the earlier ones had types `Ts`.
template <typename T, typename... Ts>
class LogStreamer<T, Ts...> final {
public:
RTC_FORCE_INLINE LogStreamer(T arg, const LogStreamer<Ts...>* prior)
: arg_(arg), prior_(prior) {}
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<std::is_arithmetic<U>::value ||
std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V, T, Ts...> operator<<(U arg) const {
return LogStreamer<V, T, Ts...>(MakeVal(arg), this);
}
template <typename U,
typename V = decltype(MakeVal(std::declval<U>())),
absl::enable_if_t<!std::is_arithmetic<U>::value &&
!std::is_enum<U>::value>* = nullptr>
RTC_FORCE_INLINE LogStreamer<V, T, Ts...> operator<<(const U& arg) const {
return LogStreamer<V, T, Ts...>(MakeVal(arg), this);
}
template <typename... Us>
RTC_FORCE_INLINE void Call(const Us&... args) const {
prior_->Call(arg_, args...);
}
private:
// The most recent argument.
T arg_;
// Earlier arguments.
const LogStreamer<Ts...>* prior_;
};
通过上面的模板结构,我们看到一个指针const LogStreamer
template <typename... Us>
RTC_FORCE_INLINE static void Call(const Us&... args) {
static constexpr LogArgType t[] = {Us::Type()..., LogArgType::kEnd};
Log(t, args.GetVal()...);
}
这里将所有LogStreamer的参数作为模板参数,放到模板参数列表中。但是最终还是通过c语言的可变参数函数(c调用约定)来输出。 另外输出的变量在这里不是使用裸类型,而是稍作封装,见MakeVal。个人理解,这个应该是确保模板能正确匹配,对于字符串这些类型处理不会出错。 比较特殊的一个输出变量是LogMetaData,他主要包含了文件名、行号、日志级别信息。LogMetadata 是最后一个参数,见RTC_LOG_FILE_LINE,因此它也是第一个被递归输出的,见Log函数。
class LogMetadata {
public:
LogMetadata(const char* file, int line, LoggingSeverity severity)
: file_(file),
line_and_sev_(static_cast<uint32_t>(line) << 3 | severity) {}
LogMetadata() = default;
const char* File() const { return file_; }
int Line() const { return line_and_sev_ >> 3; }
LoggingSeverity Severity() const {
return static_cast<LoggingSeverity>(line_and_sev_ & 0x7);
}
private:
const char* file_;
// Line number and severity, the former in the most significant 29 bits, the
// latter in the least significant 3 bits. (This is an optimization; since
// both numbers are usually compile-time constants, this way we can load them
// both with a single instruction.)
uint32_t line_and_sev_;
};
第一个LogStreamer使用Log来输出日志,主要是可变参数处理,函数的第一个参数为输出的日志字段类型列表,后续为可变参数。下面主要是一些参数处理:
void Log(const LogArgType* fmt, ...) {
va_list args;
va_start(args, fmt);
LogMetadataErr meta;
const char* tag = nullptr;
switch (*fmt) {
case LogArgType::kLogMetadata: {
meta = {va_arg(args, LogMetadata), ERRCTX_NONE, 0};
break;
}
case LogArgType::kLogMetadataErr: {
meta = va_arg(args, LogMetadataErr);
break;
}
#ifdef WEBRTC_ANDROID
case LogArgType::kLogMetadataTag: {
const LogMetadataTag tag_meta = va_arg(args, LogMetadataTag);
meta = { {nullptr, 0, tag_meta.severity}, ERRCTX_NONE, 0};
tag = tag_meta.tag;
break;
}
#endif
default: {
RTC_NOTREACHED();
va_end(args);
return;
}
}
LogMessage log_message(meta.meta.File(), meta.meta.Line(),
meta.meta.Severity(), meta.err_ctx, meta.err);
if (tag) {
log_message.AddTag(tag);
}
for (++fmt; *fmt != LogArgType::kEnd; ++fmt) {
switch (*fmt) {
case LogArgType::kInt:
log_message.stream() << va_arg(args, int);
break;
case LogArgType::kLong:
log_message.stream() << va_arg(args, long);
break;
case LogArgType::kLongLong:
log_message.stream() << va_arg(args, long long);
break;
case LogArgType::kUInt:
log_message.stream() << va_arg(args, unsigned);
break;
case LogArgType::kULong:
log_message.stream() << va_arg(args, unsigned long);
break;
case LogArgType::kULongLong:
log_message.stream() << va_arg(args, unsigned long long);
break;
case LogArgType::kDouble:
log_message.stream() << va_arg(args, double);
break;
case LogArgType::kLongDouble:
log_message.stream() << va_arg(args, long double);
break;
case LogArgType::kCharP: {
const char* s = va_arg(args, const char*);
log_message.stream() << (s ? s : "(null)");
break;
}
case LogArgType::kStdString:
log_message.stream() << *va_arg(args, const std::string*);
break;
case LogArgType::kStringView:
log_message.stream() << *va_arg(args, const absl::string_view*);
break;
case LogArgType::kVoidP:
log_message.stream() << rtc::ToHex(
reinterpret_cast<uintptr_t>(va_arg(args, const void*)));
break;
default:
RTC_NOTREACHED();
va_end(args);
return;
}
}
va_end(args);
}
这里字符串拼接使用了LogMessage里面定义的StringBuilder。
LogStreamer完成递归后,使用LogMessage写日志。LogMessage中调用LogSink写日志。
LogSink一般由更上层实现,是具体日志写文件或写命令行的实现,用户有很大的定制自由度:
// Virtual sink interface that can receive log messages.
class LogSink {
public:
LogSink() {}
virtual ~LogSink() {}
virtual void OnLogMessage(const std::string& msg,
LoggingSeverity severity,
const char* tag);
virtual void OnLogMessage(const std::string& message,
LoggingSeverity severity);
virtual void OnLogMessage(const std::string& message) = 0;
private:
friend class ::rtc::LogMessage;
#if RTC_LOG_ENABLED()
// Members for LogMessage class to keep linked list of the registered sinks.
LogSink* next_ = nullptr;
LoggingSeverity min_severity_;
#endif
};
LogSink以链表形式,表明一个LogMessage可以写入到多个LogSink中。
需要关注的是,一个全局的LogSink,以及真正做日志字符串拼接的StringBuilder。
在调用完Log函数后,LogMessage会析构,真正的输出结束实在LogMessage的析构函数中,会遍历
LogMessage::~LogMessage() {
FinishPrintStream();
const std::string str = print_stream_.Release();
if (severity_ >= g_dbg_sev) {
#if defined(WEBRTC_ANDROID)
OutputToDebug(str, severity_, tag_);
#else
OutputToDebug(str, severity_);
#endif
}
webrtc::MutexLock lock(&g_log_mutex_);
for (LogSink* entry = streams_; entry != nullptr; entry = entry->next_) {
if (severity_ >= entry->min_severity_) {
#if defined(WEBRTC_ANDROID)
entry->OnLogMessage(str, severity_, tag_);
#else
entry->OnLogMessage(str, severity_);
#endif
}
}
}
这里介绍webrtc的log设计,不是因为他多么优秀,主要还是因为他设计得比较优雅,值得学习。特别是这里LogStreamer的链式调用,可变参数模板、可变参数函数处理比较巧妙,这些都是值得赞叹的。
如果您觉得文章对您有用能够解决您的问题,欢迎您通过扫码进行打赏支持,谢谢!