目录
一、引言
上一章我们讲述了日志器的大体框架,日志目录和文件的建立与时间、文件、行号的标识,格式化字符串。这次我们来将和格式化字符串相关的日志器的落地。这也是为了好调试格式化字符串。毕竟我们写完一部分代码最好立刻、马上、赶快做一次全面的测试。不然在总的部分出了问题又要缝缝补补就非常难受。所以我们程序员一定要养成一个良好的写完就测试的习惯。
我们之前写的代码最后一个按照一个.hpp文件存放好。因为是日志,所以我们直接打印出来就知道结果信息对不对了。
不过这一次就不一样了。我们要设置日志的输出方向,要通过文件中的内容进行判断。
二、 日志等级和日志信息
这回我们直接从最简单的开始。日志等级是我们一个筛选的标准,在程序出错时,我们筛选出可能会导致程序出错的代码设置好输出方向从输出的方向中去查看。
不同公司的日志等级各有差异,所以我们采用一般常用的日志等级。
[OFF]:关闭
[DEBUG]:调试,调试时的关键信息输出。
[INFO]:提示,普通的提示型日志信息。
[WARN]:警告,不影响运行,但是需要注意⼀下的日志。
[ERROR]:错误,程序运行出现错误的日志
[FATAL]:致命,⼀般是代码异常导致程序无法继续推进运行的⽇志
日常我们用到的日志等级大概就是这些。
| OFF | 关闭 |
| DEBUG | 调试,调试时的关键信息输出。 |
| INFO | 提示,普通的提示型日志信息。 |
| WARN | 警告,不影响运行,但是需要注意⼀下的日志。 |
| ERROR | 错误,程序运行出现错误的日志。 |
| FATAL | 致命,⼀般是代码异常导致程序无法继续推进运行的⽇志。 |
那我们怎么实现日志?又怎么实现日志的等级的筛选?
我们用int整形来控制日志等级的输出。表明日志等级的高低。级别高的自然就是int整形大的。
方法1:我们使用unordered_map<std::string,int>或map<std::string,int>
方法2:我们使用枚举常量,enum class 。
这两种方法都是可以的。但是方法一有一个不大好的缺点:不好进行日志等级的赋值和初始化,毕竟我们给日志等级默认我们也要知道对应的日志等级名称。当然我们也可以定义全局变量int给他们日志等级对应的日志等级名称。还需要注意实现一个全局的unordered_map<int,std::string,int>存储日志等级名称和日志等级。在打印的时候只需要unordered_map<int,std::string>变量后面加一个[日志等级]查找到字符串。
上述方法一还是不太好,定义对应日志等级的日志等级名称,意味着这个名字就不能再用了,虽然我们也可以使用namespace命名空间,将unordered_map<int,std::string,int>和这些日志等级赋值变量放入命名空间中,也可以将unordered_map<int,std::string,int>和这些日志等级赋值变量前加上static放入类域中。
方法二:由于C++11中发现enum类型非常容易收到其他重名变量或是重名函数和类影响,C++11中新增了enum class限定枚举类型。使用时要带上枚举类::枚举类型存入的类型,更加安全。只是有点不好的是在我们打印时必须要字符串,所以我们还要将枚举类型转为字符串类型。当然根据方法一给我们的启示,我们可以使用unordered_map<枚举类型,std::string>,也可以使用switch语句。
#ifndef __LEVEL__HPP__
#define __LEVEL__HPP__
#include <string>
namespace Logs
{
class Level
{
public:
enum class Value
{
UNKNOW = 0,
DEBUG,
INFO,
WARN,
ERROR,
FATAL,
OFF
};
static std::string To_String(const Value& level)
{
switch (level)
{
case Value::UNKNOW: return "UNKNOW" ;
case Value::DEBUG : return "DEBUG" ;
case Value::INFO : return "INFO" ;
case Value::WARN : return "WARN" ;
case Value::ERROR : return "ERROR" ;
case Value::FATAL : return "FATAL" ;
case Value::OFF : return "OFF" ;
}
return "UNKNOW";
}
// static std::string To_String(const Value& level)
// {
// static std::unordered_map<Value,std::string> dic =
// {
// // 未知日志等级。
// // { Value::UNKNOW, "UNKNOW" } ,
// { Value::DEBUG , "DEBUG" },
// { Value::INFO , "INFO" },
// { Value::WARN , "WARN" },
// { Value::ERROR , "WARN" },
// { Value::FATAL , "FATAL" },
// { Value::OFF , "OFF" }
// };
// if(dic.find(level) == dic.end())
// {
// return "UNKNOW";
// }
// return dic[level];
// }
private:
};
}
#endif
三、 日志输出信息
我们在上一章中讲述的格式化字符串,用到很多日志输出信息内部的变量,但是当时小编并没有讲如何定义日志信息。想用类似printf()函数使用的占位符先让大家先理解一下日志消息所需要输出的信息和占位符被代替为有效信息。
日志信息需要可能出错的
日志信息:
时间:描述本条日志的输出时间。
线程ID:描述本条日志是哪个线程输出的。
日志等级:描述本条日志的等级。
日志数据:本条⽇志的有效载荷数据。
日志文件名:描述本条日志在哪个源码⽂件中输出的。
日志行号:描述本条日志在源码⽂件的哪一行输出的。
#ifndef __MESSAGE_HPP__
#define __MESSAGE_HPP__
#include "Level.hpp" // 上文中的文件内容。
#include <thread>
/*
1.日志输出时间
2.日志等级
3.源文件名称
4.源代码代号
5.线程ID
6.日志主体消息
7.日志器名称
*/
namespace Logs
{
struct Message
{
public:
time_t _ctime;
Level::Value _level;
std::string _file;
size_t _line;
std::thread::id _tid;
std::string _message;
std::string _logger;
Message(time_t ctime,
Level::Value level,
std::string file,
size_t line,
std::string message,
std::string logger
)
:_ctime(ctime)
,_level(level)
,_file(file)
,_line(line)
,_tid(std::this_thread::get_id())
,_message(message)
,_logger(logger)
{
;
}
};
}
#endif
四、 文件的落地方向(日志的输出方向)
在日常开发调bug中我们很难通过打印出的内容判断出bug,因为服务器运行太快了,打印的内容是一闪而过的。就算我们将控制日志输出的等级,也很难抓住重点。所以我们应该控制它的落地方向。落地方向也多种多样,向屏幕输出、向指定文件输出,还可以设置滚动按大小滚动(大小到了阈值就创建一个新的文件。)、按时间滚动(超过了指定时间就创建新的文件。)。
根据这些不同的落地方向,我们可以封装出不同的类,为了更好的调用这些类。我们使用一个纯虚类作为一个基类(父类)。通过子类可以给父类对象赋值。(可以理解为位断。)
我们再往上封装一个工厂通过传递模版(模版实例化)来进行指定类的创建。
#ifndef __SINK_HPP__
#define __SINK_HPP__
#include <iostream>
#include <fstream>
#include <sstream>
#include <cassert>
#include <memory>
#include <ctime>
#include "Until.hpp"
namespace Logs
{
class Sink
{
public:
using Ptr=std::shared_ptr<Sink>;
virtual ~Sink(){}
virtual void log(const char* str,size_t len) = 0;
};
enum class Time_Gap
{
GAP_DAY,
GAP_HOUR,
GAP_MINUTE,
GAP_SECOND
};
// 按时间进行滚动。
class RollByTimeSink : public Sink
{
public:
RollByTimeSink(const std::string& base_filename,Time_Gap gap)
:_base_filename(base_filename)
{
switch(gap)
{
case Time_Gap::GAP_DAY : _gap_size = 3600 * 24 ;break;
case Time_Gap::GAP_HOUR : _gap_size = 3600 ;break;
case Time_Gap::GAP_MINUTE : _gap_size = 60 ;break;
case Time_Gap::GAP_SECOND : _gap_size = 1 ;break;
}
// 当前是第几段。
_cur_gap = time(nullptr) % _gap_size;
Until::File::createdirectory(Until::File::path(_base_filename));
}
void log(const char* str,size_t len) override
{
time_t cur = Until::Time::GetTime();
std::string filename;
if(cur % _gap_size == _cur_gap)
{
filename = CreateFile();
}
_ofs.close();
_ofs.open(filename,std::ios::binary | std::ios::app);
assert(_ofs.good());
_ofs.write(str,len);
}
std::string CreateFile()
{
time_t t = Until::Time::GetTime();
struct tm lt;
localtime_r(&t,<);
std::stringstream ss;
ss << _base_filename
<< lt.tm_year +1900
<< lt.tm_mon + 1
<< lt.tm_mday
<< lt.tm_hour
<< lt.tm_min
<< lt.tm_sec
<< ".log";
return ss.str();
}
private:
std::string _base_filename;
std::ofstream _ofs;
size_t _gap_size; // 时间段大小。
size_t _cur_gap; // 第几个时间段。
};
// 直接输出到屏幕上。
class StdoutSink : public Sink
{
public:
void log(const char* str,size_t len) override
{
std::cout.write(str,len);
}
};
// 输出到文件里。
class FileSink : public Sink
{
public:
// 打开就关下次还要再打开,太麻烦了。
FileSink(const std::string& pathname)
:_pathname(pathname)
{
// 1.创建文件所在目录。
Until::File::createdirectory(Until::File::path(_pathname));
// 2.创建并打开文件。
_ofs.open(_pathname,std::ios::binary | std::ios::app);
// 3. 判断文件是否被打开。
assert(_ofs.good());
}
void log(const char* str,size_t len) override
{
_ofs.write(str,len);
}
private:
std::string _pathname;
std::ofstream _ofs;
};
// 输出到滚动文件里。
// 按照大小滚动。
class RollBySizeSink : public Sink
{
public:
RollBySizeSink(const std::string& base_filename,const size_t& max_fsize)
:_base_filename(base_filename)
,_max_fsize(max_fsize)
,_cur_fsize(0)
{
Until::File::createdirectory(Until::File::path(base_filename));
//_ofs.open(CreateFile(), std::ios::binary | std::ios::app);
//assert(_ofs.good());
}
void log(const char* str,size_t len) override
{
if(_cur_fsize >= _max_fsize)
{
_ofs.close();
_ofs.open(CreateFile(), std::ios::binary | std::ios::app);
assert(_ofs.good());
_cur_fsize = 0;
}
_ofs.write(str,len);
_cur_fsize += len;
}
std::string CreateFile()
{
time_t t = Until::Time::GetTime();
struct tm lt;
localtime_r(&t,<);
std::stringstream ss;
ss << _base_filename
<< lt.tm_year + 1900
<< lt.tm_mon + 1
<< lt.tm_mday
<< lt.tm_hour
<< lt.tm_min
<< lt.tm_sec
<< "-"
<< _name_count++
<< ".log";
return ss.str();
}
protected:
// 防止多种相同的文件。
int _name_count;
std::string _base_filename;
std::ofstream _ofs;
size_t _max_fsize;
size_t _cur_fsize;
};
// // 按时间进行滚动。
// // 直接继承按时间进行滚动,但是滚动就不单一了。
// class RollByTimeSink : public RollBySizeSink
// {
// public:
// RollByTimeSink(const std::string& base_filename,const size_t& max_fsize,Time_Gap gap)
// :RollBySizeSink(base_filename,max_fsize)// 在初始化列表中初始化父类。
// {
// switch(gap)
// {
// case Time_Gap::GAP_DAY : _gap = 3600 * 24 ;break;
// case Time_Gap::GAP_HOUR : _gap = 3600 ;break;
// case Time_Gap::GAP_MINUTE : _gap = 60 ;break;
// case Time_Gap::GAP_SECOND : _gap = 1 ;break;
// }
// }
// void log(const char* str,size_t len)
// {
// RollBySizeSink::log(str,len);
// }
// void CreateFile()
// {
// time_t cur = time(nullptr);
// if(cur % _gap == 0)
// {
// ++Time_Size;
// RollBySizeSink::CreateFile();
// }
// }
// private:
// // std::string _base_filename;
// // std::ofstream _ofs;
// size_t _gap;
// size_t Time_Size;
// };
// 制造对应的结构体对象。
class SinkFactory
{
public:
// 显示化传递。
template< class SinkType , class ...Args >
static Sink::Ptr CreateSink(Args&& ...args)
{
// 参数包的内存。
// sizeof ...(args);
// 传入函数后再展开。
return std::make_shared<SinkType>(std::forward<Args>(args)...);
}
private:
;
};
}
#endif
516

被折叠的 条评论
为什么被折叠?



