工程
先创建一个工程用于运行示例。如何创建工程,参考《WebRTC源码分析之工程-project》,在src\examples\BUILD.gn中添加如下内容:
rtc_executable("webrtc_learn"){
testonly = true
sources = [
"webrtclearn/main.cc"
]
deps = [
"../rtc_base:rtc_base"
]
}
#include "rtc_base/logging.h"
int main()
{
RTC_LOG(INFO) << "hello world";
return 0;
}
日志默认是输出到标准错误stderr
#include "rtc_base/logging.h"
#include
int main()
{
/*显示时间戳*/
rtc::LogMessage::LogTimestamps(true);
/*显示线程id*/
rtc::LogMessage::LogThreads(true);
RTC_LOG(INFO) << "hello world";
Sleep(1);
RTC_LOG(INFO) << "China is great";
return 0;
}
显示的时间从第一条日志开始计时,也可以程序的开始主动调用rtc::LogMessage::LogStartTime()开始计时。
#include "rtc_base/logging.h"
#include "rtc_base/log_sinks.h"
using namespace rtc;
int main()
{
/*日志默认是输出到标准错误的,不调用也是可以的。*/
LogMessage::SetLogToStderr(true);
/*创建日志文件*/
FileRotatingLogSink frls("C:\\Users\\study\\Desktop\\log","webrtc_log",1024,2);
frls.Init();
/*将日志输出到日志文件中,接收WARNING及以上级别的日志。*/
LogMessage::AddLogToStream(&frls, WARNING);
RTC_LOG(INFO) << "information";
RTC_LOG(WARNING) << "warning";
RTC_LOG(LERROR) << "error";
return 0;
}
日志是分等级的,在创建文件流的时候,可以指定接收日志的最低等级。文件中没有接收 RTC_LOG(INFO) << “information”;这条日志。
日志默认输出到标准错误,但可以通过SetLogToStderr()设置是否向标准错误输出。
#include "rtc_base/logging.h"
#include
int main()
{
/*函数执行出错设置该全局变量*/
errno = 6;
RTC_LOG_ERRNO(INFO) << "hello world";
return 0;
}
输出的日志带有错误值,及错误信息。
RTC_LOG源码所在的文件位置:src\rtc_base\logging.h logging.cc
RTC_LOG和RTC_DCHECK的设计是类似的,很多代码也是重复的。在RTC_DCHECK中介绍过的,在这里就不做介绍了。
LogMessage类所在的文件位置:src\rtc_base\logging.h logging.cc
LogMessage类用于接收日志数据,将数据写入标准错误或日志文件。LogMessage类的
绝大部分数据成员和函数成员都是静态的,可以保存全局信息。
typedef std::pair<LogSink*, LoggingSeverity> StreamAndSeverity;
typedef std::list<StreamAndSeverity> StreamList;
static StreamList streams_;
static bool thread_, timestamp_;
static bool log_to_stderr_;
以上的数据均是静态数据成员,相当于类内部的全局变量。
streams_中保存的是文件流,每个文件流都有自己的等级,在输出日志的时候,只要日志等级大于或等于文件流的等级,日志信息就写入文件流中。可以有多个文件流同时接收日志数据。接收日志的文件流需要继承自LogSink类,并覆写OnLogMessage()函数。
thread_、timestamp_分别记录着在打印日志时,是否输出线程id,是否打印时间信息。
log_to_stderr_用于标识是否将日志数据打印到标准错误,默认值是true,表示需要将日志信息输出到标准错误。
rtc::StringBuilder print_stream_;
LoggingSeverity severity_;
std::string extra_;
每条日志都会生成一个LogMessage类对象,用于处理日志。
print_stream_:所有的日志数据都会格式化成字符串,保存在这个变量中。
severity_:记录日志的等级
extra_:若需要输出错误信息,需要将错误信息格式化成字符串保存这个变量中。
#if !defined(NDEBUG)
static LoggingSeverity g_min_sev = LS_INFO;
static LoggingSeverity g_dbg_sev = LS_INFO;
#else
static LoggingSeverity g_min_sev = LS_NONE;
static LoggingSeverity g_dbg_sev = LS_NONE;
#endif
g_min_sev和g_dbg_sev是全局变量,分别保存着输出日志的最小级别和调试时输出的最小日志级别。
/*获取日志开始时间*/
int64_t LogMessage::LogStartTime()
{
static const int64_t g_start = SystemTimeMillis(); /*只初始化一次*/
return g_start;
}
/*获取日志开始墙上的时间*/
uint32_t LogMessage::WallClockStartTime()
{
static const uint32_t g_start_wallclock = time(nullptr); /*只有第一次调用时才初始化*/
return g_start_wallclock;
}
这两个函数用于返回日志开始的时间,一个是相对时间,一个是绝对时间。函数的内部均有一个局部变量用于记录开始的时间,在首次调用函数的时候,才会对局部变量初始化,之后就不再初始化了,这样就记录了首次调用的时间,也就是日志开始的时间。
LogMessage::LogMessage(const char* file, int line, LoggingSeverity sev)
: LogMessage(file, line, sev, ERRCTX_NONE, 0) {}
LogMessage::LogMessage(const char* file,int line,LoggingSeverity sev,LogErrorContext err_ctx,int err)
: severity_(sev)
{
/*如果时间戳开启,则在日志中输出时间戳信息*/
if (timestamp_)
{
int64_t time = TimeDiff(SystemTimeMillis(), LogStartTime());
WallClockStartTime(); /*初始化墙上开始时钟*/
print_stream_ << "[" << rtc::LeftPad('0', 3, rtc::ToString(time / 1000))
<< ":" << rtc::LeftPad('0', 3, rtc::ToString(time % 1000))
<< "] ";
}
/*如果开启打印线程名字,则在日志中会输出打印日志的线程名。*/
if (thread_)
{
PlatformThreadId id = CurrentThreadId();
print_stream_ << "[" << id << "] ";
}
/*输出日志函数调用时,所在的文件名和所在行号。*/
if (file != nullptr)
{
/*文件名只打印名字不打印路径*/
print_stream_ << "(" << FilenameFromPath(file) << ":" << line << "): ";
}
/*输出错误值,及其对应的错误信息。*/
if (err_ctx != ERRCTX_NONE)
{
char tmp_buf[1024];
SimpleStringBuilder tmp(tmp_buf);
tmp.AppendFormat("[0x%08X]", err); /*添加错误码*/
switch (err_ctx)
{
case ERRCTX_ERRNO:
tmp << " " << strerror(err); /*添加错误信息*/
break;
default:
break;
}
/*保存的是与错误相关的信息*/
extra_ = tmp.str();
}
}
每条待输出的日志,都会单独生成一个LogMessage类对象,用于处理这条日志。
print_stream_中保存着格式化的日志数据,要输出的日志数据,经过格式化后,存放到这个变量。处理完所有的日志数据后,将print_stream_一次性输出。
调用WallClockStartTime()函数并没有用于获取时间,而是当主程序中没有主动调用初始化时间时,在输出第一条日志时初始化时间。
在构建LogMessage类时,需要根据数据成员而决定是否显示日志的时间、线程id等信息。
因为一条日志用一个单独的LogMessage类对象处理,在处理日志的过程中,所有需要打印的数据都会被格式化成字符串保存在类对象中。在LogMessage类对象析构时,需要将日志数据输出到指定文件流或标准错误。
void LogMessage::FinishPrintStream()
{
if (!extra_.empty())
print_stream_ << " : " << extra_;
print_stream_ << "\n";
}
将错误信息添加到print_stream_中,此时print_stream_中保存着,日志数据经格式化后的所有数据。
/*向标准错误输出日志*/
void LogMessage::OutputToDebug(const std::string& str,LoggingSeverity severity)
{
bool log_to_stderr = log_to_stderr_;
if (log_to_stderr)
{
fprintf(stderr, "%s", str.c_str()); /*打印日志*/
fflush(stderr);
}
}
根据log_to_stderr_的值,将日志数据输出到标准错误。
LogMessage::~LogMessage()
{
/*添加错误信息*/
FinishPrintStream();
const std::string str = print_stream_.Release();
/*日志的级别大于调试的最小日志级别*/
if (severity_ >= g_dbg_sev)
{
OutputToDebug(str, severity_); /*向标准错误输出日志*/
}
/*上锁访问stream_*/
CritScope cs(&g_log_crit);
for (auto& kv : streams_) /*向指定的流中输出日志*/
{
if (severity_ >= kv.second)
{
kv.first->OnLogMessage(str, severity_);
}
}
}
每文件流都有自己的最小日志级别,而g_dbg_sev保存的最小日志级别用于控制标准错误的最小日志级别。当日志的级别小于g_dbg_sev时,就不再向标准错误输出了。
在析构器中先是将日志输出到标准错误,然后遍历streams_保存的数据流,若日志级别不小于文件流级别,则将日志输出到流文件。streams_可以保存多个文件流,一条日志可以写入多个文件。
streams_存在竞争冒险,在一个线程添加文件流时,另一个线程就不可以访问streams_。每个线程修改或访问streams_时,都需要独占streams_,所以需要加锁。
void LogMessage::UpdateMinLogSeverity() RTC_EXCLUSIVE_LOCKS_REQUIRED(g_log_crit)
{
LoggingSeverity min_sev = g_dbg_sev;
/*遍历所有流,获取最小的日志等级。*/
for (const auto& kv : streams_)
{
const LoggingSeverity sev = kv.second;
min_sev = std::min(min_sev, sev);
}
g_min_sev = min_sev;
}
从所有的文件流和标准错误中,获取最小的日志级别。在处理日志时,若日志的级别小于最小日志级别,则该日志不需要打印,放弃处理。
/*设置是否显示线程id*/
void LogMessage::LogThreads(bool on)
{
thread_ = on;
}
/*设置是否显示时间信息*/
void LogMessage::LogTimestamps(bool on)
{
timestamp_ = on;
}
/*设置调试日志的最低级别*/
void LogMessage::LogToDebug(LoggingSeverity min_sev)
{
g_dbg_sev = min_sev;
CritScope cs(&g_log_crit);
UpdateMinLogSeverity();
}
/*设置是否将日志打印到标准错误流*/
void LogMessage::SetLogToStderr(bool log_to_stderr)
{
log_to_stderr_ = log_to_stderr;
}
这些函数很简单,注释已经解释的很清楚了。
void LogMessage::ConfigureLogging(const char* params)
{
LoggingSeverity current_level = LS_VERBOSE;
LoggingSeverity debug_level = GetLogToDebug();
std::vector<std::string> tokens;
tokenize(params, ' ', &tokens);
for (const std::string& token : tokens)
{
if (token.empty())
continue;
if (token == "tstamp")
{
LogTimestamps(); /*显示时间*/
} else if (token == "thread")
{
LogThreads(); /*显示线程id*/
} else if (token == "verbose")
{
current_level = LS_VERBOSE;
} else if (token == "info")
{
current_level = LS_INFO;
} else if (token == "warning")
{
current_level = LS_WARNING;
} else if (token == "error")
{
current_level = LS_ERROR;
} else if (token == "none")
{
current_level = LS_NONE;
} else if (token == "debug")
{
debug_level = current_level; /*更改调试时日志最小级别*/
}
}
LogToDebug(debug_level);
}
通过字符串的方式配置LogMessage。tokenize()函数会把字符串按照空格进行分割,分割后的字符串存放在vector中。
使用示例如下:
#include "rtc_base/logging.h"
int main()
{
char buf[] = "tstamp thread warning debug";
rtc::LogMessage::ConfigureLogging(buf);
RTC_LOG(INFO) << "information"; /*级别太低,不会被打印。*/
RTC_LOG(WARNING) << "warning";
RTC_LOG(LERROR) << "error";
return 0;
}
配置的日志显示时间、线程id,并且只打印WARNING
级别及以上级别的日志。
/*返回流对象*/
rtc::StringBuilder& LogMessage::stream()
{
return print_stream_;
}
/*日志的最小级别*/
int LogMessage::GetMinLogSeverity()
{
return g_min_sev;
}
/*调试时日志可输出的最小级别*/
LoggingSeverity LogMessage::GetLogToDebug()
{
return g_dbg_sev;
}
/*获取指定流的等级,若为nullptr则返回所有流中最小级别。*/
int LogMessage::GetLogToStream(LogSink* stream)
{
CritScope cs(&g_log_crit);
LoggingSeverity sev = LS_NONE;
for (auto& kv : streams_)
{
if (!stream || stream == kv.first)
{
sev = std::min(sev, kv.second);
}
}
return sev;
}
/*判断severity级别日志是否可以打印*/
bool LogMessage::IsNoop(LoggingSeverity severity)
{
/*若日志级别大于规定的级别,则返回false。*/
if (severity >= g_dbg_sev || severity >= g_min_sev)
return false;
CritScope cs(&g_log_crit);
if (streams_.size() > 0)
return false;
return true;
}
/*添加接收日志的文件流*/
void LogMessage::AddLogToStream(LogSink* stream, LoggingSeverity min_sev)
{
CritScope cs(&g_log_crit);
streams_.push_back(std::make_pair(stream, min_sev));
/*更新日志的最小级别*/
UpdateMinLogSeverity();
}
/*删除接收日志的流*/
void LogMessage::RemoveLogToStream(LogSink* stream)
{
CritScope cs(&g_log_crit);
/*遍历流列表,删除指定的流。*/
for (StreamList::iterator it = streams_.begin(); it != streams_.end(); ++it)
{
if (stream == it->first)
{
streams_.erase(it);
break;
}
}
/*更新日志的最小级别*/
UpdateMinLogSeverity();
}
添加或删除文件流后,需要更新日志的最小级别,可能删除的流就是唯一的最小,所以需要及时更新。
void Log(const LogArgType* fmt, ...)
{
va_list args;
va_start(args, fmt);
/*变参args的第一个参数是LogMetadata或LogMetadataErr*/
LogMetadataErr meta;
const char* tag = nullptr;
switch (*fmt)
{
case LogArgType::kLogMetadata:
{
/*把LogMetadata包装成LogMetadataErr*/
meta = {va_arg(args, LogMetadata), ERRCTX_NONE, 0}; /*注意ERRCTX_NONE*/
break;
}
case LogArgType::kLogMetadataErr:
{
meta = va_arg(args, LogMetadataErr);
break;
}
default:
{
RTC_NOTREACHED(); /*断言失败,结束进程。*/
va_end(args);
return;
}
}
/*meta.meta.Severity()获取日志等级,判断是否打印该日志。*/
if (LogMessage::IsNoop(meta.meta.Severity()))
{
va_end(args);
return;
}
/*根据LogMetadataErr中的数据构建LogMessage*/
LogMessage log_message(meta.meta.File(), meta.meta.Line(),meta.meta.Severity(), meta.err_ctx, meta.err);
/*将日志数据流入到log_message对象中*/
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);
/*log_message对象离开作用域,调用析构器,在析构器中打印日志数据。*/
}
调用全局函数Log()时,RTC_LOG中数据全部以变参的方式传入至args
中,变参中数据的类型保存在fmt
数组。变参args
中的第一个参数是kLogMetadata类型或kLogMetadataErr类型,若是kLogMetadata类型则转成kLogMetadataErr类。
再根据kLogMetadataErr生成LogMessage对象,之后会将变参args
中的数据,格式化成字符串,保存在LogMessage对象中,在这个函数最后,LogMessage对象离开作用域时,调用析构函数,在析构函数中将日志数据输出到文件中。
log_message.stream() << va_arg(args, int);中,log_message.stream()将返回print_stream_,va_arg(args, int)将从变参中读取一个int类型数据。print_stream_是StringBuilder类对象,底层重载了operator<<()可以将数据以字符串的方式保存到print_stream_中。
LogCall类所在的文件位置:src\rtc_base\logging.h
class LogCall final
{
public:
template <typename... Ts>
RTC_FORCE_INLINE void operator&(const LogStreamer<Ts...>& streamer)
{
streamer.Call();
}
};
LogCall类重载了&运算符,这个运算符接收LogStreamer类对象,并调用其Call()函数。
LogCall类相当于递归处理的终止条件,到达本类后,开始递归返回。
LogStreamer类在RTC_DCHECK
中的介绍过,在处理完所有变参后,RTC_DCHECK
中会调用FatalLog()函数,将所有参数传至这个函数进行下一步处理。此处会调用Log()函数进一步处理所有参数。有关LogStreamer的更多介绍,参看《WebRTC源码分析之断言-RTC_DCHECK》。
#define RTC_LOG(sev) RTC_LOG_FILE_LINE(rtc::sev, __FILE__, __LINE__)
#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)
RTC_LOG(INFO) << "hello world";
将宏展开后:
rtc::webrtc_logging_impl::LogCall() & rtc::webrtc_logging_impl::LogStreamer<>() << rtc::webrtc_logging_impl::LogMetadata(__FILE__, __LINE__, INFO) << "hello world";
去掉命名空间,简化后:
LogCall() & LogStreamer<>() << LogMetadata(__FILE__, __LINE__, INFO) << "hello world";
<<运算符的优先级比&运算符高,且<<运算符的结合性是从左到右。LogMetadata(__FILE__, __LINE__, INFO)将定义一个LogMetadata对象,LogStreamer<>对象会把后面的两个参数一层一层的包裹起来,返回一个新的LogStreamer对象,作为参数传递至LogCall()临时对象的operator&()函数作为参数。然后递归的调用LogStreamer对象的Call()函数,将LogMetadata(__FILE__, __LINE__, INFO)对象和"hello world"组成变参,最终传递至全局函数Log()中,进行下一步的处理。
<<运算符和&运算符的处理过程描述的很简单,在《WebRTC源码分析之断言-RTC_DCHECK》有更为详细的描述。
#define RTC_LOG_ERRNO_EX(sev, err) RTC_LOG_E(sev, ERRNO, err)
#define RTC_LOG_ERRNO(sev) RTC_LOG_ERRNO_EX(sev, errno)
#define RTC_LOG_E(sev, ctx, err) \
rtc::webrtc_logging_impl::LogCall() & \
rtc::webrtc_logging_impl::LogStreamer<>() \
<< rtc::webrtc_logging_impl::LogMetadataErr { \
{__FILE__, __LINE__, rtc::sev}, rtc::ERRCTX_##ctx, (err) \
}
RTC_LOG_ERRNO(INFO) << "hello world";
将宏展开后:
rtc::webrtc_logging_impl::LogCall() & rtc::webrtc_logging_impl::LogStreamer<>() << rtc::webrtc_logging_impl::LogMetadataErr {{__FILE__, __LINE__, rtc::INFO}, rtc::ERRCTX_ERRNO, (errno)} << "hello world";
去掉命名空间,简化后:
LogCall() & LogStreamer<>() << LogMetadataErr {{__FILE__, __LINE__, INFO},ERRCTX_ERRNO, (errno)} << "hello world";
这条宏展开后和上面宏大体相同,只是这里传入Log()函数的变参,第一个参数是LogMetadataErr类型,携带着错误信息。
LogSink类所在的文件位置:src\rtc_base\logging.h logging.cc
class LogSink
{
public:
LogSink() {}
virtual ~LogSink() {}
virtual void OnLogMessage(const std::string& message) = 0;
};
LogSink类是一个接口类,所有需要接收日志的文件流都需要继承自此类,并覆写OnLogMessage()函数。参数message保存的是一条完整的日志,将message写入文件即可。
FileRotatingLogSink类所在的文件位置:src\rtc_base\log_sinks.h log_sinks.cc
WebRTC提供了FileRotatingLogSink类用于以文件的方式存放日志。FileRotatingLogSink是对FileRotatingStream类的进一步包装。FileRotatingStream类在《WebRTC源码分析之流-Stream》中有介绍。
FileRotatingLogSink::FileRotatingLogSink(const std::string& log_dir_path,const std::string& log_prefix,size_t max_log_size,size_t num_log_files)
: FileRotatingLogSink(new FileRotatingStream(log_dir_path,log_prefix,max_log_size,num_log_files)) /*调用下面的构造器*/
{}
/*构造器*/
FileRotatingLogSink::FileRotatingLogSink(FileRotatingStream* stream)
: stream_(stream)
{
RTC_DCHECK(stream);
}
在创建FileRotatingStream对象时,需要提供日志文件所在的目录,日志文件名的前缀,单个文件的大小和文件的数量。
bool FileRotatingLogSink::Init()
{
return stream_->Open();
}
创建FileRotatingStream对象后,在往文件中写入数据之前,需要调用Init()函数,调用后文件才会创建。
void FileRotatingLogSink::OnLogMessage(const std::string& message)
{
if (stream_->GetState() != SS_OPEN)
{
std::fprintf(stderr, "Init() must be called before adding this sink.\n");
return;
}
/*将日志写入文件*/
stream_->WriteAll(message.c_str(), message.size(), nullptr, nullptr);
}
这是子类覆写的虚函数,在LogMessage::~LogMessage()函数的kv.first->OnLogMessage(str, severity_);语句会调用本函数,将日志写入文件中。
CallSessionFileRotatingLogSink类所在的文件位置:src\rtc_base\log_sinks.h log_sinks.cc
CallSessionFileRotatingLogSink继承自FileRotatingStream,但将底层的文件流换成了CallSessionFileRotatingStream。CallSessionFileRotatingLogSink生成的日志文件,当写入的数据超过了日志文件的总大小,日志文件将只保留开始的日志和最后的日志。CallSessionFileRotatingLogSink类的更多信息,在《WebRTC源码分析之流-Stream》中有介绍。
本文介绍RTC_LOG源码时是以《WebRTC源码分析之断言-RTC_DCHECK》为前提的。本文介绍了WebRTC中如何使用日志函数RTC_LOG,以及其底层实现。