‹  返回课程

记录系统运行日志

课文
阅读量:768
技术范畴
除掌握如何初始化日志对象以外,还需重点理解为何需要记录日志、为何需要区分服务日志和应用日志、为何需要制定日志级别、各个级别如何划分等知识点。
课前导言
除掌握如何初始化日志对象以外,还需重点理解为何需要记录日志、为何需要区分服务日志和应用日志、为何需要制定日志级别、各个级别如何划分等知识点。
记录系统运行日志
如何在大器Web后台程序员记录系统运行日志

一个实际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.");  // <-- 新加入最后一行
}

重要信息:

  1. 要正确运行上述程序,需要事先在程序的运行目录下,创建好“log”子目录;
  2. 日志相关功能都放在 log 名字空间下;
  3. 取得全局服务日志对象的方法是: log::Server(),它是一个智能指针;
  4. info() 用于输出普通级别的日志信息,更多的日志信息分类分级见后;
  5. 正如视频所演示的,程序退出前的最后一条服务日志,一定是“Server Destroied.”。例中的“Bye.” 将夹在“Server Stopped”和“Server Destroied.”之间。
  6. 服务日志除了输出到屏幕,还将输出到文件中,相应文件存储在初始化时所指定的目录下,在本例中为“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.");
}
课后补充

如果你有看视频,应该同意:现在我们的后端系统运行时,比之前的版本正经多了。让我们用鲁迅先生的一句话来结束本课的文字教程:

所有只谈恋爱不结婚的处对象都是乱来,所有只执行业和不记录日志后台服务系统都是在耍流氓。