日志的各个级别
这一篇写的特别好,https://blog.youkuaiyun.com/qq_28114615/article/details/103210148.
代码分析
SourceFile类
在logging.h中,声明了一个SourceFile类,这个类的作用就是给定一个字符串,把字符串中文件的文件名(basename)提取出来,就是类似于Python中的os.path.basename这个函数,该类具体为:
class SourceFile
{
public:
template<int N>
//两个构造函数,要么重载指针,要么重载引用
SourceFile(const char (&arr)[N])
: data_(arr),
size_(N-1)
{
//获取具体的文件名
const char* slash = strrchr(data_, '/'); // builtin function
if (slash)
{
data_ = slash + 1;
size_ -= static_cast<int>(data_ - arr);
}
}
explicit SourceFile(const char* filename)
: data_(filename)
{
const char* slash = strrchr(filename, '/');
if (slash)
{
data_ = slash + 1;
}
size_ = static_cast<int>(strlen(data_));
}
const char* data_;
int size_;
};
类中只有两个构造函数,先说第二个,参数是const char *类型的,这里先简要说明下字符串数组和常量字符串的区别,先看下面两行代码
char *filename1 ="888/9991.cpp";
char filename2[] ="888/9992.cpp";
第一行表示char *类型的指针filename1指向一个字符串,指针存储的值代表这个地址,那么这个地址的指针就是"888/9991.cpp"存储的首地址,即&“888/9991.cpp”[0],这个表达式是可以实际运行的,C/C++语言中该字符串存储在全局区(静态区),第二行中filename2是一个字符串数组,filename2存储在栈区,filename2的大小就是字符串的长度+1,因为默认后面添加了一个’\0‘。
当数组作为形参的类型时,便会退化为指针,这是我们都知道的,但是如果退化为指针,这就意味着我们需要要么多传一个描述数组长度的参数,要么重新计算下数组的长度(仅对字符串数组而言),那么有没有便利的方法呢,那就是传引用,一个问题又来了,char [10] 的引用是char (&)[10],char [9] 的引用是char (&)[9],具有很大的变化性,因此可以采用模板中非类型参数推导实现,那就是SourceFile的第一个构造函数:
template<int N>
//两个构造函数,要么重载指针,要么重载引用
SourceFile(const char (&arr)[N])
由于数组中加了一个’\0’,所以长度比真实的多了1个,因此函数体内N-1,如此操作,当传入的是一个字符串数组时,我们就不用再算一下它的长度了,真的非常巧妙,再次见识到引用+模板的强大威力。
Impl类
class Impl
{
public:
typedef Logger::LogLevel LogLevel;
Impl(LogLevel level, int old_errno, const SourceFile& file, int line);
void formatTime();
void finish();
Timestamp time_;
LogStream stream_;
LogLevel level_;
int line_;
SourceFile basename_;
};
这里的Timestamp类不是很难,一个获取时间的类,里面包含各种字符串的转换等,这里需要介绍一个LogStream类,介绍LogStream类之前,FixedBuffer类也需要介绍一下:
template<int SIZE>
class FixedBuffer : noncopyable//不能复制
{
public:
FixedBuffer()
: cur_(data_)//让cur_指向data_数组
{
//初始化,先设置回调函数为空,显然cookieStart只能是static
setCookie(cookieStart);
}
~FixedBuffer()
{
setCookie(cookieEnd);
}
void append(const char* /*restrict*/ buf, size_t len)
{
// FIXME: append partially
//判断空间够不够,够的话将字符串复制到data_中(cur_后面)
//当然这里,如果不够的话,默认是不处理的
if (implicit_cast<size_t>(avail()) > len)
{
memcpy(cur_, buf, len);
cur_ += len;
}
}
const char* data() const { return data_; }
//当前存储的字符串的长度
int length() const { return static_cast<int>(cur_ - data_); }
// write to data_ directly
char* current() { return cur_; }
//还剩多少空间,指针相减类型伪ptrdiff_t,转化为int类型
int avail() const { return static_cast<int>(end() - cur_); }
void add(size_t len) { cur_ += len; }
void reset() { cur_ = data_; }
void bzero() { memZero(data_, sizeof data_); }
// for used by GDB
const char* debugString();
void setCookie(void (*cookie)()) { cookie_ = cookie; }
// for used by unit test
string toString() const { return string(data_, length()); }
StringPiece toStringPiece() const { return StringPiece(data_, length()); }
private:
const char* end() const { return data_ + sizeof data_; }
// Must be outline function for cookies.
static void cookieStart();
static void cookieEnd();
//明显是一个回调函数
void (*cookie_)();
char data_[SIZE];
char* cur_;
};
这个类还好,可以说是一个简化版的vector,正式进入LogStream类:
class LogStream : noncopyable
{
typedef LogStream self;
public:
//缓冲区的大小是4K
typedef detail::FixedBuffer<detail::kSmallBuffer> Buffer;
self& operator<<(bool v)
{
buffer_.append(v ? "1" : "0", 1);
return *this;
}
//一系列的运算符重载,把输入转换成字符串加入缓冲区中
self& operator<<(short);
self& operator<<(unsigned short);
self& operator<<(int);
self& operator<<(unsigned int);
self& operator<<(long);
self& operator<<(unsigned long);
self& operator<<(long long);
self& operator<<(unsigned long long);
self& operator<<(const void*);
self& operator<<(float v)
{
*this << static_cast<double>(v);
return *this;
}
self& operator<<(double);
// self& operator<<(long double);
self& operator<<(char v)
{
buffer_.append(&v, 1);
return *this;
}
// self& operator<<(signed char);
// self& operator<<(unsigned char);
self& operator<<(const char* str)
{
if (str)
{
buffer_.append(str, strlen(str));
}
else
{
buffer_.append("(null)", 6);
}
return *this;
}
self& operator<<(const unsigned char* str)
{
return operator<<(reinterpret_cast<const char*>(str));
}
self& operator<<(const string& v)
{
buffer_.append(v.c_str(), v.size());
return *this;
}
self& operator<<(const StringPiece& v)
{
buffer_.append(v.data(), v.size());
return *this;
}
self& operator<<(const Buffer& v)
{
*this << v.toStringPiece();
return *this;
}
void append(const char* data, int len) { buffer_.append(data, len); }
const Buffer& buffer() const { return buffer_; }
void resetBuffer() { buffer_.reset(); }
private:
void staticCheck();
template<typename T>
void formatInteger(T);
Buffer buffer_;
static const int kMaxNumericSize = 32;
};
依靠的类基本上介绍完毕,继续Logging.h:
extern Logger::LogLevel g_logLevel;
inline Logger::LogLevel Logger::logLevel()
{
return g_logLevel;
}
根据外部的g_LogLevel设置Logger::logLevel(),下面的代码就是根据指定的g_logLevel来设置日志的级别了 下面的代码就是定义LOG_TRACE,LOG_DEBUG等临时对象,该对象属于LogStream类:
#define LOG_TRACE if (muduo::Logger::logLevel() <= muduo::Logger::TRACE) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::TRACE, __func__).stream()
#define LOG_DEBUG if (muduo::Logger::logLevel() <= muduo::Logger::DEBUG) \
muduo::Logger(__FILE__, __LINE__, muduo::Logger::DEBUG, __func__).stream()
#define LOG_INFO if (muduo::Logger::logLevel() <= muduo::Logger::INFO) \
muduo::Logger(__FILE__, __LINE__).stream()
#define LOG_WARN muduo::Logger(__FILE__, __LINE__, muduo::Logger::WARN).stream()
#define LOG_ERROR muduo::Logger(__FILE__, __LINE__, muduo::Logger::ERROR).stream()
#define LOG_FATAL muduo::Logger(__FILE__, __LINE__, muduo::Logger::FATAL).stream()
#define LOG_SYSERR muduo::Logger(__FILE__, __LINE__, false).stream()
#define LOG_SYSFATAL muduo::Logger(__FILE__, __LINE__, true).stream()
const char* strerror_tl(int savedErrno);
LogFile类
AppendFile类
分析到上面,我们发现,上面这么多类实现的就是一个往缓冲区data_添加字符串的操作,没有涉及文件、输出这些,下面我们进行下面这部分的内容:
class AppendFile : noncopyable
{
public:
explicit AppendFile(StringArg filename);
~AppendFile();
void append(const char* logline, size_t len);
void flush();
off_t writtenBytes() const { return writtenBytes_; }
private:
size_t write(const char* logline, size_t len);
FILE* fp_;
char buffer_[64*1024];
off_t writtenBytes_;
};
StringArg类
一个简单的小类,目的就是传入字符串或者string对象时获取字符串指针
// For passing C-style string argument to a function.
class StringArg // copyable
{
public:
StringArg(const char* str)
: str_(str)
{ }
StringArg(const string& str)
: str_(str.c_str())
{ }
const char* c_str() const { return str_; }
private:
const char* str_;
};
扫清了上面一个障碍之后继续读代码,首先构造函数:
FileUtil::AppendFile::AppendFile(StringArg filename)
: fp_(::fopen(filename.c_str(), "ae")), // 'e' for O_CLOEXEC
writtenBytes_(0)
{
assert(fp_);
::setbuffer(fp_, buffer_, sizeof buffer_);
// posix_fadvise POSIX_FADV_DONTNEED ?
}
作用:使用标准的ISO C中的fopen打开(创建)一个文件,并将缓冲区设置为buffer_的大小(64KB)。这里没有用Linux中的open函数,因为带缓冲区,所以肯定不是线程安全的。
析构函数就是关闭这个文件指针,下面讲一下:
void FileUtil::AppendFile::append(const char* logline, const size_t len)
{
size_t written = 0;
while (written != len)
{
size_t remain = len - written;
//注意:这里的write不是linux下的write,而是调用了自己写的函数
size_t n = write(logline + written, remain);
if (n != remain)
{
int err = ferror(fp_);
if (err)
{
fprintf(stderr, "AppendFile::append() failed %s\n", strerror_tl(err));
break;
}
}
written += n;
}
writtenBytes_ += written;
}
AppendFile::write函数:
size_t FileUtil::AppendFile::write(const char* logline, size_t len)
{
// #undef fwrite_unlocked
return ::fwrite_unlocked(logline, 1, len, fp_);
}
由上可见,该函数为无锁写入一个文件fp_中,总结一下,AppendFile就是创建一个文件,设置缓冲区,并往里写字符串,下面开始正式介绍:
LogFile类
class LogFile : noncopyable
{
public:
LogFile(const string& basename,
off_t rollSize,
bool threadSafe = true,
int flushInterval = 3,
int checkEveryN = 1024);
~LogFile();
void append(const char* logline, int len);
void flush();
bool rollFile();
private:
void append_unlocked(const char* logline, int len);
static string getLogFileName(const string& basename, time_t* now);
//日志文件
const string basename_;
//日志文件达到rollSize_后换一个新文件
const off_t rollSize_;
const int flushInterval_;
const int checkEveryN_;
int count_;
std::unique_ptr<MutexLock> mutex_;
//上一次记录日志的时间
time_t startOfPeriod_;
//上一次滚动日志的时间
time_t lastRoll_;
//上一次日志写入文件的时间
time_t lastFlush_;
std::unique_ptr<FileUtil::AppendFile> file_;
const static int kRollPerSeconds_ = 60*60*24;
};
LogFile类中封装了一个AppendFile类,类中通过append等函数往append_file对象中添加字符串,添加的过程中对添加的日志进行计数,计时等,超过一定限制就重新生成一个日志文件。下面将结合一个muduo中给出的示例讲解一下如何实现一个完整的日志输出功能(LogFile_test.cc):
#include "muduo/base/LogFile.h"
#include "muduo/base/Logging.h"
#include <unistd.h>
//g_logFile所指的对象不能被复制
std::unique_ptr<muduo::LogFile> g_logFile;
//输出函数,将msg字符串添加到g_logFile所指的对象中
void outputFunc(const char* msg, int len)
{
g_logFile->append(msg, len);
}
//冲洗缓冲区
void flushFunc()
{
g_logFile->flush();
}
int main(int argc, char* argv[])
{
char name[256] = { '\0' };
strncpy(name, argv[0], sizeof name - 1);
//一个LogFile对象
g_logFile.reset(new muduo::LogFile(::basename(name), 200*1000));
//这里将Logger中的回调函数设置为outputFunc
muduo::Logger::setOutput(outputFunc);
muduo::Logger::setFlush(flushFunc);
muduo::string line = "1234567890 abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ ";
for (int i = 0; i < 10000; ++i)
{
//LOG_INFO生成一个临时的Logger对象,该对象调用operator<<将line等变量
//放到内置的impl_.stream_中,该对象在析构的时候,一个完整的日志即被生
//成,这个日志将调用Logger类中的静态函数将日志输出。
//这里,g_logFile接住了这个输出,因此达到了最终的目的
LOG_INFO << line << i;
usleep(1000);
}
}