文章目录
  1. 1. 想法
  2. 2. 实现
    1. 2.1. 关键就是LOG(ERROR) << "this is error"的内部实现
    2. 2.2. 临时匿名对象
  3. 3. 代码
    1. 3.1. 构造函数
    2. 3.2. 重载operator<<
    3. 3.3. 析构函数
    4. 3.4. WriteLog函数

在我的DBPoolSocketPool当中,使用的日志库就是glog,google的东西好用自然不必说,但是对于我这种小型程序还需要链接一个外部的库,我觉得重量还是有点大。我更加写一个简单的超轻量级别的日志库,就是只要日志库的主要功能就可以了。
为了准备昨天(2015年12月19号)的基金从业资格考试,我的很多空闲时间都被用掉了,关键是还不知道能不能通过:)

想法

昨天下午刚考完试,我就开始构思我的日志库logobj。我的想法是这样的:

  • 轻量级别,就和我以前写的所有obj的代码一样,一个头文件,一个cpp文件就能完成
  • 能够像google的glog那样,用LOG(ERROR) << "this is error"这种形式来打日志。
  • 应该还要加上我常用的编译器内置宏__FILE__ 还有__LINE__
  • 日志当中要有打印这行日志的时间,不是编译时间,而是运行的时候的打日志时间。编译时间可以用编译器内置宏__DATE__来得到,而运行时间就要使用#include <time.h>
  • 日志级别当然至少需要INFOWARNERROR这三个级别
  • 日志的名称和路径可以自己设定,也可以从配置文件当中读取进来。
  • 并不需要异步写。

实现

关键就是LOG(ERROR) << "this is error"的内部实现

首先,按照流的方式是一定不能通过文件描述符直接写进日志当中的。所以只能构建一个buffer,将字符串流当中的数据放到buffer里面,然后再通过dprintf()函数将buffer当中的数据写到日志文件当中去。
这就意味着必须是这样的形式

1
2
3
4
//写buffer
LOG(ERROR) << "this is error"
//从buffer当中取数据放入日志文件
LOG(ERROR).WriteLog();

但是在任何的日志库当中都不可能除了要求调用者传入要写的字符串,还要求调用者再显示地调用一次LOG(ERROR).WriteLog()这种函数。这明显就是设计的不合理。
我本来是想把这个WriteLog()的函数直接整合进LOG(ERROR) << "this is error"当中去,然后等用户输入完成之后,直接调用WriteLog()。但是这样的话,关键问题又来了:我怎么样才能知道用户输入完成了?不可能要让用户在字符串流的结束位置加上什么特定的标示符号来告知程序已经输入完成了。因为这样的话,那还不如上面的方案。起码上面的方案不要用户去记这个标示符号。

这里貌似只能用陈硕老师muduo当中的日志库的思想—–使用临时匿名对象

临时匿名对象

临时匿名对象是这种对象:

1
2
3
4
5
6
7
8
9
10
11
12
class student {
....
};
//这是普通对象
student alice("she", "test");

//这是临时匿名对象,根本没有对象名称
student("he", "hello");

//临时匿名对象一使用完就马上调用析构函数销毁
//上面的临时匿名对象是这样,这里赋值操作完成后也是马上销毁。
student bob = student("bob", "h");

这里就是需要用到临时匿名对象的一旦使用完就马上销毁的这个特性。

想象一下这样的场景: 当我在一行代码处需要打日志的时候,我这样写:

1
LOG(ERROR) << "this is error"

LOG(ERROR)是一个宏,它等价于构建一个临时匿名对象,构造函数成功后,再把”this is error”这句信息传递给这个临时匿名对象,然后马上就析构释放这个对象。

我可以在析构函数当中调用WriteLog(),这样的话就解决了上面提到的问题:我如何知道用户已经输入完成了?当然能知道,在临时匿名对象调用析构函数的时候,100%确定用户已经输入完成了。这个时候就能使用WriteLog()来把字符串内容写进日志文件当中去。

这里有第2个问题:我能不能不使用临时匿名对象,但是我在我的析构函数当中调用WriteLog()函数?具体来说,就是下面的代码逻辑有无问题?

1
2
3
4
5
6
7
if (pointer == NULL) {
//使用具有名字的对象
LogObj myobj(INFO);
myObj << "this is info";
LogObj myerror(ERROR);
myerror << "this is error";
}

这样的话确实在退出作用域的时候,两个对象都将被析构,同时都将调用WriteLog()函数来将日志写到文件当中去。但是需要知道的是C++对于栈中的具名对象,先创建的后销毁。也就是说myerror这个对象的销毁操作在myobj对象之前。那么就是说在程序当中先打的日志反而会更晚写到日志文件当中。这就相当与扰乱了日志文件的顺序,变得十分不好阅读。而日志文件如果不能有效阅读,那将是致命的。

综上所述,解决第一个问题的技术只能使用临时匿名对象。

那么接下来的问题就是要设计对象的构造函数和析构函数。

代码

这个logobj的设计是这样的,private成员变量有日志的名称,日志保存目录,日志的级别,以及buffer
类当中有6段public。每段public都是一个组件。它们分别是

  • 日志级别以及转换函数
  • 构造函数,析构函数和Dump(这是我写代码的老风格了)
  • 对日志名称和保存路径的Getter和Setter方法,还有获取当前时间的方法
  • buffer的操作:在buffer尾部追加字符串的函数Append,buffer的Getter方法,清空buffer的方法。
  • 重载的<<运算符。这里我并没有像陈硕老师那样大量重载<<运算符,我只对我常用的几个类型进行了重载:unsigned int, int, float, double, const char*, string
  • 将日志从buffer写到文件内的函数WriteLog()

这里需要着重说的是这么几个函数:

构造函数

1
2
3
4
5
explicit LogObj(const string& logName,
const string& savePath,
LogLevel logLevel,
const char* FILE,
int LINE);

构造函数传入的参数中logName,savePath,logLevel都是用来初始化成员变量的。而FILE就是当前的文件名,LINE就是当前代码行的行数。这两个都是通过编译器的内置宏传进来的。

1
2
3
4
5
6
7
#define G_LOGNAME "undefined_.log"
#define G_SAVEPATH "../log"

#define LOG(level) LogObj(G_LOGNAME, G_SAVEPATH, LogObj::level, __FILE__, __LINE__)

//有了这个宏,调用LOG(ERROR)的时候就是构造一个临时匿名对象。

拿到了logName之后做的第一件事情就是加上后缀,后缀为根据函数GetCurrentTime(0)取到的当前时间。以年月日为格式。比如:

1
undefined_.log20151220

然后把当前时间(年月日时分秒格式),当前文件名,当前行号,还有日志级别先写入到buffer当中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
LogObj::LogObj(const string& logName,
const string& savePath,
LogLevel logLevel,
const char* FILE,
int LINE)
: logName_(logName),
savePath_(savePath),
logLevel_(logLevel) {
logName_ += GetCurrentTime(0);
memset(buffer_, 0, 2048);
//获取到当前的时间,再把__FILE,__LINE__ 还有日志级别先写入到buffer_当中
string level = LogObj::LogLevelConvert(logLevel);
string now = GetCurrentTime(1);
sprintf(buffer_,
"LogLevel[%s]-----Time:[%s]\nFILE:[%s]-----LINE:[%d]\n",
level.c_str(),
now.c_str(),
FILE,
LINE);
}

重载operator<<

这里仅仅贴出一部分:
千万不要忘记LogObj&当中的这个&符号,如果忘记那么返回的将是拷贝而非引用,这样的话const char* v只是写入到拷贝当中,而不能写到buffer里面。

1
2
3
4
5
6
7
8
LogObj& LogObj::operator <<(const char* v) {
if (v) {
Append(v, strlen(v));
} else {
Append("(null)", 6);
}
return *this;
}

重载的<<操作符将传入的字符串追加到buffer后面,也就是构造函数把当前时间(年月日时分秒格式),当前文件名,当前行号,还有日志级别先写入到buffer当中,传入的字符串就放在buffer尾部。

析构函数

1
2
3
LogObj::~LogObj() {
WriteLog();
}

WriteLog函数

在这个函数当中,只是简单地把buffer当中的数据写到日志文件当中。
注意文件描述符打开的方式为O_APPEND,表示写文件的时候,是把要写的内容追加到文件的末尾。如果没有这个符号,那表示的就是覆盖原有内容。

1
2
3
4
5
6
void LogObj::WriteLog() {
//由于这里需要将savePath_和logName组合起来,我默认的savePath_的目录的结尾是没有/符号的
int fd = open((savePath_ + "/" + logName_).c_str(), O_CREAT|O_RDWR|O_APPEND|O_LARGEFILE, 00640);
dprintf(fd, "%s\n", buffer_);
close(fd);
}

完整代码请见https://github.com/adairjun/MQueue/blob/master/util/logobj.cpp

代码已经同步更新到DBPoolSocketPool

文章目录
  1. 1. 想法
  2. 2. 实现
    1. 2.1. 关键就是LOG(ERROR) << "this is error"的内部实现
    2. 2.2. 临时匿名对象
  3. 3. 代码
    1. 3.1. 构造函数
    2. 3.2. 重载operator<<
    3. 3.3. 析构函数
    4. 3.4. WriteLog函数