前言

公司生产环境发生了一个事故,导致公司造成了损失。先说结果是因为业务人员修改参数时设置错了参数,导致事故发生。一开始业务人员认为是我们代码程序出了错误,好在有操作日志记录,于是前去查看发现,操作日志记录不全,没有记录到那个修改的参数。然后在排查代码发现没有问题之后,只能通过查找 MySQL 的 binlog 执行日志与 Nginx 的请求日志作为佐证,才将扣在身上的锅甩了出去。因此我意识到操作日志的重要性。

为什么要记录操作日志?

上文也已经说了,操作日志几乎存在于所有的系统中,尤其是后台管理系统,需要详细的记录下管理员所做出的操作,以便于更好的排查问题与溯源分析。

系统日志与操作日志的区别

系统日志

系统日志主要为了开发更好的排查问题,一般记录在日志文件中。系统日志一般是开发人员会进行查看,所以可读性不需要很高,甚至会包含一些代码信息。

操作日志

操作日志主要是为了记录使用人员的添加、修改、删除操作。一般是给用户看的,所以需要很高的可读性,能让人一眼就看明白操作了什么东西,而不需要再去结合代码进行分析。

常见的日志记录方式

自定义函数

在很多系统中都是定义了一个类似的admin_log()函数,然后在需要记录日志的地方调用这个函数传入记录的信息即可。 优点:

  • 比较灵活,可以随意拼接自己想要记录的日志信息
  • 在想要记录日志的地方只需调用函数就可以了

缺点:

  • 日志的记录穿插在代码中,与代码耦合度很高,增加了代码复杂度
  • 大块的日志信息在代码中看起来很丑
  • 随着代码的修改可能还会需要对日志信息进行修改维护
  • 有可能会忘记日志的记录,导致出了问题背锅

注解+ AOP

在 Java 中使用注解+ AOP 是一种很常见的日志记录方式,网上有很多这样的例子,但是在 PHP 中好像没有见过这种方式。

思考

我们现在的系统中就是使用第一种自定义函数的方式来记录操作日志的,有着很多的缺点,所以就想要寻求一种优雅的记录操作日志的方式。

在美团技术团队的《如何优雅的记录操作日志?》一文中很详细的描写了如何利用注解+AOP 实现操作日志的记录。而 PHP 也是有着注解和 AOP 的,看了这篇文章之后就一直思考该如何实现。想了很久,看了很多遍这篇文章还是感觉不是很完美,虽然把日志的记录从代码迁移到注解中与代码分离,但还是有很强的耦合,每次业务代码的修改还是需要对日志进行更新的。

想法

操作日志一般是需要记录下用户的增删改操作,对数据库增删改的操作我们一般使用 ORM 完成,所以我们可以使用 ORM 的模型事件createdupdateddeleted来记录用户的增删改操作。

在其中我们可以很方便的获取到用户的操作内容,然后怎么实现可读性呢?

我的想法是通过获取表注释来明确这次操作的含义,然后通过字段注释知道操作字段的具体含义内容。如此我们就可以记录到可读性的日志内容了嘛?

实现

假如有张用户表,SQL 如下:

create table tb_user
(
  id   int auto_increment primary key,
  name varchar(20)       null comment '姓名',
  sex  tinyint default 0 null comment '性别'
) comment '用户';

在其中添加一条数据:

id name(姓名) sex(性别)
1 Chance 1

获取表注释

SELECT TABLE_NAME, TABLE_COMMENT FROM infORMation_schema.TABLES WHERE TABLE_SCHEMA = '数据库名称'

获取字段注释

SELECT TABLE_NAME,COLUMN_NAME,COLUMN_COMMENT FROM infORMation_schema.COLUMNS WHERE TABLE_SCHEMA = '数据库名称'

根据表注释与字段注释我们可以得到以下一条日志信息:

添加了用户:id 为:1,姓名为:Chance,性别为:1

可以看到日志信息中还存在一个性别为:1的信息不易理解,用户看了根本不明白性别为什么是 1,他们会觉得是不是出了 bug,所以我们还需要把这些不易理解的数据内容转换一下,这个就可以使用模型的访问器来完成。

定义访问器

public function getSexTextAttribute($key): string
{
    return ['女','男'][($key ?? $this->sex)] ?? '未知';
}

至此就可以获取到一条具有可读性的操作日志:

添加了用户:id 为:1,姓名为:Chance,性别为:男

具体代码实现可查看Chance-fyi/log仓库。

总结

使用这种方式来记录操作日志,可以记录下用户可读的操作日志,并且可以减少开发者的负担,开发过程中只需关心业务逻辑,不需要关心日志的记录,会自行完成日志记录。因为会统一记录,所以日志都是要使用统一的模板,所以灵活性和可定制性会比较差,请使用者自行衡量。如果你有好的想法,欢迎提个issues共同交流。

更新

2022-10-10

ORM 的模型事件并不能覆盖到所有的增删改操作,一般在进行批量增删改操作时 ORM 一般都不会选择触发事件,还有一些直接使用 DB 的操作同样不会触发事件。所以利用事件来记录日志并不能覆盖到全部的场景,需要换一个思路来记录日志。

Think 的 ORM 所有的增删改查操作最后都会通过 think\db\Query 查询对象来解析执行,而 Laravel 的 ORM 所有的增删改查操作则都会通过 \Illuminate\Database\Query\Builder 来解析执行。我们就可以在此拦截到所有的增删改操作来生成记录操作日志。