linux日志相关的机制

860 阅读33分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

前记

最近系统上出现部分日志丢失的情况,为了解决这个问题,开始了解Linux的日志结构和机制。以下是我的学习总结, 主要涉及到了如何发送日志(socket), 怎么处理日志和传输日志(rsyslog)以及怎么管理日志(logrotate)。 注:由于这些应用都是需要用配置去改良或者指导他们如何去做,所以除了简述Linux的结构和功能外,更多的是如何去配置他们。但其中最重要的是了解整个日志记录的机制,以及通过配置去了解这套机制会如何去限制日志的记录以及优化传输,这样我们才能去灵活使用他们。

原文地址

1.Linux中日志的发送流程

一般来说, 我们所写的Web应用的日志都是直接打开一个文件, 然后把日志信息写到这个文件中。 这样非常方便, 但是缺少了一个统一的定义和管理, 所以Linux中就使用了一套稳定的日志管理系统--rsyslog。 它可以接收应用程序通过socket发送过来的日志数据, 然后按照规则非常方便的过滤和分发日志, 同时它也支持稳定的把所有日志从某台机器发送到另外一台机器, 不漏一条消息。

那么这整套机制是怎么运行的呢, 先看看一个本机应用程序发送日志, 本机rsyslog接收日志的流程图:

注:操作系统为debain7, 该流程图以本地发送, 本地接受为例。

img

在这个流程图中, 第一步是应用程序把日志根据syslog协议进行封装, 比如用户指定的日志等级, 系统log渠道等等, 然后把封装好的数据发送到指定socket -- /dev/log/dev/log是一个UNIX域套接字,它负责接受在本地机器上运行的进程所产生的消息。

除此之外还有一个与它十分相似的/dev/klog, /dev/klog是一个从UNIX内核接受消息的设备的socket, 如果看到syslog记录了kern的日志,却没记录普通日志,那就是/dev/log这个socket缓冲区满了, 而/dev/klog缓存区还有空间, 还可以正常运行, 这时候可以尝试通过kern.ipc.maxsockbuf改变缓冲区大小,但并不能完全解决问题。

第二步是由系统的journald开始服务, 在我的系统中,由于使用了Systemd, 它的附属套件systemd/journald提供了一个socket -- /run/systemd/journal/syslog,它通过软连接,让发送到/dev/log/的数据转而发送到/run/systemd/journal/syslog, 然后通过读取标准socket来读取日志。不过systemd-journald所记录的数据其实是在内存中,只是系统利用文件的型态将它记录到/run/log/下面, 在重新开机后,这些数据就会被清除掉。 当然, 也可以通过在/etc/systemd/journald.conf中更改设置, 设置它的存储位置和存储限制, 但一般都不会去更改它的这些存储相关的配置, 而是设置ForwardToSyslog=yes, 让journald把日志发送到rsyslog, 进入下一步。(journald自己也提供了很多功能,比如有:过滤输出,大小限制等等)

PS: 在还没有systemd的时代, 必须要开机完成并且执行了rsyslogd这个daemon之后,登录文件才会开始记录日志。所以,需要自己产生一个klogd的服务, 才能将系统在开机过程、启动服务的过程中的信息记录下来,然后等rsyslogd启动后才传送给它来处理。现在有了systemd主动调用 systemd-journald来协助记载登录文件, 在开机过程中的所有信息, 包括启动服务与服务若启动失败的情况等等,都可以直接被记录到systemd-journald.

第三步是rsyslog服务了, 在rsyslog服务启动后(以监控本地日志为例子,也就是加载module(load="imuxsock")) 从/run/systemd/journal/syslog这个socket消费syslog类型日志, 这些日志在经过预处理后会进入到主队列, 然后根据对应规则被分发, 由于rsyslog采用C/S结构,它可以将日志的信息追加到对应的日志文件中,一般在/var/log目录下(可以通过配置进行更改)。还可以把日志数据通过网络协议发送到另一台Linux服务器上,或者将日志存储在MySQL或Oracle等数据库中。 除此之外, rsyslog能够快速的过滤,转发,发送日志(官方说的每秒支持百万级日志),所以rsyslog的内容非常丰富。 rsyslog为了能让我们方便的使用这些功能, 提供了多个版本的配置语言, 我们只要通过对配置文件进行简单的修改,就能实现传输,过滤等功能。

如上面的结构图,rsyslog中只有一个主消息队列,任何消息都要先进入这个队列,然后直到进入到动作队列之后消息才会从这个队列中删除。通常,我们都不会去动主队列的配置,因为默认的设置已经工作得很好;消息经过主消息队列之后,就被rule processor解析和处理,然后根据预先配置的规则压入各自的动作队列,动作队列的消息最终被消费掉, 而我们经常要配置的就是这一部分。

可以发现, 上面这三步就是一个简单的消息队列, 应用程序通过syslog把日志输出到一个指定的log, 这是一个生产的步骤, systemd-journald则是以FIFO的形式暂存日志数据, 最后被rsyslog以消费者的形式进行消费。而rsyslog的内部则更像一个高级版本的消费队列, 一个简易版本的RabbitMQ

2.journald的配置

上面说到, 整个日志发送流程中, 对于journald只使用到它的暂存日志并转发到rsyslog的功能,所以这里分析的与暂存内存相关的配置, 如果要了解更多可以直接从man手册查看, 如果觉得内容太干, 也可以跳过, 都是从官网那边整理过来的。

  • Storage: 指定收到数据时, 如何存数据,默认值为none, 此外它还有几个值:

    • volatile: 表示仅保存在内存中, 也就是仅保存在/run/log/journal目录中(将会被自动按需创建)。
    • persistent: 表示优先保存在磁盘上, 也就优先保存在/var/log/journal目录中(将会被自动按需创建), 但若失败(例如在系统启动早期"/var"尚未挂载), 则降级转而保存在/run/log/journal目录中(将会被自动按需创建)。
    • auto: 与persistent类似, 但不自动创建/var/log/journal目录, 因此可以根据该目录的存在与否决定日志的保存位置。
    • none: 表示不保存任何日志(直接丢弃所有收集到的日志), 但日志转发不受影响。
  • Compress: 默认值"yes", 它表示压缩存储大于特定阈值(默认为512字节)的对象。 也可以直接设置一个字节值(可以带有 K, M, G 后缀)来表示要压缩存储大于指定阈值的对象。

  • Seal: 默认值"yes", 它表示如果存在一个"sealing key"(由 journalctl(1) 的 --setup-keys 命令创建), 那么就为所有持久保存的日志文件启用FSS(Seekable Sequential Key Generators)保护, 以避免日志文件 被恶意或无意的修改。

  • SplitMode: 设置是否按照每个用户分割日志文件,以实现对日志的访问控制(日志守护进程会确保每个用户都能读取自己的日志文件)。 默认值为uid, "uid" 表示每个用户都有自己专属的日志文件(无论该用户是否拥有登录会话),但系统用户的日志依然记录到系统日志中。此外还有一个值"none", 它表示不对日志文件按不同用户进行分割,而是将所有日志都记录到系统日志中。这意味着非特权用户根本无法读取属于自己的日志信息。 需要注意的是, 仅分割持久保存的日志(/var/log/journal), 永不分割内存中的日志(/run/log/journal)。

  • RateLimitIntervalSec: 用于设置一个时间段长度,可以使用下面的时间单位: "ms", "s", "min", "h", "d"来限制日志的生成速率.表示在 RateLimitIntervalSec 时间段内, 每个服务最多允许产生 RateLimitBurst 数量(条数)的日志。 在同一个时间段内,超出数量限制的日志将被丢弃,直到下一个时间段才能再次开始记录。 对于所有被丢弃的日志消息,仅用一条类似"xxx条消息被丢弃"的消息来代替。 这个限制是针对每个服务的限制,一个服务超限并不会影响到另一个服务的日志记录。 如果一个服务已经通过systemdLogRateLimitIntervalSec=LogRateLimitBurst=选项限制了自身的日志生成速率,那么将会覆盖此处的设置。

  • RateLimitBurst 用于设置一个正整数,表示消息条数,默认值是10000条, 说明见RateLimitIntervalSec。

  • SystemMaxUse: 限制磁盘使用量, 也就是/var/log/journal的使用量。限制全部日志文件加在一起最多可以占用多少空间。默认值是空间的10%与4G空间两者中的较小者;

  • SystemKeepFree: 限制磁盘使用量, 也就是/var/log/journal的使用量, 但与SystemMaxUse有所不同, 它的值的意思是除日志文件之外,至少保留多少空间给其他用途。systemd-journald会同时使用SystemKeepFreeSystemMaxUse两个值, 并且尽量限制日志文件的总大小,以同时满足这两个限制。默认值是空间的15%与4G空间两者中的较大者;

  • SystemMaxFileSize: 限制磁盘使用量, 也就是/var/log/journal的使用量。它限制的是单个日志文件的最大体积, 到达此限制后日志文件将会自动滚动。 默认值是对应的 SystemMaxUse值的1/8 , 这也意味着日志滚动 默认保留7个历史文件。日志大小 可以使用以1024为基数的 K, M, G, T, P, E 后缀, 分别对应于 1024, 1024², … 字节。

  • SystemMaxFiles, 限制磁盘使用量, 也就是/var/log/journal的使用量。 限制最多允许同时存在多少个日志文件, 超出此限制后, 最老的日志文件将被删除, 而当前的活动日志文件 则不受影响。 默认值为100个。

  • RuntimeMaxUse 限制内存使用量, 也就是/run/log/journal的使用量。限制全部日志文件加在一起最多可以占用多少空间。默认值是空间的10%与4G空间两者中的较小者;

  • RuntimeKeepFree 限制内存使用量, 也就是/run/log/journal的使用量。除日志文件之外,至少保留多少空间给其他用途。systemd-journald 会同时RuntimeKeepFree与RuntimeMaxUse , 并且尽量限制日志文件的总大小,以同时满足这两个限制。默认值是空间的15%与4G空间两者中的较大者;

  • RuntimeMaxFileSize 限制内存使用量, 也就是/run/log/journal的使用量。限制单个日志文件的最大体积, 到达此限制后日志文件将会自动滚动。 默认值是对应的 RuntimeMaxUse值的1/8 , 这也意味着日志滚动 默认保留7个历史文件。日志大小 可以使用以1024为基数的 K, M, G, T, P, E 后缀, 分别对应于 1024, 1024², … 字节。

  • RuntimeMaxFiles 限制内存使用量, 也就是/run/log/journal的使用量。 限制最多允许同时存在多少个日志文件, 超出此限制后, 最老的日志文件将被删除, 而当前的活动日志文件 则不受影响。 默认值为100个。

  • MaxFileSec 日志滚动的时间间隔。 默认值是一个月, 设为零表示禁用基于时间的日志滚动策略。 可以使用 "year", "month", "week", "day", "h", "m" 时间后缀, 若不使用后缀则表示以秒为单位。 通常 并不需要使用基于时间的日志滚动策略, 因为由 SystemMaxFileSize 与 RuntimeMaxFileSize 控制的基于文件大小的日志滚动策略 已经可以确保日志文件的大小不会超标。

  • MaxRetentionSec 日志文件的最大保留期限。 默认值零表示不使用基于时间的日志删除策略。可以使用 "year", "month", "week", "day", "h", "m" 时间后缀, 若不使用后缀则表示以秒为单位。 当日志文件的最后修改时间(mtime)与当前时间之差, 大于此处设置的值时,日志文件将会被删除。 通常并不需要使用基于时间的日志删除策略,因为由 SystemMaxUse= 与 RuntimeMaxUse= 控制的基于文件大小的日志滚动策略 已经可以确保日志文件的大小不会超标。

  • SyncIntervalSec 向磁盘刷写日志文件的时间间隔, 默认值是五分钟。 刷写之后,日志文件将会处于离线(OFFLINE)状态。 注意,当接收到 CRIT, ALERT, EMERG 级别的日志消息后, 将会无条件的立即刷写日志文件。 因此该设置仅对 ERR, WARNING, NOTICE, INFO, DEBUG 级别的日志消息有意义。

  • ForwardToSyslog 表示是否将接收到的日志消息转发给传统的 syslog 守护进程,默认值为"no"。 如果设为"yes",但是没有任何进程监听对应的套接字,那么这种转发是无意义的。 此选项可以被内核引导选项 "systemd.journald.forward_to_syslog" 覆盖。

  • ForwardToKMsg 表示是否将接收到的日志消息转发给内核日志缓冲区(kmsg),默认值为"no"。 此选项可以被内核引导选项 "systemd.journald.forward_to_kmsg" 覆盖。

  • ForwardToConsole 表示是否将接收到的日志消息转发给系统控制台,默认值为"no"。 如果设为"yes",那么可以通过下面的 TTYPath= 指定转发目标。 此选项可以被内核引导选项 "systemd.journald.forward_to_console" 覆盖。

  • ForwardToWall 表示是否将接收到的日志消息作为警告信息发送给所有已登录用户,默认值为"yes"。 此选项可以被内核引导选项 "systemd.journald.forward_to_wall" 覆盖。

  • MaxLevelStore 设置记录到日志文件中的最高日志等级,默认值为"debug";可以被内核引导选项"systemd.journald.max_level_store"覆盖 可以设为日志等级的名称, 也可以设为日志等级对应的数字: "emerg"(0), "alert"(1), "crit"(2), "err"(3), "warning"(4), "notice"(5), "info"(6), "debug"(7) 。 所有高于设定等级的日志消息都将被直接丢弃, 仅保存/转发小于等于设定等级的日志消息。

  • MaxLevelSyslog 设置转发给传统的 syslog 守护进程的最高日志等级, 默认值为"debug";可以被内核引导选项"systemd.journald.max_level_syslog"覆盖 选项说明同MaxLevelStore

  • MaxLevelKMsg 设置转发给内核日志缓冲区(kmsg)的最高日志等级,默认值为"notice"; 可以被内核引导选项"systemd.journald.max_level_kmsg"覆盖 选项说明同MaxLevelStore

  • MaxLevelConsole 设置转发给系统控制台的最高日志等级,默认值为"info";可以被内核引导选项"systemd.journald.max_level_console"覆盖 选项说明同MaxLevelStore

  • MaxLevelWall 设置作为警告信息发送给所有已登录用户的最高日志等级,默认值为"emerg";可以被内核引导选项"systemd.journald.max_level_wall"覆盖 选项说明同MaxLevelStore

  • ReadKMsg 是否收集内核日志。 默认值 yes 表示从 /dev/kmsg 中读取内核产生的日志消息。

  • TTYPath 指定 ForwardToConsole=yes 时所使用的控制台TTY, 默认值是 /dev/console

  • LineMax 在将日志流转化为日志记录时,每条日志记录最大允许的长度(字节)。 如果将单元的标准输出(STDOUT)/标准错误(STDERR)通过流套接字连接到日志中, 那么将会以换行符("\n", ASCII 10)与NUL字符("\0", ASCII 0)作为分割符, 把日志流切分成一条条独立的日志记录。 如果超过此处设置的长度之后仍然没有遇到分割符, 那么将会自动插入一个分割符,以强制将单行超长日志截断为多行。 此选项的值越大,每个日志流客户端日志守护进程占用的内存也越大(最大值等于此选项的值)。 另外,此选项的值太大也会造成与传统日志传输协议的不兼容(太长的日志无法封装在单个 AF_UNIX 或 AF_INET 报文内)。 此选项的值以字节为单位,同时也可以在数字的末尾加上 K, M, G, T 后缀(以1024为基准)。 默认值 48K 是一个足够大并且也能保持与传统日志传输协议兼容的值。 注意, 不能设为小于 79 的值(将被自动提升到79)。

3.rsyslog配置

上面说到, rsyslog的配置主要是针对主队列之后的所有模块, 同时它目前支持两种语法配置, 个人觉得两种都有它的方便性,所以以下的配置一般都是混用。rsyslog的配置文件一般位于:/etc/rsyslog.conf, 他可以引用模块, 设置变量, 还有设置消息规则, 而规则就是指明一条消息要怎么输出, 也就是上面所说的过滤器+动作队列+输出模块的组合。

3.1过滤器

消息经过主队列后进来的第一步就是过滤器, 过滤器有时候被称为选择器(selector), 它用于过滤消息, 并把消息指向对应的操作队列,rsyslog可以配置三种形式的过滤器, 分别为Facility/Priority-based过滤器, Property-based过滤器以及Expression-based过滤器。

3.1.1.Facility/Priority-based 过滤器

在syslog中指定消息发送时必须带有facilitypriority字段, 对于facility我更想把它定义是channel, 也就是渠道, Linux系统默认设置了24个facility, 前16个都有专门的用途, 后8个交给用户自定义, 一般来说一个线上的机器只运行几个服务, 8个是够的, 以下是Linux支持渠道的列表:

代码名称描述
0kern内核
1user用户级
2mail邮件
3daemon系统
4auth安全与授权
5syslog守护进程
6lpr打印相关
7news网络消息
8uucpuucp子系统
9时钟
10authpriv安全与授权
11ftpFTP
12-NTP
13-日志审计
14-日志报警
15cron定时器
16local0用户自定义
17local1用户自定义
18local2用户自定义
19local3用户自定义
20local4用户自定义
21local5用户自定义
22local6用户自定义
23local7用户自定义

而至于priority, 大家都会十分熟悉, 只不过这里换了个说法, 设计上他代表的就是日志等级, 支持的日志等级列表有:

代码名称简写描述
0Emergencyemerg紧急
1Alertalert报警
2Criticalcrit关键
3Errorerr错误
4Warningwarn警告
5Noticenotice通知
6Informationalinfo消息
7Debugdebug调试

了解完了FacilityPriority后就可以编写过滤器了, 过滤器编写十分简单, 他就是一个通配符, 通过.来区分FacilityPriority.前面是Facility, .后面是Priority。 例如通配符\*的意思是任何类型, 而\*.\*的意思就是匹配所有的FacilityPriority

举个例子, 一般默认的配置里都有一段规则:

cron.alert /var/log/cron

其中corn.alert就是规则中的FacilityPriority过滤器,其中Facility为cron的, Priority为alert, 一般来说就是crontab定期任务所产生的日志且任何等级等于或高于alert的日志(也就是代码值大于alert的日志等级)都会被指定的动作处理。 如果希望只处理定义等级的日志,而不处理高于这个等级的日志,可以使用等号(=),例如:

cron.=alert /var/log/cron

此外, 有一些子系统产生的日志可能没有Priority,那么可以使用关键字none,例如

news.none /var/log/messages

除了星号和等号,过滤器中还可以使用逗号(,), 感叹号(!)和分号(;),逗号用于分隔多个Priority,而感叹号的作用是取反,例如:

cron.!info,!debug /var/log/cron

意思是除了info和debug等级的日志外,都写入到/var/log/cron中。 对于分号(;), 它表示或的意思, 如系统中的默认配置:

*.=debug;\
    auth,authpriv.none;\
    news.none;mail.none -/var/log/debug

它表示的是*.=debug, auth,authpriv.none, news.nonemail.none四组过滤器都不要马上写入到/var/log/debug中。

3.1.2.Property-based 过滤器

Property-based过滤器是基于属性的过滤器,使我们可以根据不同的属性值进行日志处理。配置文件中常用的属性有:msg、hostname、fromhost、programname、timegenerated等。它的基本语法是:

PROPERTY,[!]COMPARE_OPERATION,"VALUE"

其中PROPERTRY为属性, COMPARE_OPERATION为操作符, VALUE为值

属性过滤器可用的操作符如下:

  • contains 检查属性值是否包含指定的字符串(大小写敏感)
  • contains_i 和上面一样,但忽略大小写
  • isequal 属性值是否等于目标字符串
  • startswith 属性值是否以某字符串开头(大小写敏感)
  • startswith_i 如上,但忽略大小写
  • regex 正则表达式匹配
  • ereregex 使用扩展正则表达式匹配
  • isempty 属性值是否为空

一个具体的例子是:

#日志信息中是否包含“error”字符串
:msg, contains, "error" 
#主机名称是否相等
:hostname, isequal, "host1"

3.1.3.Expression-based 过滤器

Expression-based过滤器是一个基于表达式的过滤器,这个表达式是一个条件表达式,即当满足特定条件的时候,执行指定的操作。 一般来说,表达式过滤器需要上面两种过滤器来结合使用。 基本语法如下(这里使用了新式语法):

if expression_true then action else action

else后续部分并不是必须的,它可以指定不满足条件的时候所执行的操作。 表达式中可用的操作符有:

  • and、or、not
  • ==、!=、<>、<、>、<=、>= (!=和<>的作用基本相等)
  • contains
  • startswith、startswith_i(case-insensitive)

一些例子如下::

#日志中包含error,保存到/var/log/errlog中
if $msg contains 'error' then /var/log/errlog
#如果要同时满足多个条件,使用and连接这些条件,整个表达式需要写在一行中
if $syslogfacility-text == 'local0' and $msg startswith 'DEVNAME' and ($msg contains 'error1' or $msg contains 'error0') then /var/log/somelog
if $syslogfacility-text == 'local0' and $msg startswith 'DEVNAME' and not ($msg contains 'error1' or $msg contains 'error0') then /var/log/somelog

3.1.4.动作

在上面介绍过滤器时, 其中有一段关于分号(;)配置示例:

*.=debug;\
    auth,authpriv.none;\
    news.none;mail.none -/var/log/debug

其中/var/log/debug前面的-就是一个动作, 表示日志不会马上写入到文件中,而是缓存到内存中。 常见的动作有:

  • 写入文件
  • 转发到另一台服务器
  • 写入数据库
  • 丢弃
  • 发送给用户

如从rsyslog的默认配置:

auth,authpriv.*	/var/log/auth.log
*.*;auth,authpriv.none	-/var/log/syslog
cron.*	/var/log/cron.log
daemon.*	-/var/log/daemon.log
kern.*	-/var/log/kern.log
lpr.*	-/var/log/lpr.log
mail.*	-/var/log/mail.log
user.*	-/var/log/user.log

*.emerg	:omusrmsg:*

可以看出rsyslog会根据规则写入到各个指定的文件夹,动作前面有一个(-)横杠,表示日志不会马上写入到文件中,而是缓存到内存中,这样可以提高日志系统的性能,但有可能会造成日志的丢失;使用波浪符号(~)表示处理需要丢弃的日志。 而omusrmsg则表示会把消息转发给指定的用户,用户登录时就可以收到该消息.默认为全部用户,如果需要指定用户则如下编写:

*.emerg	:omusrmsg:root,so1n

它代表会把消息发送给用户root以及我(so1n)

3.1.5.输出

rsyslog的输出有多种, 比如上面介绍过滤器时, 它的动作是把日志写入到哪个文件以及通过配置写入到数据库, 此外它还支持把日志通过远程传输的方式传到别的服务器。 rsyslog提供三个远程日志传输方式:

  • UDP: 基于传统UDP协议进行远程日志传输,也是传统syslog使用的传输协议; 可靠性比较低,但性能损耗最少, 在网络情况比较差, 或者接收服务器压力比较高情况下,可能存在丢日志情况。 仅在对日志完整性要求不是很高,以及在可靠的局域网环境下可以使用。

  • TCP: 基于传统TCP协议明文传输,需要回传进行确认,可靠性比较高; 但在接收服务器宕机或者两者之间网络出问题的情况下,会出现丢日志情况。 这种协议相比于UDP在可靠性方面已经好很多,并且rsyslog原生支持,配置简单, 同时针对可能丢日志情况,可以进行额外配置提高可靠性,因此使用比较广。

  • RELP: RELP(Reliable Event Logging Protocol)是基于TCP封装的可靠日志消息传输协议; 是为了解决TCPUDP协议的缺点而在应用层实现的传输协议,也是三者之中最可靠的。 不过需要多安装一个包rsyslog-relp以支持该协议。

远程传输需要更改客户端和服务端的配置, 首先是客户端配置, 客户端配置需要先配置对应的传输模块:

# 使用udp传输模块
module(load="imudp")
# 使用udp传输模块, 指定端口为514
input(type="imudp" port="514")

# 使用tcp传输模块
module(load="imtcp")
# 使用tcp传输模块, 指定端口为514
input(type="imtcp" port="514")

然后在动作那里配上如下格式

@[(zNUMBER)]HOST:[PORT]

其中@代表udp(UDP 在主机名前加"@";TCP 在主机名前加"@@";RELP 在主机名前加":omrelp:"),可选值zNUMBER设置了是否允许使用zlib对日志压缩(压缩级别1-9)。比如下面是一个使用relp发送的示例:

# local0.=warn,local1.* :omrelp:35.227.112.245:30514

而服务端的配置也不复杂, 首先是UDP, 先在服务端创建配置文件:/etc/rsyslog.d/udp.conf

# server configure
$ModLoad imudp # 加载模块
$UDPServerRun 10514 # 指定监听端口
$AllowedSender UDP, 10.0.0.0/16 # 设置白名单, 也就是客户端的IP

# 创建模板location和uformat
# location模板是根据客户端IP存放到不同目录下,以日期命名文件
$template location,/data/%fromhost-ip%/%$YEAR%-%$MONTH%-%$DAY%.log
# uformat模板是自定义日志格式
$template uformat,"%fromhost-ip% %msg%\n"

# 使用属性过滤器匹配非本地传输的日志, 然后执行动作, 这个动作使用了上面的模板, 也就是按照指定的文件路径及格式保存
:fromhost-ip, !isequal, "127.0.0.1" -?location;uformat
# 最后一句指明了该日志已经被匹配过了, 不需要其他的动作队列处理, 其中& 表示已经匹配处理的内容,~ 表示不再进行其他处理, 是stop元素的一个符号代替
& ~

接着就是TCP, 在服务端创建配置文件:/etc/rsyslog.d/tcp.conf

# server configure
$ModLoad imtcp # 加载模块
$InputTCPServerRun 20514 # 指定监听端口
$AllowedSender TCP, 10.0.0.0/16 # 设置白名单
# 根据客户端IP存放到不同目录下,以日期命名文件
$template location,"/data/log/syslog/%fromhost-ip%.log"
# 自定义日志格式
$template uformat,"%timestamp% %fromhost-ip% %rawmsg%\n"
# 把非本地传输的日志按照指定的文件路径及格式保存
:fromhost-ip, !isequal, "127.0.0.1" -?location;uformat
# & 表示已经匹配处理的内容,stop 表示不再进行其他处理
& stop

最后是RELP, 首先需要在客户端和服务端安装RELP

apt-get update
apt-get -y install rsyslog-relp

客户端需要额外加载模块

module(load="omrelp")

然后在服务端编辑配置(/etc/rsyslog.d/relp.conf)

# server configure
$ModLoad imrelp    # 加载模块
$InputRELPServerRun 30514    # 指定监听端口
# 根据客户端IP存放到不同目录下,以日期命名文件
$template location,"/data/log/syslog/%fromhost-ip%.log"
# 自定义日志格式
$template uformat,"%timestamp% %fromhost-ip% %rawmsg%\n"
# 把非本地传输的日志按照指定的文件路径及格式保存
:fromhost-ip, !isequal, "127.0.0.1" -?location;uformat
# & 表示已经匹配处理的内容,stop 表示不再进行其他处理
& stop

3.1.6模板

模板$template常用于接收端, 可用来定义消息格式、文件名。 语法如下:

$template <模板名>,<内容>,<可选项>
$template MyTemplateName,"\7Text %property% some more text\n",<options>

其中内容可以使用模板变量, 比如一条消息格式如下:

<接收内容的时间> <发送者的hostname> <$InputFileTag> <原始消息%msg%>
Dec 18 20:39:27 jumper-172-31-56-18 karltestdemoTag blala... dummy msg

如果只需要显示原始消息,可设置

$template CleanMsgFormat,"%msg%\n"

除了msg变量, 模板还支持很多变量,常见变量如下(更多变量见官网文档):

%msg%
%syslogfacility%
%HOSTNAME%
%syslogpriority%
%timereported:::date-mysql%
%timegenerated:::date-mysql%
%iut%
'%syslogtag%'

如果要生成动态文件名,并把日志写入该文件,那可以这样配置:

$template DynamicFile,"/var/log/test_logs/%timegenerated%-test.log"    # timegenerated属性从日志信息中提取出消息的时间戳,这样可以为每个日志生成唯一文件名称。
*.* ?DynamicFile

3.1.7常用模块

上面我们看到服务端会通过引入模块来拓展输出模块的功能, 比如输出到本地文件或者输出到Redis,Kafka等等...

imfile模块

imfile模块主要解决的问题是将非syslog日志转为syslog日志 假设有个没有按照syslog协议生成的日志文件/var/log/helloworld.log, 下面将用imfile模块把它加载到rsyslog中并处理:

# 加载imfile这个模块, 使用inotify模式来实时加载数据
module(load="imfile" mode="inotify" PollingInterval="1")
# 指定输入端使用了imfile, 配置是日志文件路径以及tag, severity, facility这些参数
input(type="imfile" File="/var/log/helloworld.log" Tag="helloworld" Severity="error" Facility="local0")
# 由于local有限,一般可以专门预留一个local用来接受带有Tag的日志,解决local不足的问题
# 将helloworld的应用日志发送到远程服务器
:programname, contains, "helloworld" @192.168.1.2

其他imfile配置如下,如果使用input做输入端,input的参数基本是以下配置的名称去掉input前缀, 更多配置见官网

$InputFileName /path/to/file # 待监控的文件路径
$InputFileTag tag # 文件唯一标识tag,最好保持唯一,用于接收端区分原始log文件,可以包含特殊字符,如":"、","等 
$InputFileStateFile /path/to/state/file
# 需要保证发送端唯一,记录读取到哪儿,状态文件保存在$WorkDirectory,默认为 /var/lib/rsyslog
# 如果某个要监控的文件名变化了,一定要重新设置该值
$InputFileFacility facility # log类型,默认local0, local开头的表示自定义类型
$InputFileSeverity severity # log级别:info,warning,默认notice
$InputRunFileMonitor # 启动监控当前的文件,如果忘记这行,则啥事也不会发生
$InputFilePollInterval seconds # 全局设置,默认轮询是10s
$InputFilePersistStateInterval lines # 每多少行更新state文件状态 
$InputFileMaxLinesAtOnce number # 默认10240,如果在发送端,需要同时监控多个文件,会处理完当前文件特定行后,切换到下一个文件,避免一个文件一直占用处理,导致收集别的文件不及时。
$InputFileBindRuleset ruleset # 绑定ruleset,可以把这个listener绑定到特点的规则(http://www.rsyslog.com/doc/v5-stable/concepts/multi_ruleset.html)

在input的语法中stateFile参数已经不建议使用。原因在于,为了防止出现重复的state files,rsyslog会基于下面的规则自动生成这些文件:

  • 在具体的被监控文件前添加”imfile-state:”字符串
  • 文件名前的反斜杠会被替换为短横杠。 尽量在文件刚生成时初始化日志,或者使用freshStartTail参数,然rsyslog并不建议使用该参数...

omprog模块

omprog模块可以让日志通过管道的形式发送给程序(以每行日志分开发送),然后再由程序处理日志(类似于Map-Reduce)。

以下是官网的一个例子, 例子中该模块会根据配置执行Python程序,并通过stdin的形式发送到Python程序,Python程序会一直运行,等待数据的到来,如果收到数据则处理数据(这个例子是写入数据库), 如果程序终止,则重新启动,如果rsyslo终止,则程序的stdin会捕获到EOF,此时程序会终止.

# 配置
module(load="omprog")

# 省略了过滤器, 只说一下语法
action(type="omprog"
       name="db_forward"
       binary="/usr/share/logging/db_forward.py"
       confirmMessages="on"  # 它告诉rsyslog等待程序确认其初始化以及收到的每条消息。该设置的目的是防止由于数据库连接失败而导致日志丢失。如果程序无法将日志写入数据库,它将通过stdout向rsyslog返回否定确认
       confirmTimeout="30000"  # 指定超时时间内未收到程序的响应,则rsyslog将终止并重新启动它
       queue.type="LinkedList"
       queue.saveOnShutdown="on"
       queue.workerThreads="5" # 使用具有(最多)5个工作线程的专用磁盘辅助队列,以避免在高负载时影响其他日志目标
       action.resumeInterval="5" # Rsyslog将失败的日志保留在队列中,并在5秒后再次将其发送给程序。
       killUnresponsive="on"
       output="/var/log/db_forward.log"  # 程序将错误详细信息写入stderr,rsyslog捕获并写入/var/log/db_forward.log
)

除了使用模块接收日志,还可以用程序读取日志(建议用上inotify),或者开个端口接收日志

3.2.各个日志文件简介

  • /var/log/secure:记录用户登陆系统的信息,比如SSH,telnet,ftp等记录

  • /var/log/btmp:记录登陆失败的信息,被编码过,所以必须使用last解析

  • /var/log/messages:在开机运行中几乎所有的系统发生的错误都在此记录。

  • /var/log/boot.log:记录一些开机或者关机启动的一些服务信息

  • /var/log/cron:用来记录crontab这个服务执行任务计划产生的日志

  • /var/log/utmp:记录现在登陆的用户

  • /var/log/dmesg:内核日志

  • /var/log/kern:内核产生的信息

  • /var/log/daemon.log:系统监控程序产生的日志。

3.3.队列

队列是在rsyslog中我们在使用中最无感, 就像上面我们使用的语法中, 其实都用到了队列, 但是很难发现它的存在, 即便如此队列也是最重要的, 因为队列可以加速日志的传输,还会让传输更加可靠。 Rsyslog中分为两种队列, 主队列和动作队列。

以下内容大多数来源于官方文档

3.3.1主队列

一般日志进来到rsyslog时,都会先进入主队列,然后根据配置消费日志分发到各个队列以及执行动作,一般主队列的鲁棒性很强,我们都不用去修改他, 改改参数即可, 如使用下面命令可以告诉主消息队列在关闭时保存其内容:

$MainMsgQueueSaveOnShutdown on

3.3.2动作队列

动作队列用于接受主队列的消息,并根据规则执行动作,一般与动作绑定在一起, rsyslog的动作队列有以下4种:

  • Direct queue 该队列是默认队列,如果对action没进行配置,则默认采用该队列,该队列既不排队也不缓冲任何队列消息,而是直接将消息传给消费者。同时,该队列是唯一一个会把执行结果从消费者(action processor)返回给生产者的队列。通过这个返回值提醒action queue,让action queue取回这些处理失败的消息,如此循环,直到消息处理成功。

  • Disk queue
    该队列使用硬盘进行缓冲,而不在内存中缓冲任何内容。因此该队列是超级可靠的,但到也是效率最慢的队列。正常情况下,不建议使用此队列模式。如果日志数据非常重要,为了确保在极端情况下也不能丢失,则可以使用该队列。 写入该队列时,它是分块完成的。每个块接收其各自的文件。文件以前缀命名(通过$<object>QueueFilename设置),后跟7位数字(从1开始,每个文件递增)。默认情况下,块为10mb,可通过$<object>QueueMaxFileSize设置不同的大小。不过,大小限制不是一个很严格的限制,因为rsyslog总是写一个完整的队列条目,即使它会比大小限制还大。因此,块实际上比配置的大小大一点(通常小于1k),所以,每个块的大小都会不一样。 此外,每一个队列可以使用不同的位置保存数据,可以通过队列的$WorkDirectory指令设置,这个指令需要在队列创建之前配置。

  • memory queue (LinkedList/FixedArray) 这种类型的队列会把所有的消息都保存在内存中,因此它的处理速度非常快,缺点是当电脑关闭或死机的时候,所有未被处理的消息都会丢失。如果希望电脑关机的时候保存这些消息,可以设置变量$<Object>QueueSaveOnShutdown

    memory queue队列拥有两种模式:

    • FixedArray queue: 预先分配一定的内存来保存这些消息,它的缺点是,无论你的日志有多少,它都需要完全占用这些内存;好处是当数据量不大的时候,它的性能是最好的。
    • LinkedList queue: 内存是运行时分配的,会根据数据量的不同而作出调整,好处是内存利用率高,LinkedList队列适合使用在一些突发数据量大的场景。

    如果不知道用哪个模式,建议使用LinkedList模式, 因为它与FixedArray相比,处理开销较低,并且可以通过减少内存使用量来弥补。在大多数不使用的指针数组页面中分页可能比动态分配它们慢得多。创建不同类型的队列方法为分别使用$<object>QueueType LinkedList or /$<object>QueueType FixedArray创建 LinkedLIst和FixesArray队列。

  • Disk-Assisted In-memory queue 这种队列实际上是以内存队列为主,Disk Queue为辅的队列。在正常情况下,不会使用辅助的Disk queue,但当内存队列被填满,或者主机关闭的时候,Disk Queue就会被激活,数据被写入硬盘。结合两者使用,可以同时满足速度和数据的可靠性。

设置队列的旧式语法如下:

$ActionQueueType LinkedList
$ActionQueueFileName fileName

#还有另外两个参数设置
$<Object>QueueHighWatermark  #当队列中的数据超过这个设置的值的时候,要么把数据保存,要么把数据丢弃,如果是Disk-Assisted In-memory Queue,队列中的数据超过这个值,Disk Queue就会被激活。
$<Object>QueueLowWatermark  #和上面的相反,这是一个低水位设置,当数据小于这个值的时候,就停止相关的操作,如果是Disk-Assisted In-memory Queue,数据低于这个值,Disk Queue就会被取消激活状态。

但是旧语法使用起来很奇怪, 我更喜欢使用新语法, 新式语法使用如下, action表示要执行的操作, 比如这个action指明要以什么方式发送, 发送到哪里, 这个action绑定的是哪个队列:

if $syslogfacility-text == 'local6' then action(
       type="omrelp"\ 
       Target="127.0.0.1"\
       Port="30514"\
       queue.type="LinkedList"\
       queue.spoolDirectory="/var/spool/rsyslog"\
       queue.filename="demo"\
       queue.size="100000"\
       queue.maxdiskspace="1g"\
       queue.highwatermark="60000"\
       queue.lowwatermark="2000"\
       queue.discardmark="80000"\
       queue.timeoutenqueue="3000"\
       queue.maxfilesize="200m"\
       queue.dequeuebatchsize="1000"\
       queue.saveonshutdown="on"\
       action.resumeRetryCount="-1"\
)

3.3.3队列管理

队列的管理过程实际上是对队列的参数进行调优的过程。主队列的参数和动作队列的参数基本一样。这些参数必须在队列创建之后才能使用,每个不同的队列可以设置不同的数值,这些值在下一个队列创建之前被重置,前一个队列设置的值不会影响到下一个队列(如果使用新式队列, 则会与action绑定)。

  • 限制队列容量

    • $<Object>QueueSize <number>
    • $<Object>QueueHighWaterMark <number> 两者之间有细微的差别 $<Object>QueueSize用于设置队列的总容量,即队列可容纳的消息数量。 而$<Object>QueueHighWaterMark只用于disk-assisted类型的队列,当队列中的消息数量达到这个值之后,消息就会被写入到硬盘。但是这种行为是有依赖性的,仅当日志的输出目标无法到达的时候(数据库无法访问,远程服务器离线等),它才会发生。
  • 丢弃消息

    • $<Object>QueueDiscardMark 设置队列的最高值,当队列中的消息达到这个指定的值时,消息就会被丢弃
    • $<Object>QueueDiscardSeverity 设定要丢弃哪些日志消息(见rsyslog的 Priority)
  • 队列的终止 只有在系统被关闭的那一刻,队列才会被结束。当队列被终止的时候,队列中可能有数据尝试进入,rsyslog会试图处理这些数据,可以使用配置控制rsyslog的做法.

    • $<Object>QueueTimeoutShutdown <milliseconds> 队列会仍然去处理这些数据,当队列关闭时间超过这个值,队列中的所有数据被丢弃
    • $<Object>QueueTimeoutActionCompletion 只处理当前被处理的消息,其他的消息全部被抛弃
    • $<Object>QueueSaveOnShutdown 不丢弃任何消息且队列是Disk-Queue或者Disk-assisted-Queue

3.4Ruleset

Ruleset就是多个规则的集合,当消息进来后rsyslogd会从ruleset的第一条规则开始处理,直到这个ruleset的最后一条规则或者是设置了终止标记。rsyslogd有一个默认的Ruleset,名为RSYSLOG_DefaultRuleset, 它的使用方法如下:

$template Centos7Server,"/var/log/%hostname%/messages-%$now%.log"
$RuleSet remote
*.* ?Centos7Server
 
$RuleSet RSYSLOG_DefaultRuleset

这个方法先创建了一个名叫remote的Ruleset,规则中使用了一个预先定义的模板。最后切换回默认的Ruleset,不切换回去的话,后面的配置就还是remote Ruleset。

多个Ruleset的使用场景在于区分本地写日志和网络传日志. 本地通过$DefaultRuleset命令指定Ruleset,而利用TCP传输模块可以通过如下方法设置规则如下:

$ModLoad imtcp
$InputTCPServerBindRuleset remote #绑定自定以的Ruleset
$InputTCPServerRun 514

这时走TCP的日志就会使用remote Rules,如果是正常启用TCP传输日志,那么会走DefaultRuleset指定的ruleset,因为没有使用InputTCPServerBindRuleset去覆盖Ruleset. 如果有多个端口传输日志,每个端口的ruleset不同,那可以利用InputTCPServerBindRuleset 的覆盖性进行如下配置:

$InputTCPServerBindRuleset remote10001
$InputTCPServerRun 10001
 
$InputTCPServerBindRuleset remote10002
$InputTCPServerRun 10002
 
$InputTCPServerBindRuleset remote10003
$InputTCPServerRun 10003

但是我会更喜欢用新式语法写rule。

3.5.RainerScript

上面的介绍中我经常说新式语法和旧式语法, 其中新式语法的名字就叫RainerScript它是一个比较新的配置语言, 具有更紧凑的语法,使配置过程更加清晰,更不容易出错, 同时能支持复用, 让人觉得更像在写一个变成语言, 在上面有些情况我已经用了新式的语法了, 下面看看新式语法的区别:

  • 加载模块

    # 旧语法
    $ModLoad imtcp
    # 新语法
    module(load="imtcp")
    
  • input

    上面已经有使用imfile的例子了,这里就不多阐述

    # 旧语法
    $ModLoad imfile #导入模块
    $InputFileName /var/log/nginx/access.log #需要导入的文件
    $InputFileTag ng-acc #添加标签名称
    $InputFilePersistStateInterval 10 #多久处理一次追踪文件
    $InputFileStateFile state-ng-acc
    $InputRunFileMonitor #开始监控文件
    # 新语法
    module(load="builtin:imfile")
    input(Type="imfile" File="/var/log/nginx/access.log" Tag="ng-acc"PersistStateInterval=10)
    
  • action

    action是输出模块,包括了结构图里的action和队列

    # 旧语法
    *.* /var/log/messages
    # 新语法
    *.* action(Type="omfile" File="/var/log/messages")
    # 包含队列的新语法
    action(type="ommysql" server="localhost" db="Syslog" uid="root" pwd="password"\
    queue.type="LinkedList" queue.filename="name" queue.saveonshutdown="on"\
    action.resumeRetryCount="-1"
    
  • template

    新的template对象提供了两种常用的模板定义方式,一种是string,另一种是list。string方式比较接近旧template的定义方式,而list方式可以更清晰的区分字符串和属性值。

    # 旧语法
    $template remote, "message from %hostname%, received at %timegenerated%."
    # 新语法string
    template(namme="remote" type="string" string="message from %hostname%, received at %timegenerated%.")
    # 新语法list
    # constant用于定义字面字符串值,而property用于定义实际的属性
    template(name="dbFormat" type="list" option.sql="on") {
        constant(value="insert into SystemEvents (Message, FromHost,ReceivedAt)")
        constant(value=" values ('")
        property(name="msg")
        constant(value=", '")
        property(name="hostname")
        constant(value="', '")
        property(name="timegenerated" dateFormat="mysql")
        constant(value="')")
    }
    
  • ruleset

    ruleset推荐用新的语法,比原来的清晰了很多。

    # 旧语法
    $Ruleset remote
    mail.none /var/log/mail.log
    cron.none /var/log/cron.log
    
    $ModLoad imtcp
    $InputTCPBindRuleset remote
    $InputTCPServerRun 514
    # 新语法
    ruleset(name="remote-514") {
    cron.none action(type="omfile" file="/var/log/remote-514-cron")
    mail.none action(type="omfile" file="/var/log/remote-514-mail")
    }
    
    input(type="imtcp" port="514" ruleset="remote-514");
    

4.日志轮转--Logrotate

在查看日志时我们很容易就发现出现有命名类似于syslog syslog.1 syslog.2.gz等类型的日志,这些日志都是靠Logrotate进行日志轮转的,方便我们对日志进行查找和管理。 Logrotate的主脚本位于/etc/cron.daily/logrotate, 它不会自动运行, 而是通过contab定时运行脚本执行日志轮转。 Logrotate不止可以针对系统日志, 还可以使一些应用本身不带日志轮转功能,或者本身日志轮换功能残缺的应用(对就是说Supervisor)拥有完善,高自定义的日志轮转功能。

如果多个程序(多个进程)写入到同一个日志文件,那请不要使用程序自己的轮转来轮转日志, 不然会出现丢数据的情况。建议所有的日志轮转都使用Logrotate

Logrotate实际运行时,会调用配置文件/etc/logrotate.conf。我们除了修改/etc/logrotate.conf文件外, 还可以在/etc/logrotate.d目录里放置自定义好的配置文件,用来覆盖Logrotate的缺省值。 同时, 这些脚本我们也可以手动执行, 不过要记得添加-f参数,表示强制轮转, 否则Logrotate会判断还没达到条件而不执行轮转。此外, 手动运行时最好要添加-d参数来测试配置文件是否有错误,常用参数如下:

logrotate [OPTION...] <configfile>
-d, --debug :debug模式,测试配置文件是否有错误。
-f, --force :强制转储文件。
-m, --mail=command :压缩日志后,发送日志到指定邮箱。
-s, --state=statefile :使用指定的状态文件。
-v, --verbose :显示转储过程。

Logrotate的原理是检查日志是否需要轮转, 以及在轮转后使进程重新打开文件描述符(有两种方法, 最优的方法是发送信号量, 不过需要程序支持), 确保进程能重新打开文件,写入到新的文件。

如果程序不支持信号量重载日志文件描述符的方法, 那就只能在脚本启用copytruncate,该方法会直接把文本的内容复制到新的文件后再清空当前的日志文件, 如果文件的数据量过高则有可能漏掉一些日志。

一般来说,我们只要在/etc/logrotate.d/目录下配置针对某个日志的日志文件轮转配置就可以了, 这个配置文件主要是声明要轮换的日志文件路径以及花括号内配置轮转属性, 以Nginx为例子:

# 匹配Nginx的所有日志
/usr/local/nginx/logs/*log {
    daily
    # 每天转储
    rotate 30
    # 只保存30个备份
    missingok
    # 在日志转储期间,任何错误将被忽略
    notifempty
    # 文件为空时不转储
    compress
    # 通过 gzip 压缩
    dateext
    # 日志文件以当前日期为格式结尾
    sharedscripts
    # 所有日志文件转储完毕后执行一次脚本
    postrotate
    # 转储之后执行命令, 和endscript成对使用
    # 该命令使nginx重新打开日志文件描述符,
        /bin/kill -USR1 \$(cat /usr/local/nginx/logs/nginx.pid 2>/dev/null) 2>/dev/null || :
    endscript
    # 转储之后执行命令,和postrotate成对使用
}

此外, 如果像rsyslog一样控制的日志比较多,可以像以下这样配置,多个日志文件多行显示,并同样的用花括号把配置包起来

/var/log/mail
/var/log/messages
/var/log/syslog
{
    sharedscripts
    dateext
    rotate 25
    size 40M
    compress
    dateformat -%Y%m%d%s
    postrotate
            /bin/kill -HUP `cat /var/run/syslogd.pid 2> /dev/null` 2> /dev/null || true
    endscript
}

4.1logrotate参数

Logrotate的主要使用也是以配置为主,需要注意一些配置, 不然logrotate会以文件太小或者时间间隔比较低(如配置1天轮转一次,logrotate首次轮转可能要间隔两天)而不进行轮转

参数描述
dateext切换后的日志文件会附加上一个短横线和YYYYMMDD格式的日期,没有这个配置项会附加一个小数点加一个数字序号.
dateformat配合dateext使用可以为切割后的日志加上YYYYMMDD格式的日期,如dateformat -%Y%m%d
compress通过gzip 压缩转储以后的日志
nocompress不需要压缩时,用这个参数
copytruncate用于还在打开中的日志文件,把当前日志备份并截断
nocopytruncate备份日志文件但是不截断
create mode owner group转储文件,使用指定的文件模式创建新的日志文件
nocreate不建立新的日志文件
delaycompress一起使用时,转储的日志文件到下一次转储时才压缩
nodelaycompress覆盖 delaycompress 选项,转储同时压缩。
errors address专储时的错误信息发送到指定的Email 地址
ifempty即使是空文件也转储,这个是 logrotate 的缺省选项。
notifempty如果是空文件的话,不转储
mail address把转储的日志文件发送到指定的E-mail 地址
nomail转储时不发送日志文件
olddir directory转储后的日志文件放入指定的目录,必须和当前日志文件在同一个文件系统
noolddir转储后的日志文件和当前日志文件放在同一个目录下
sharedscripts运行postrotate脚本,作用是在所有日志都轮转后统一执行一次脚本。如果没有配置这个,那么每个日志轮转后都会执行一次脚本
prerotate/endscript在转储以前需要执行的命令可以放入这个对,这两个关键字必须单独成行
postrotate/endscript在转储以后需要执行的命令可以放入这个对,这两个关键字必须单独成行
daily指定转储周期为每天
weekly指定转储周期为每周
monthly指定转储周期为每月
rotate count指定日志文件删除之前转储的次数,0 指没有备份,5 指保留5 个备份
tabootext [+] list让logrotate 不转储指定扩展名的文件,缺省的扩展名是:.rpm-orig, .rpmsave, v, 和 ~
size size当日志文件到达指定的大小时才转储,后缀MB.

5.一些常见坑

可以看到整套日志流程涉及多个流程和多种参数, 所以容易因为参数导致出现一些问题, 以下是我遇到的几个问题和解决办法:

  • log太长了,被截断; 这是由于被$MaxMessageSize限制的,它的默认大小是2k,大概可以保存1000个中文字符,在加载imtcp/imudp之前设置, 此配置包括发送和接收,所以rsyslog客户端、服务端都要设置
  • 如果一行日志消息大小大于4K,只能用TCP。这是因为UDP栈大小限制的。
  • 某个系统日志文件过大; 像用debian的话,可以看到/var/log/syslog会随着你的配置,里面的东西越来越多,主要要对该行默认配置进行修改,不然所有日志都会发送到这里了(以定义了local5,6为例子)
    *.info;mail.none;authpriv.none;cron.none /var/log/syslog
    *.info;mail.none;authpriv.none;cron.none;local5.none;local6.none /var/log/messages
    
  • 接收端保存的文件路径不对; 主要是用到了tag, 且他们两个tag有共同的前缀导致匹配出错,解决办法是调整顺序, 把长的放在前面, 比如如下配置:
    # For erp_wms
    $template erp_wms_FileFormat,"/Data/logs/erp/wms/%fromhost-ip%/%syslogtag:F,44:2%-%$YEAR%%$MONTH%%$DAY%.log"
    if $syslogtag startswith 'erp_wms' then ?erp_wms_FileFormat;CleanMsgFormat
    & ~
    
    # For erp_wms3
    $template erp_wms3_FileFormat,"/Data/logs/erp/wms3/%fromhost-ip%/%syslogtag:F,44:2%-%$YEAR%%$MONTH%%$DAY%.log"
    if $syslogtag startswith 'erp_wms3' then ?erp_wms3_FileFormat;CleanMsgFormat
    & ~
    
    修改后
    # 这里注意下面的tag的顺序, 一定要让长的tag(erp_wms3)保持在上面,因为他们有共同的前缀(erp_wms)
    # For erp_wms3
    $template erp_wms3_FileFormat,"/Data/logs/erp/wms3/%fromhost-ip%/%syslogtag:F,44:2%-%$YEAR%%$MONTH%%$DAY%.log"
    if $syslogtag startswith 'erp_wms3' then ?erp_wms3_FileFormat;CleanMsgFormat
    & ~
    
    # For erp_wms
    $template erp_wms_FileFormat,"/Data/logs/erp/wms/%fromhost-ip%/%syslogtag:F,44:2%-%$YEAR%%$MONTH%%$DAY%.log"
    if $syslogtag startswith 'erp_wms' then ?erp_wms_FileFormat;CleanMsgFormat
    & ~
    
  • 接收端rsyslog文件名太长后被截断, 这个一般是发送端的配置造成的, 如以下发送端默认配置:
    template (name="ForwardFormat" type="string" string="<%PRI%>%TIMESTAMP:::date-rfc3339% %HOSTNAME%
    %syslogtag:1:32%%msg:::sp-if-no-1st-sp%%msg%")
    
    中的%syslogtag:1:32%限制了文件名长度,所以需要发送端重新绑定到一个新的模板

6.常见rsyslog不记日志问题

前记里说过, 我是因为遇到日志写入缺少时才开始查看整个Linux的日志机制, 在查阅资料后我总结了以下几个不记日志的原因:

  • 1.OOM kernel不能及时释放cache来满足应用程序的突发内存需求的情况。
  • 2.系统内存严重不够或者Rsyslog限制了它将在内存中保留的消息数量。如果该消息大于系统中的内存,则需要调整它。要么给机器更多内存,要么将rsyslog配置为更小的队列大小。
  • 3.jounald配置问题, 数据只暂存于内存中, 且由于系统内存不够导致部分日志被删除了。
  • 4.回车控制符(来源于网络,没遇到过) 有可能是由于某用户的debug或者info log中,包含了回车控制符\n,而我们rsyslog client段的配置文件中EscapeControlCharactersOnReceive是off的,即不对控制符做转义,所以含有\n控制符的log被发送给了rsyslog center。而rsyslog center是根据回车控制符\n来判断是不是一条log的。如果用户的log中包含类似字段:GET /123/id=123&\n90887294--sdf,那么rsyslog会把”GET /123/id=123&“当作一条log,而把90887294作为下一条message的长度,并且会有刚才的Framing Error的报错。rsyslog会等待接收90887294byte的数据,然后判断大于了MaxMessageSize 4KB,然后rsyslog就只保存了从90887294开始之后的4KB日志,剩下的90887294 - 4*1024= 90883198byte的数据全被截断丢弃。这就有了received oversize message的报错。 解决办法:
    issues:https://github.com/rsyslog/rsyslog/issues/111
    1,开启rsyslog client的EscapeControlCharactersOnReceive为on状态。对回车控制符进行转义,即可避免此问题
    影响:把用户log中的\n转换成了以#开头的三位八进制数#012。(根基ascii表转换)
    2,关闭rsyslog center的SupportOctetCountedFraming为off状态。即不支持Octet数据流。
    影响:当用户log中有\n的时候,log会被截断。
    
  • 5.rsyslog 性能跟不上导致丢日志,需要进行调优如对rsyslog的main queue和action queue进行调优

7.附录A:用python发送日志到syslog

Python的logging模块除了自己输出日志外, 也兼容Linux的日志机制。 修改方法也十分简单, 只需要对logger进行一些小修改, 就可以把日志发向/dev/log/从而被rsyslog捕获到日志, 以下是一个普通的logging代码:

# 初始化logger
logger = logging.getLogger('exp')
logger.setLevel(logging.INFO)
# 初始化handler
filehandler = logging.FileHandler(os.environ['HOME'] + '/exp.log', encoding='utf8')
filehandler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s"))
logger.addHandler(filehandler)

进行以下修改后, 他就可以发送到rsyslog了:

# 初始化logger
logger = logging.getLogger('exp')
logger.setLevel(logging.INFO)
# 初始化handler, 这个handler是sysloghandle, 且定义了输出是本机的/dev/log, 还定义了一些syslog参数
sys_handler = logging.handlers.SysLogHandler('/dev/log', facility=logging.handlers.SysLogHandler.LOG_LOCAL0)
sys_handler.setFormatter(logging.Formatter('exp_log' + ":%(asctime)s - %(name)s - %(levelname)s - %(message)s"))
logger.addHandler(sys_handler)

另外,如果你想用python读日志的话也可以直接监听/dev/log/再打印日志

import socket
import time
sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
sock.bind('/dev/log') #需要先手动kill掉已经在运行的[r]syslogd进程,否则这里无法执行成功
while True:
    time.sleep(5)
    data, addr = sock.recvfrom(1024)
    print(data)