在服务端编程中,日志是必不可少的。
开发过程中,日志的存在能方便我们调试错误和更好地理解程序;运行过程中,日志能帮助我们诊断系统故障并处理、记录系统运行状态。
(1)日志消息有多种级别(level),如TRACE、DEBUG、INFO、WARN、ERROR、FATAL。日志的输出级别在运行时可调。
代码片段1:返回当前日志级别
文件名:Logging.cc
Logger::LogLevel initLogLevel()
{
if (::getenv("MUDUO_LOG_TRACE")) // 获取环境变量MUDUO_LOG_TRACE
return Logger::TRACE;
else if (::getenv("MUDUO_LOG_DEBUG"))
return Logger::DEBUG;
else
return Logger::INFO;
}
(2)日志类Logger的使用流程
Logger使用时序图如下:
Logger类主要负责日志的级别等,它的内部嵌套类Impl(疑问:为什么要采用内部嵌套类的设计?)则负责实际的实现。使用时,首先构造一个匿名的Logger对象,然后调用stream()函数返回一个LogStream对象,LogStream对象再调用重载的<<运算符来输出日志。事实上,日志先输出到缓冲区,然后才输出到标准输出或文件。匿名的Logger对象在销毁时调用析构函数,析构函数调用g_output和g_flush输出到日志对应的设备。
代码片段2:Logger的析构函数
文件名:Logging.cc
Logger::~Logger()
{
impl_.finish();
const LogStream::Buffer& buf(stream().buffer()); // 获取缓冲区
g_output(buf.data(), buf.length()); // 默认输出到stdout
// 当日志级别为FATAL时,flush设备缓冲区并终止程序
if (impl_.level_ == FATAL)
{
g_flush();
abort();
}
}
Logger类的使用示例:
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__, __LINE__).stream()
LOG_INFO << "info ..."; // 使用方式
muduo::Logger(__FILE__, __LINE__).stream() << "info ..."; // 传递代码所在的文件名和行号参数
(3)重载<<运算符
以输出int类型的<<运算符为例,它并不是直接存放int类型的数据,而是转换为string类型后再存放到buffer:
代码片段3:重载<<运算符
文件名:LogStream.cc
......
// 通过调用convert函数将整数转换为字符串
template<typename T>
size_t convert(char buf[], T value)
{
T i = value;
char* p = buf;
do
{
int lsd = static_cast<int>(i % 10); // 得到最后一个数字,last digit
i /= 10;
// const char digits[] = "9876543210123456789";
// (疑问:digits数组为什么是"9876543210123456789",而不直接赋为"0123456789"?)
// const char* zero = digits + 9;
// 假如此时获取的lsd值为5,指针zero指向digits[]中的'0'
// zero[lsd]再偏移lsd即5个位置,便获取到了字符'5',保存到了buf中
*p++ = zero[lsd];
} while (i != 0);
// 为负数则添加负号
if (value < 0)
{
*p++ = '-';
}
*p = '\0';
std::reverse(buf, p); // 将字符串逆转
return p - buf;
}
......
template<typename T>
void LogStream::formatInteger(T v)
{
// kMaxNumericSize的值为32,即如果buffer的空间足够大
if (buffer_.avail() >= kMaxNumericSize)
{
size_t len = convert(buffer_.current(), v);
buffer_.add(len);
}
}
......
LogStream& LogStream::operator<<(int v)
{
formatInteger(v); // 调用formatInteger()函数
return *this; // 返回LogStream对象的指针
}
(4)FixedBuffer的设计
FixedBuffer的实现为一个模板类,传入一个非类型参数SIZE表示缓冲区的大小。通过成员 data_首地址、cur_指针、end()完成对缓冲区的各项操作,例如:
代码片段4:模版类FixedBuffer的成员函数avail()返回当前可用的空间
文件名:LogStream.h
int avail() const
{
return static_cast<int>(end() - cur_);
}
(5)日志滚动
muduo库日志滚动的条件通常有两个:
文件大小 - 例如每写满1G换下一个文件
时间 - 例如每天零点新建一个文件,不管前一个文件是否写满
I.日志文件文件名的设计
例:logfile_test.20120603-144022.hostname.3605.log
第一部分如“logfile_test”是日志文件的basename;
第二部分如“20120603-144022”是日志的创建时间(UTC时间);
第三部分如“hostname”是主机名称;
第四部分如“3605”是进程id;
最后是日志后缀名“.log”。
代码片段5:获取日志文件名
文件名:LogFile.cc
string LogFile::getLogFileName(const string& basename, time_t* now)
{
string filename;
// 预留basename的size加上64字节的空间
filename.reserve(basename.size() + 64);
filename = basename;
char timebuf[32];
char pidbuf[32];
struct tm tm;
*now = time(NULL);
gmtime_r(now, &tm); // 线程安全,获取日志创建时间
strftime(timebuf, sizeof timebuf, ".%Y%m%d-%H%M%S.", &tm); // 将时间格式化
filename += timebuf;
filename += ProcessInfo::hostname(); // 用到了gethostname()返回主机名
snprintf(pidbuf, sizeof pidbuf, ".%d", ProcessInfo::pid());
filename += pidbuf;
filename += ".log";
return filename;
}
II.日志的滚动实现
代码片段6:日志的滚动
文件名:LogFile.cc
void LogFile::rollFile()
{
time_t now = 0;
string filename = getLogFileName(basename_, &now);
// 注意,这里先除以kRollPerSeconds_、后乘kRollPerSeconds_表示
// 对齐至kRollPerSeconds_(24*60*60)整数倍,也就是时间调整到当天零点。
time_t start = now / kRollPerSeconds_ * kRollPerSeconds_;
// 如果now大于上一次滚动日志文件时间就滚动
if (now > lastRoll_)
{
lastRoll_ = now; // lastRoll_是上一次滚动日志文件时间
lastFlush_ = now; // lastFlush_是上一次日志写入文件时间
startOfPeriod_ = start; // startOfPeriod_是开始记录日志时间(调整至零点的时间)
file_.reset(new File(filename));
}
}
代码片段7:写入日志时,判断是否需要滚动日志
文件名:LogFile.cc
void LogFile::append_unlocked(const char* logline, int len)
{
file_->append(logline, len);
// 写入的字节数大于rollSize_时要滚动
if (file_->writtenBytes() > rollSize_)
{
rollFile();
}
else
{
// 计数值count_超过kCheckTimeRoll_时也要判断是否需要滚动
if (count_ > kCheckTimeRoll_)
{
count_ = 0;
time_t now = ::time(NULL);
time_t thisPeriod_ = now / kRollPerSeconds_ * kRollPerSeconds_;
if (thisPeriod_ != startOfPeriod_)
{
rollFile();
}
// 大于flush的间隔时间时则写入日志,不滚动
else if (now - lastFlush_ > flushInterval_)
{
lastFlush_ = now;
file_->flush();
}
}
else
{
++count_;
}
}
}