Logging On Linux

简单回顾一些Linux上日志系统的一些背景,并配置一个rsyslog的日志结构化落db的系统。

背景

push模型:应用程序自己主动推日志到消息系统

pull模型:消息系统监控应用程序的日志,自己去query

syslog 系:syslog->rsyslog or syslog-ng

push 模型,监听/dev/log,收到的日志一般存放在/var/log 下面。配置在/etc/rsyslog.conf

日志格式

有 2 个 rfc

rfc 3164 BSD style

这个更简单一点,最大长度 1024

<35>Oct 12 22:14:15 client_machine su: 'su root' failed for joe on /dev/pts/2
field explain
<35> priority = facility + severity
Oct 12 22:14:15 日期 格式只能是这个
client_machine 主机名称、ip 地址
su: tag,最多 32B,以[ : space 结束
‘su root’ fail。。。 消息正文

rfc 5424

2009 年出的,最大长度 2048

<34>1 2003-10-11T22:14:15.003Z mymachine.example.com su - ID47 - BOM'su root' failed for lonvick on /dev/pts/8
field explain
<34> pri
1 version
2003-10-11T22:14:15.003Z 时间
mymachine.example.com 机器
su app name
- 应该是进程 pid
ID47 消息 ID
- 结构化数据
BOM’su root’ failed for… 消息正文

再来一个有结构化数据的例子

[exampleSDID@32473 iut="3" eventSource="Application" eventID="1011"][examplePriority@32473 class="high"][sigSig ver="1" rsID="1234" ... signature="..."]

格式就是这么个格式 ‘exampleSDID@32473’ 是个 SD-ID,有一些标准化的 ID,比如这里的’sigSig’。

进阶功能

接收(顾名思义)
  • imudp
  • imtcp
  • imfile
处理
  • mmjson rfc3164 的简单 json 格式化
  • mmnormalize 用 liblognorm 自定义解析规则,出来是个 json
输出(顾名思义)
  • ommysql
  • omelastic
  • omfile
  • omkafka
  • omprog: 调用自定义的处理程序,用 stdin/stdout 处理

linux 系:systemd-journald

push 模型,监听/run/system/journal/syslog,日志存放/var/log/journal,配置在/etc/systemd/journald.conf

非明文存储,利于搜索。

结构化消息,下划线开头是 journald 自己添加的消息。用 journalctl 读取。例子:

$ journalctl -n1 -o verbose
-- Logs begin at Tue 2022-02-08 19:21:10 CST, end at Mon 2022-03-28 08:05:23 CST. --
Mon 2022-03-28 08:05:23.517238 CST [s=5a9ef8f0b27749e8be1fb46418165a0c;i=1ccd;b=1c4958a7b26240628713f627042e87f8;m=f2d223e7b;t=5db3c119c1b36;x=fc7b3dd29adfe7b8]
    PRIORITY=6
    _UID=1000
    _GID=1000
    _CAP_EFFECTIVE=0
    _AUDIT_SESSION=3
    _AUDIT_LOGINUID=1000
    _SYSTEMD_OWNER_UID=1000
    _SYSTEMD_UNIT=user@1000.service
    _SYSTEMD_SLICE=user-1000.slice
    _SYSTEMD_USER_SLICE=-.slice
    _BOOT_ID=1c4958a7b26240628713f627042e87f8
    _MACHINE_ID=2ce2d4feff754f93ab21e7355f77e4c6
    _HOSTNAME=ybaipc
    _TRANSPORT=stdout
    _COMM=chrome
    _EXE=/snap/chromium/1945/usr/lib/chromium-browser/chrome
    _SELINUX_CONTEXT=snap.chromium.chromium (enforce)
    _SYSTEMD_CGROUP=/user.slice/user-1000.slice/user@1000.service/snap.chromium.chromium.990dd0f1-5415-48d7-b34d-0847ae198b0b.scope
    _SYSTEMD_USER_UNIT=snap.chromium.chromium.990dd0f1-5415-48d7-b34d-0847ae198b0b.scope
    _SYSTEMD_INVOCATION_ID=f24c28562e634bd98b4dcf7bbe178952
    _STREAM_ID=c8846ca93bce46b7903aaa6881b310d9
    SYSLOG_IDENTIFIER=chromium_chromium.desktop
    _PID=8770
    _CMDLINE=/snap/chromium/1945/usr/lib/chromium-browser/chrome --type=utility --utility-sub-type=network.mojom.NetworkService --lang=en-US --service-sandbox-type=none --change-stack-guard-on-fork=enable --shared-files=v8_context_sna>
    MESSAGE=[8770:8773:0328/080523.517107:ERROR:ssl_client_socket_impl.cc(995)] handshake failed; returned -1, SSL error code 1, net_error -101

信息是真的很丰富了

进阶功能

journal的进程为system-journald.service

这也可以配置日志转发,但这里叫systemd-journal-upload.service

接收端是systemd-journal-remote.service

还有个可以通过http进行日志记录的systemd-journal-gatewayd.service

但是!:没有过滤!

elk:logstash+filebeat

Pull 模型,日志系统主动拉取消息。

本来是直接部署 logstash 的,但是资源占用堪忧(java 写的),现在直接部署 filebeat(golang 写的)只用作日志搜集。logstash 用作格式转换和 es 写入。

存储一般就到了 elasticsearch 里,展示就用 kibana。

进阶功能

对接 elastic 的生态,尤其是各种日志生成的 visualize+dashboard

以及监控,很大的可玩性

就是有点费机器

Kernel Log

kernel的log是硬编码在内核里的一个ringbuffer的。一般发行版会起一个klogd的进程,不断的读取这个ringbuffer,然后发给syslogd。syslogd把消息再存到磁盘上。

kernel的log一般就是printk出来的。

用户空间和kernel的log相关的系统调用就2个

  • syslog(就叫这个,和glibc的syslog同名)
  • klogctl

Rsyslog in action

rsyslog兼容通用的配置格式,也就是$ModLoad/$WorkDirectory这种美元符号开头的脚本式格式。但后续增添了Rainer Script(作者叫Rainer)的新型配置格式,还能混着用。我看了下代码,解析这块就是传统的flex+bison写的一个解析器。这个有明确的作用域,可以用花括号包起来,写复杂逻辑推荐用这个新的格式。

小目标

当前的某个应用里有不少流水数据,每次查询的时候需要登陆到机器上手动从日志中grep;难处在程序有几十个实例,分散在多台机器上,手动查找IP并一路cd的操作效率很低。现在希望可以将这些数据格式化的送入数据库中,后续就可以借助丰富的数据库前端或者各种BI(metabase之类)来进行查询。

需要做的一些步骤

  • 监控日志文件: 自带模块 imfile
  • 日志提取与结构化: mmnormalize,利用liblognorm从消息中提取字段,规整化到json中,供后续步骤使用
  • 发送到数据库: ommysql模块可以提供支持,也有ommongo/omelastic等后端

关联模块准备

数据库就MySQL了,默认这个已经足够了。

Centos 7里默认的Rsyslog还是有点老,好在官网提供了最新的仓库,自己添加一个repo就能用(Install rsyslog on RHEL/CENTOS)。

$ yum install rsyslog-mmnormalize rsyslog-mysql

配置文件

默认文件为/etc/rsyslog.conf,文件中大部分发行版默认配置一条$IncludeConfig /etc/rsyslog.d/*.conf,因此可以将自己的配置放在该目录下即可。这次的配置可以全部集中到/etc/rsyslog.d/myconf.conf中。

全局使用的配置尽量放在/etc/rsyslog.conf,比如启用模块的指令,就需要放在文件的最开始。

Rainer script中,作为key的名字大小写无关。

# /etc/rsyslog.conf
module(load="imfile")
module(load="mmnormalize")
module(load="ommysql")
# /etc/rsyslog.d/myconf.conf
input(
    type="imfile"
    File="/tmp/mylog.txt"
    Tag="mylog:"
    RuleSet="rs_me" # 绑定的ruleset
)

# 示例日志
# 2021-12-13 12:12:12.132435,Quantity: 23
template(name="tpl_biz" type="string" option.sql="on"
    string="insert into biz_table (created_at, quantity) values (\"%$!gentime%\", \"%$!quantity%\")")

# 这2个template是等价的
# template(name="tpl_biz" type="list") {
#     constant(value="insert into biz_table (quantity) values (")
#     property(name="$!gentime")
#     constant(value=", ")
#     property(name="$!quantity")
#     constant(value=")")
# }

ruleset(name="rs_me") {
    action(type="mmnormalize"
        rulebase="/etc/rsyslog.d/me.rulebase"
    )

    if $!event.tags contains "qty" then { # 这里的tags是从rulebase里来的
        action(type="ommysql"
            Template="tpl_biz"
            server="localhost" serverport="3306" db="Syslog"
            uid="aaa" pwd="aaa"
        )
    } else { # else不要也是可以的,就相当于丢弃了
        action(type="omfile"
            File="/tmp/me.out"
        )
    }
}

日志文件监听

数据源需要用input来指明,tag属性是必需的。

格式解析:liblognorm

mmnormalize只是简单调用了一下lognormalizer的功能。lognormalizer可以先从shell里调用来确认自己写的规则是不是正确。

举例,现在的rulebase是这样的

# me.rulebase
rule=qty:%gentime:char-sep:,%,Quantity: %quantity:number%

需要这样调用,默认输出是json,-T输出event.tags。

$ echo '2021-12-13 12:12:12.132435,Quantity: 23' | lognormalizer -r me.rulebase -T
{ "quantity": "23", "gentime": "2021-12-13 12:12:12.132435", "event.tags": [ "qty" ] }

格式解析的模块是mmnormalize,是绑定的ruleset:rs_me的一部分。

注意: 在centos7上默认的rsyslog版本,如果rule里的key是大写开头(quantity->Quantity),有概率是没有办法在action的template里去到这个值。所以尽量把抽取出来的key按照全部小写命名。

输出到数据库

时间格式
  • 为了能直接送到db里,时间格式需要调整到MySQL直接识别的格式,也就是YYYY-MM-DD hh:mm:ss[.fraction]
  • 若可以直接调整代码是最好不过了
时间精度
  • DATETIME(6)来定义,可以精确到微妙(10^-6)。这里的6就是fraction。

需要事先在db中新建一张表

CREATE TABLE `biz_table` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `created_at` datetime(6) DEFAULT NULL,
  `quantity` bigint DEFAULT '0',
  PRIMARY KEY (`id`),
);

template里option.sql="on"是必须的,可能是出于编译模板的需要。

排错指南

  • rulebase需要事先在shell里验证好
  • module的加载放在/etc/rsyslog.conf开头
  • 发行版默认是用的systemd,写好配置后用systemctl restart rsyslog重启,然后systemctl status rsyslog看下启动的输出。这个输出只有几行,如果出错的地方很多,可以用journalctl -u syslog调用默认pager(也就是less)看完整的启动日志。

展望

ansible: syslog是有标准的role的,把配置集成到自己的playbook中,能大大提升自动化效率。

开发相关

不找依赖的话,估计现在没有开箱即用的 rfc5424。glibc 的 syslog 还是太弱了点。

glibc 里 syslog 相关的实现

openlog(tag, flag)

closelog()

syslog(level, fmt, …)

代码在glibc/misc/syslog.c里,不复杂,但是只能 Log 到”/dev/log"里。不过倒是线程安全的。

这个是符合 rfc3164 的

systemd service

如果包装成systemd service,用systemd接管程序的起停,那么自动获得送入journald的功能。

sd-journal

开发时依赖一个 systemd 的库 libsystemd-dev,运行时只需要一个libsystemd.so就行了。

systemd for Developers III (点进去看例子)

只需要几个函数

  • sd_journal_send 可自己添加kv
  • sd_journal_print 类似 syslog

如何用到实际项目里

需要自己包装一个吗?

push/pull区别在哪儿?

  • 开发、运维
  • 流量
  • 结构化在哪儿做(生产者机器还是集中消费的地方)

想法:日志记录时进行标准化的结构,直接落es库。

落es之前的步骤如何选择?