69. 日志系统与监控

系统日志让你了解系统内部发生了什么,当出现故障时首先想到的就是日志,drupal日志系统可以按信息类别分别记录日志,并且按照RFC 5424PSR-3将各类信息按紧迫程度划分为8个级别,这样方便筛选查看;除此外你只需进行简单实现就可在系统发生严重问题时得到及时通知,比如数据库连接不上或系统不可用时发送手机短信给你,本篇将介绍这些。

 

日志等级:

依据日志重要性(或严重程度)将信息分为如下8个级别:

emergency:系统不可用,非常严重

alert:严重警告,必须立即处理

critical:临界情况,快到危险的边缘

error:错误,应该注意监控

warning:警告,比如使用了被弃用的方法

notice:正常信息,需要通知

info:正常信息,如用户登录等

debug:调试信息,非常详细

以上级别用数字表示为从07(数字越小越严重),他们被定义或描述在RFC 5424PSR-3中,见以下资料:

http://tools.ietf.org/html/rfc5424

https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md

 

drupal日志系统概述:

系统中记录的日志信息各种各样,需要被归类,在drupal中“类别”称为“频道channel”,每个频道对应一个频道对象,我们可以从日志工厂中通过频道名实例化一个频道对象,频道名可以由开发者自定义,频道对象是使用日志系统的入口;在底层日志信息是通过记录器Logger处理的,每个频道对象可以有多个记录器,她们按优先级依次执行,频道对象提供给用户统一的使用接口,接到调用后将数据发给每一个记录器,系统默认只实现了一个记录器,她负责将日志写入数据库,我们可以定义额外的记录器用来做别的事情,比如依据重要级别发送邮件或短信等。

 

日志工厂:

负责实例化频道对象

服务名:logger.factory

类:\Drupal\Core\Logger\LoggerChannelFactory

得到工厂对象: \Drupal::service("logger.factory");

得到频道对象:\Drupal::service("logger.factory")->get($channel);

得到频道对象的快捷方法:\Drupal::logger($channel);

在容器编译阶段系统会收集所有标签为“logger”的服务(也就是记录器),在工厂对象实例化时她们会被注入,在运行过程中也可以调用如下代码添加更多记录器:

addLogger(LoggerInterface $logger, $priority = 0)

在实例化频道对象时,工厂会将所有的记录器传给她,这里需要注意:频道对象也有同名方法用以添加记录器,在工厂对象上添加和在频道对象上添加是有区别的,若在工厂对象上添加,那么所有频道对象都会被添加,且已经实例化的所有频道对象都会被添加该记录器,但如果在频道对象上调用添加记录器,那么其他频道对象不会被添加

 

频道对象:

这是我们使用日志系统的统一接口,类定义如下:

\Drupal\Core\Logger\LoggerChannel

工厂会为每一个传入的频道名(日志类别标志)实例化一个单独的频道对象并注入所有记录器,频道对象依据日志的8类重要性等级提供如下8个快捷方法:

emergency($message, array $context = array());
alert($message, array $context = array());
critical($message, array $context = array());
error($message, array $context = array());
warning($message, array $context = array());
notice($message, array $context = array());
info($message, array $context = array());
debug($message, array $context = array());

她们只是快捷方法,实际上都在如下方法中执行:

log($level, $message, array $context = []);

如使用该方法需要自行传递等级信息,可以是字符串或数字,见前文。

 

其中$message是要记录的消息,可以包含变量,变量内容在$context中提供,变量占位有drupal风格和PSR3风格:

drupal风格如下:

\Drupal::logger('theme')->warning('Theme hook %hook not found.', ['%hook' => $hook]);

$message中变量名需要加前缀,有三种前缀:@%:(冒号) ,这提示系统进行不同的安全处理,含义如下:

@:转义html特殊字符,如果是MarkupInterface对象将不进行任何处理

%:像@一样做转义处理,并将结果包装到<em>元素中

:(冒号):剥离危险的协议,并做转义处理,常常用于处理链接信息。

$context中键名也需要带前缀,关于占位变量详细信息见:

\Drupal\Component\Render\FormattableMarkup::placeholderFormat

 

PSR3风格如下:

还可以用PSR3风格的样式,也就是变量以{}包裹,如:

\Drupal::logger('theme')->warning('Theme hook {hook} not found.', ['hook' => $hook]);

但这需要记录器使用如下服务将其转换为drupal风格:

服务idlogger.log_message_parser

类:\Drupal\Core\Logger\LogMessageParser

该服务在内部将PSR3风格占位替换为drupal@占位,等同于如下:

\Drupal::logger('theme')->warning('Theme hook @hook not found.', ['@hook' => $hook]);

 

在频道对象内部上下文数组会被自动追加以下变量,所以即便没有传递也可以使用:

$context += [
      'channel' => $channel, //频道名
      'link' => '',
      'user' =>$currentUser , //当前用户账号对象,注意该对象不能直接转化为字符串
      'uid' => $currentUser->id(), //用户id
      'request_uri' => $request->getUri(),
      'referer' => $request->headers->get('Referer', ''),
      'ip' =>$request->getClientIP(),
      'timestamp' => time(),
];

注意这些追加的默认变量采用了PSR3风格传递,所以使用她们也要用PSR3风格的样式,如:

\Drupal::logger('user')->warning('hill @name : your uid is {uid}.',[“@name”=>$name]);

如你所见PSR3风格可以和drupal风格混用,由于默认变量采用PSR3风格,所以建议所有记录器均采用log_message_parser服务对上下文$context进行处理,以得到统一的转换(见下文,关于这一点云客认为drupal需要改进,将该服务放到频道对象中执行);注意经过该服务转换的变量中没有被$message用到的PSR3风格变量不被保留;在PSR3风格的样式中大括号要紧挨着变量名,不要出现空格;自定义变量并不推荐你用PSR3风格,使用drupal风格可有更多控制且性能更好。

 

除以上提到的频道对象方法外,该对象还有如下方法:

setRequestStack:设置请求堆栈,用于给默认上下文参数提供值

setCurrentUser:设置当前用户,用于给默认上下文参数提供值

setLoggers:设置全部记录器

addLogger:设置单个记录器

 

系统默认记录器:

系统提供了一个默认的记录器,也是默认安装唯一的一个,用于将日志信息记录到数据库,如下:

服务idlogger.dblog

类:Drupal\dblog\Logger\DbLog

优先级:0 (记录器用整数表示优先级,可以为负数,数值越大优先级越高,越先执行)

 

该记录器在数据库表watchdog中记录日志,这里对该表的部分重要字段说明如下:

uid用户id

type频道名,也就是日志类别标志,会被截取到64字节以内

message没有进行占位变量替换的日志消息,已经将PSR3风格转换成了drupal风格

variables经过logger.log_message_parser处理的上下文参数,已经将PSR3风格转换成了drupal风格,没有被用到的默认变量和用户自定义的PSR3风格变量不被保留

severity数字值,前文提到的信息严重级别,从07

link上下文中的link变量

location请求uri

referer来源网址

hostname客户端ip

timestamp时间戳

在系统后台/admin/reports/dblog可以看到该表的信息,如果开发者自定义了频道名,该界面将增加该频道名

 

有时候在数据库中记录日志时,该操作本身可能会发生异常,此时无法进行日志记录,可以通过设置专用日志数据库的方式解决,可以在配置文件中设置一个目标名叫做“dedicated_dblog”的数据库,该名由如下常量定义:

\Drupal\dblog\Logger\DbLog::DEDICATED_DBLOG_CONNECTION_TARGET

配置如下:

$databases['default']['default']=$dsn_1; //主服务器 

$databases['default']['replica']=$dsn_2;

$databases['default']['dedicated_dblog']=$dsn_3;

数据库配置详见本系列数据库主题,如上情况发生时将使用专用数据库进行日志记录,如果没有设置专用日志数据库时,将采用默认库再试一次,所有这些努力都无法记录时将抛出异常。

 

自定义记录器:

首先定义一个类,实现接口:\Psr\Log\LoggerInterface

推荐使用特征\Drupal\Core\Logger\RfcLoggerTrait,这样只需要编写log方法即可

将该类定义成服务,给出logger标签和优先级,以下假设模块名为“yunke”,我们来实现一个记录器,功能是达到某设定严重性时向管理员发送短信或邮件:

第一步:在模块目录建立文件:

yunke\src\Logger\Logger.php

内容为:

<?php
/**
 * @file
 * 日志记录器
 */

namespace Drupal\yunke\Logger;

use Psr\Log\LoggerInterface;
use Drupal\Core\Logger\RfcLoggerTrait;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use \Drupal\Component\Render\FormattableMarkup;
use Drupal\Core\Logger\LogMessageParserInterface;

class Logger implements LoggerInterface
{
    use RfcLoggerTrait;
    use DependencySerializationTrait;

    protected $parser;

    public function __construct(LogMessageParserInterface $parser)
    {
        $this->parser = $parser;
    }

    public function log($level, $message, array $context = [])
{
        //可在配置系统中指定级别,见本系列配置系统,记录器得到的级别为整数
        if ($level > 3) {
            return;
        }
        //引入log_message_parser服务以支持PSR3风格的变量占位
        $message_placeholders = $this->parser->parseMessagePlaceholders($message, $context);
        //进行占位替换
        $msg = new FormattableMarkup($message, $message_placeholders);
        //调用第三方接口给管理员发送邮件或短信 这里采用如下代码供你测试 将在系统根目录产生文件
        file_put_contents("snsyunke.txt", $msg);
    }
}

第二步:在yunke.services.yml中定义服务,如下:

  yunke.log:
    class: \Drupal\yunke\Logger\Logger
    arguments: ['@logger.log_message_parser']
    tags:
      - { name: logger , priority: 100 }
      - { name: backend_overridable }

重新安装模块即可启用该记录器,在控制器中执行以下代码:

\Drupal::logger('yunke')->alert(" hill @admin ,Website needs you now! \n ip:{ip} \n time: @time", ['@admin' => "yunke","@time"=>date("Y-m-d H:i:s")]);

你将收到以下消息:

 hill yunke ,Website needs you now! 
 ip:127.0.0.1 
 time: 2018-04-29 03:27:53

同时默认记录器也会记录日志,可在网站后台查看

 

记录器优先级:

当系统定义了多个记录器时,可以指定优先级,用整数表示,可以为负数,数值越大优先级越高,越先执行;你可能会想到让先执行的记录器以引用方式修改传入的$level, $message, $context,以影响后面的记录器,但这是不行的,这样做和接口定义不相容,优先级仅用于指定先后顺序。

 

翻译问题:

日志信息默认以传入的语言显示(系统默认英语),如需翻译,可在调用她前先调用翻译函数,如下所示:

\Drupal::logger('yunke')->alert(t("my name is @name", ['@name' => $name]));

但这样并不优雅,也不彻底,翻译不能使用PSR3风格变量占位(上下文数组不能传递PSR3风格占位变量,但消息中可以使用PSR3风格默认变量),且只能用于自己开发的代码,正确的做法是对记录器,工厂等服务定义进行覆写,在记录器中进行翻译。

 

 

补充说明:

  1. bug:在\Drupal\Core\Logger\LogMessageParser::parseMessagePlaceholders中将占位变量“:”错标记为“!”导致信息丢失
  2. 在频道对象执行期间可能引起递归调用,所以在系统设定了调用深度为5层,以阻止无限递归

3、在8个日志等级中,目前drupal8.5在后台默认没有实现配置什么等级的日志才被记录,默认全部记录

4、系统提供了dblog模块,她提供如下一些功能:管理页面/admin/config/development/logging的配置功能,可以设定日志表数据的多少,并提供维护任务,维护时,将删除多余的老旧的数据;该模块还提供日志显示相关功能等,不再赘述。

本书共97小节:

评论 (写第一个评论)