
记录系统运行日志
除掌握如何初始化日志对象以外,还需重点理解为何需要记录日志、为何需要区分服务日志和应用日志、为何需要制定日志级别、各个级别如何划分等知识点。![]()
课前导言
记录系统运行日志
一个实际WEB系统,在运行难免有问题,包括程序自身问题或用户使用问题,为了帮助排查已知问题,发现未知问题,往往需要记录系统详细的各种运行信息。出于用户友好性和系统安全性考虑,这些信息只能在后台存在,万万不可输出到前端给用户看。
大器集成著名的 “spdlog”,提供便捷、高效的后端日志记录功能。
秉承服务(Server)与应用(Application)职责分离的设计(见《第2节:使用模板输出HTML》),在大器框架中,日志区分为服务日志和各个应用的日志。服务日志用于记录系统公共的基础的设施的运行情况,比如网络连接情况、数据库连接情况、CPU负载情况等;应用日志用于记录各个应用各自的业务逻辑,比如用户异常登录信息记录、用户非法请求参数记录等。
1. 服务日志
一个Web系统中,服务只有一个(全局单例),所以服务日志记录器(以下称日志对象或logger)也只有一个。初始化这个全局单例对象的方法是:
log::InitServerLogger("用于存储服务日志文件的文件夹路径");
在服务对象创建之前,就可以(建议)初始化服务日志,如此,服务对象的创建过程中的信息,就可以被记录,因此,上面那行代码,通常可以作为main()函数的第一行代码,或者至少应该在调用Server::Supply(……)以生成服务对象的代码之前。
创建服务日志对象当然也有可能失败,所以 InitServerLogger()方法返回的是bool类型,false表示失败。此时如何记录创建日志对象失败的日志?我们通常就回归使用std::cerr或std::cout。
以下是在前一节课前加法器的代码基础上,加上初始化服务日志,并刻意在svc->Run()之后,程序即将退出时,加上一行用于告别的日志。
#include "daqi/da4qi4.hpp"
using namespace da4qi4;
void add(Context ctx)
{
try
{
//取得URL参数 a 和 b:
std::string a = ctx->Req().GetUrlParameter("a");
std::string b = ctx->Req().GetUrlParameter("b");
//把字符串转换为整数:
int na = std::stoi(a); //stoi 是 C++11新标中的字符串转换整数的函数
int nb = std::stoi(b);
//本例的核心业务逻辑,其实就这一行:
int c = na + nb;
ctx->ModelData()["c"] = c;
}
catch(std::exception const& e)
{
ctx->ModelData()["c"] = std::string("处理入参转换异常.") + e.what();
}
ctx->Render().Pass(); //Render 是动词:渲染
}
int main()
{
log::InitServerLogger("log/"); // <-- 新加入第一行
auto svc = Server::Supply("127.0.0.1", 4098);
auto app = svc->DefaultApp();
app->SetTemplateRoot("view/04/"); //模板文件根目录
app->InitTemplates();
app->AddHandler(_GET_, "/add", add);
svc->EnableDetectTemplates(3); //3秒
svc->Run();
log::Server()->info("Bye."); // <-- 新加入最后一行
}
重要信息:
- 要正确运行上述程序,需要事先在程序的运行目录下,创建好“log”子目录;
- 日志相关功能都放在 log 名字空间下;
- 取得全局服务日志对象的方法是: log::Server(),它是一个智能指针;
- info() 用于输出普通级别的日志信息,更多的日志信息分类分级见后;
- 正如视频所演示的,程序退出前的最后一条服务日志,一定是“Server Destroied.”。例中的“Bye.” 将夹在“Server Stopped”和“Server Destroied.”之间。
- 服务日志除了输出到屏幕,还将输出到文件中,相应文件存储在初始化时所指定的目录下,在本例中为“log/”。
2. 日志级别
由低到高依次是:
- trace(跟踪):系统不一定有什么问题,只是你(程序员)突然对某些逻辑的实现路径有些晕,这时可以加一些跟踪信息。
- debug(调试):已经很大怀疑系统存在某些问题了,并且也大致确定出错范围,此时需要刻意输出一些运行信息以帮忙排查。
- info(信息):通常是一些较为详细的程序运行时关键数据,记录这些信息虽然也有利于排查问题(如果有),但通常info级别的信息就是为了让我们更详细地观察系统的运行情况,以进一步改进程序。比如每天用户登录量、用户最经常访问的页面等等。
- warn(警告):有点问题了,但并不影响程序的逻辑正确性。
- err(错误):有问题,但通常只是造成当前这一笔业务无法正确执行,程序仍然可以放心地继续运行;别的业务还可以正常执行。
- critical(致命):发生致命错误,程序要么无法继续运行,要么是可以继续运行,但可预见会有大量错误随之发生。
系统运行时,可以设置服务或应用日志的输出级别。设置级别越高,则可输出的日志级别就越少。比如,设置为info级别,则只会有info及更高的“warn”、“err”和“critical”级别的信息被记录,“debug”和“trace”信息不被输出。如果设置为“trace”,则所有级别日志都被记录。如果希望干脆不记录任何日志,请使用上面没有列出的 “off”。
默认的服务日志级别是“info”,所以前例中的我们调用的“info(……)”信息可被看到。
下面是输出一行“warn”级别的服务日志的代码示例:
log::Server()->warn("天啊,我什么都还没开始做,就要退出啦?");
日志使用UTF-8编码,可以完美地支持汉字。在Linux终端屏幕上,不同级别的字样显示,有不同的颜色。
3. 应用日志
系统中可能存在多个应用,因此应用日志的初始化需要绑定各个日志,即通过app对象的“InitLogger()”方法来初始化,比如:
app->InitLogger("用于存储应用日志文件的文件夹路径"
, 级别 //即:哪一级(及以上)的日志需要输出?
,单一日志文件最大尺寸 //单位: KB
,当前应用最多生成几个日志文件);
为了避免日志文件内容太多难以使用(比如几个G的文件,打开都很困难),大器框架支持在当前日志文件大小超标后,自动生成下一个日志文件(将在文件名上进行递增编号);函数中第三个参数就用于控制单一日志文件的字节数,单位是KB。最后一个参数则用于控制最多生成几个日志文件,超过该数值时将从第一个日志文件开始覆盖。这两个参数都有默认值。
前面的服务日志初始化过程其实提供了一模一样的接口:
bool InitServerLogger("用于存储服务日志文件的文件夹路径"
, 级别 //即:哪一级(及以上)的日志需要输出?
,单一日志文件最大尺寸 //单位: KB
,最多生成几个服务日志文件);
不过,应用也可以先设置日志文件的根文件夹路径,然后再初始化,此时第二步的初始化就不需要再传入日志文件的根文件夹路径,例如:
app->SetLogRoot("log/");
appt->InitLogger();
之所以存在这个重载版本,是为了让应用保持设置各类文件夹的接口保持一致。下面就是使用这一版本实现应用日志对象初始化的main()函数,请大家注意app的 在设置模板路径和日志路径时,接口的一致性,即“SetTemplateRoot()”和“SetLogRoot()”:
int main()
{
log::InitServerLogger("log/");
auto svc = Server::Supply("127.0.0.1", 4098);
auto app = svc->DefaultApp();
app->SetTemplateRoot("view/04/"); //模板文件根目录
app->InitTemplates();
app->AddHandler(_GET_, "/add", add);
app->SetLogRoot("log/"); // <-- 先初始化日志路径
app->InitLogger(); // <-- 再初始化日志对象(使用默认参数)
svc->EnableDetectTemplates(3); //3秒
svc->Run();
log::Server()->info("Bye.");
}
如视频所示,以上代码的问题是:无法记录应用加载模板文件过程中产生的应用日志;原因是因为代码是在加载模板,即调用 InitTemplates()之后,才创建了日志对象。解决方法也简单:对调两个操作的次序即可。
int main()
{
log::InitServerLogger("log/");
auto svc = Server::Supply("127.0.0.1", 4098);
auto app = svc->DefaultApp();
app->SetLogRoot("log/"); // <-- 初始化日志路径
app->InitLogger(); // <-- 初始化日志对象(使用默认参数)
app->SetTemplateRoot("view/04/"); //模板文件根目录
app->InitTemplates();
app->AddHandler(_GET_, "/add", add);
svc->EnableDetectTemplates(3); //3秒
svc->Run();
log::Server()->info("Bye.");
}
相当于是在创建app之后,就立即初始化它的日志对象。
4. 在上下文操作中记录应用日志
我们可以随时随处获得全局惟一的服务日志,通过使用: log::Server()。那我们如何在某个操作的过程中,随时随处得到当前应用的日志呢?
很简单,通过“上下文”对象。也就是我们一直在使用的“Context”类的对象。在我们的例子中,这个对象变量名一直是“ctx”。完整过程是:通过ctx取得应用,再通过应用取得日志对象:
ctx->App().GetLogger()->info("演示如何通过上下文取得当前应用的日志记录器");
记录应用日志是很常见的行为,所以这样写虽然直观,却需要输入太多字母。上下文对象为此提供了更简捷的写法:
ctx->Logger()->info("更简捷的写法,通过上下文直接取得日志记录器");
应用日志当然也提供分级,比如:
ctx->Logger()->trace("a={}, b={}.", a, b);
这个例子还演示了spdlog一个强大功能:格式化。格式化串的“{}”不会被输出,输出的是后面对应的数据的内容。
如果就是要输出 {} ,需要使用转义符: \{ \}
本课最终完整示例代码( github ):
#include "daqi/da4qi4.hpp"
using namespace da4qi4;
void add(Context ctx)
{
try
{
//取得URL参数 a 和 b:
std::string a = ctx->Req().GetUrlParameter("a");
std::string b = ctx->Req().GetUrlParameter("b");
ctx->Logger()->trace("a={}, b={}.", a, b);
//把字符串转换为整数:
int na = std::stoi(a); //stoi 是 C++11新标中的字符串转换整数的函数
int nb = std::stoi(b);
//本例的核心业务逻辑,其实就这一行:
int c = na + nb;
ctx->ModelData()["c"] = c;
}
catch(std::exception const& e)
{
ctx->ModelData()["c"] = std::string("处理入参转换异常.") + e.what();
ctx->Logger()->warn("又有调皮的用户乱输入啦: {}.", e.what());
}
ctx->Render().Pass(); //Render 是动词:渲染
}
int main()
{
log::InitServerLogger("log/");
auto svc = Server::Supply("127.0.0.1", 4098);
auto app = svc->DefaultApp();
app->SetLogRoot("log/"); // <-- 初始化日志路径
app->InitLogger(); // <-- 初始化日志对象(使用默认参数)
app->SetTemplateRoot("view/04/"); //模板文件根目录
app->InitTemplates();
app->AddHandler(_GET_, "/add", add);
svc->EnableDetectTemplates(3); //3秒
svc->Run();
log::Server()->info("Bye.");
}
课后补充
如果你有看视频,应该同意:现在我们的后端系统运行时,比之前的版本正经多了。让我们用鲁迅先生的一句话来结束本课的文字教程:
所有只谈恋爱不结婚的处对象都是乱来,所有只执行业和不记录日志后台服务系统都是在耍流氓。