Graylog 集中日志系统

Ian Yang
22 min readOct 18, 2017

--

使用 Graylog 之前试过很多方案,包括流行的 ELK,也用过 fluentd 搭配各种存储,influxdb, mongodb 等等。但这些方案在日志量大了之后出现性能瓶颈都没有提供解决方案。而 Graylog 作为整合方案,使用 elasticsearch 存储,mongodb 缓存,带流量控制 (throttling),简单易用的查询界面,方便的管理界面,易于扩展。转移到 Graylog 省心了不少。在使用过程中积累了些经验,所以分享出来。

安装

官方提供了自动化和手动安装,以及集群配置的文档。稍微麻烦点的是安装 mongodb 和 elasticsearch 集群,以及配置 graylog 里的各种地址。

Mongodb 在有防火墙保护下,最简单的集群方案是不设置验证,只需要所有节点配置相同的 replSet 就行了

replSet = graylog

然后按照文档初始化一下,注意 _id 要和配置的 replSet 一致。

rs.initiate( {
_id : "graylog",
members: [ { _id : 0, host : "logs1.example.com:27017" } ]
})
rs.add("logs2.example.com")
rs.add("logs3.example.com")

Elasticsearch 类似,配置相同的 cluster name 并列出集群内机器

cluster.name: graylog
discovery.zen.ping.unicast.hosts:
- logs1.example.com
- logs2.example.com
- logs2.example.com

Graylog 只要保证只有一个节点配置成了 master

is_master = true

比较容易出错和混淆的就是各种地址的配置,主要是有些没配置会用另外的选项作为默认值。推荐是显式的配置所有下面四个选项:

  • rest_listen_uri 用来指定 API 启动时监听的网卡,端口和 API 地址前缀。没什么特殊原因,这个配置成 http://0.0.0.0:9000/api/ 就行了
  • web_listen_uri 同上,但是是 WEB 资源的 HTTP 服务器,可以和 API 使用相同的端口,一般配置成 http://0.0.0.0:9000/ 就可以了
  • rest_transport_uri 这个是节点暴露给集群其它节点访问用的 API 地址,一般把 rest_listen_uri 中的 IP 换成内网 IP 或者域名就行了,比如 http://logs1-internal.example.com:9000/api/
  • web_endpoint_uri 是暴露给 WEB 界面里的 Javascript 连接用的 API 地址,一般把 rest_listen_uri 中的 IP 换成公网 IP 或者域名就行了,比如 http://logs1.example.com:9000/api/。如果设置了负载均衡,或者代理,可以配置成负载均衡和代理的公网地址,也可以通过 HTTP Header 来覆盖该配置,比如 Nginx
proxy_set_header X-Graylog-Server-URL http://logs.example.com/api;

Graylog 成功启动后在 Web 管理界面就能查看节点状态和 elasticsearch 集群状态。Mongodb 可以在 mongo 命令行客户端里执行以下两个命令验证:

rs.conf()
rs.status()

收集

Graylog 通过 Inputs 收集日志,方式以被动接收为主,需要在产生日志的地方将日志发送给 Graylog。比较常用的一些方式:

  • 程序中直接集成可以发送日志给 Graylog 的库,在 Github 中搜索 GELF 可以找到大量各种语言、框架、环境下的库。
  • 采用传统的文件来记录日志,在机器上启动一个 agent 程序抓取日志文件新的内容然后发给 Graylog。
  • 使用 syslog 写日志,利用 rsyslog 的转发功能把日志发给 Graylog。

服务器端日志个人推荐最后一种方式,优点有

  • 不依赖 Graylog,可以替换成任何能接收 syslog 的其它方案。
  • 容易 Fallback,rsyslog 可以配置成同时保存到本地文件和转发 Graylog。当 Graylog 出现问题至少还有本地日志文件可以用。
  • rsyslog 的日志保存,转发已经非常成熟和稳定。

缺点是 rsyslog 日志会把整条日志作为 message 字段保存。Graylog 内部每条日志是作为 Elastic 的 Document 保存的,细化出更多的字段能满足复杂查询和数据分析的需求。所以 Graylog 基于 JSON 制定了 GELF 协议,使用 GELF 协议的 Input 可以在产生日志时直接设置各种字段。不过 Graylog 还提供了 Extractor,和 fluentd, logstash 中的 filter 相似,可以从日志中提取结构化字段出来,比如 JSON Extractor 可以解析 JSON 格式日志。使用过类似工具的应该对 Grok 很熟悉,Graylog 也提供了支持。所以使用 rsyslog 也只需要制定下日志格式,然后配置下 Extractor。

Graylog 自带了丰富的 Inputs 可供使用,同时可以通过插件扩展,可以结合自己的使用场景选择合适的方式。

本文接下来会介绍日志收集需要注意的地方,以及如何基于 rsyslog 来打造集中日志系统。

日志处理队列

Graylog 内部使用 Kafka 实现了称为 Journal 的队列系统,来缓存接收的日志。这个对集中日志系统相当重要。日志的特性决定了量大,并且分布不平均,会突发地集中在某个时间段产生大量日志。队列能有效的防止突发大数据量输入导致系统瘫痪,将闲时利用起来处理积压的日志。同时还能作为是否要扩充集群提高 Elastic 写入速度的指标。

Graylog 集群每个节点有自己独立的 Journal,扩展 Graylog 本身节点数量不但可以提升日志处理速度,还可以提升队列缓存的容量。

当 Elastic 写入出现瓶颈,Journal 的队列长度会一直增长,Graylog 设置了俩个阀值,当积压的日志超过 12 小时未处理,或者占用磁盘超过 5G 就会开始丢弃新接收到的日志。通过配置 message_journal_max_agemessage_journal_max_size 可以修改。Graylog 还提供了 API 查询状态 可以和负载均衡系统集成。

Graylog 创建 syslog input

首先要在 Graylog 中通过 System / Inputs 创建 input 来接收日志。可以选 Syslog UDP 或者 Syslog TCP,俩者区别不大,TCP 的话 rsyslog 是能保证日志不会丢失,UDP 的话开销相对更小,但是可能丢数据。TCP 的话负载均衡选择也更多,如果不知道用哪个就选 TCP。

大部分不需要改,Global 的话会在所有 Graylog 节点上启动,Bind address 使用默认的 0.0.0.0 这样其它机器才能转发日志过来。Port 需要配置一个大于 1024 的端口。

如果启用了集群,还需要使用负载均衡将 rsyslog 转发的日志分发给 Graylog 节点。如果用 TCP,可以用 HAProxy,很多云服务也提供了现成甚至免费的内网 TCP 负载均衡产品。如果用 UDP,可以参考我之前写的 使用 Nginx 作 UDP 负载均衡

日志发往 rsyslog

Syslog 是一套记录日志的协议,rsyslog 是具体的一个实现。在 Debian 和 Ubuntu 中预装的就是 rsyslog。Rsyslog 用很多扩展功能,不如不限制单条日志大小,可以转发日志。

日志不是直接发往 Graylog 的 syslog input,而是发往本地的 rsyslog,然后 rsyslog 负责转发给 Graylog。这样网络或者 Graylog 出现问题,Rsyslog 可以缓存未发送的日志,等待问题恢复。同时还可以在本地保存一份副本。

将日志发往 syslog 可以使用系统提供的 C 库中的 syslog (see man 3 syslog)。很多语言标准库都集成了 syslog,比如 RubyPython, Go。没有的也可以通过 C 集成,比如用于 skynet

注意几个比较重要的可配置参数,能方便之后日志的过滤和转发。可以在 man 3 syslog 看到

void openlog(const char *ident, int logopt, int facility);

其中 indent 在不同的地方可能又被称为 programname, tag。一般用这个来区分日志是哪个服务或者进程产生的。另一个是 facility,推荐统一使用 LOCAL6,在 rsyslog 只转发 LOCAL6 的日志到 Graylog。

Facility 的配置都比较直接,不过 indent 的配置就比较混乱了,像 Python 的 SysLogHandler 就一直不支持自定义 ident,Python 3 直到 3.3 版本中才可以通过 class-level attribute 来配置。所以分别说明下

  • Ruby: Syslog::Logger.new(program_name = 'ruby', facility = nil)program_name 就是 ident
  • Python: Python 不管哪个版本 SysLogHandler 默认都不传 ident 的,而 ident 在 syslog 标准中其实就是日志中以非特殊字符开头,: 结尾的一串字符,所以可以在设置 formatter 的时候添加,见下面的代码。
import logging
from logging.handlers import SysLogHandler

ident = 'myprogname'
h = SysLogHandler(address='/dev/log')
h.setFormatter(logging.Formatter(ident + ': %(message)s'))
logging.getLogger().addHandler(h)

logging.error('hi')
  • Go: log/syslog 的 Dial(network, raddr string, priority Priority, tag string) (*Writer, error) 中的 tag 就是 ident。

Rsyslog 配置

以 Ubuntu 14.04 为例。先上配置,保存为 /etc/rsyslog.d/00-graylog.conf,具体文件名不重要,但必须 00 开头,.conf 为后缀。

$template GLFile,"/data/log/%programname%.log"
$FileOwner syslog
$FileGroup syslog
$CreateDirs on
$DirCreateMode 0755
$FileCreateMode 0640
$RepeatedMsgReduction off
local6.* ?GLFile;RSYSLOG_SyslogProtocol23Format
local6.* @@logs-internal.example.com:1514;RSYSLOG_SyslogProtocol23Format
& stop

$ 开头的都是 rsyslog 的配置的 Directive。如果对日志可靠性要求高,可以参考Rsyslog Reliable Forwarding 的文档。

配置中 $RepeatedMsgReduction 是不删除重复的日志,其它全部都是配置本地文件副本的。$template GLFile 配置了文件名命名模版。模版可以使用一些变量,比如 %programname% 是日志的 ident,如果之前正确设置了 ident,这里可以把不同的服务的日志存到不同的文件中。模版是完整的路径,注意权限,用户 syslog 需要有创建文件的权限。因为 rsyslog 没有 root 权限,FileOwner$FileGroup 可选值不多,如果想让其它用户查看文件,可以把 $FileGroup 设置成 syslog,然后添加用户到 syslog 组。例子:

sudo mkdir -p /data/log
sudo chown syslog:adm /data/log
# 允许 deploy 查看本地日志副本
sudo gpasswd -a deploy syslog

倒数第三行 local6.* ?GLFile;RSYSLOG_SyslogProtocol23Format 是把 facility 是 LOCAL6 的日志按照模版 GLFile 保存到文件中。如果不想要本地副本,删掉本行即可。

倒数第二行俩个 @@@logs-internal.example.com:1514;RSYSLOG_SyslogProtocol23Format 表示通过 TCP 转发日志到 logs-internal.example.com 的1514端口。如果是 UDP 改成一个 @。如果用了负载均衡这里应该填负载均衡的地址和端口。

最后一行的 & stop 表示匹配之前规则的日志,也就是所有 LOCAL6 的日志不再判断配置中剩余的规则。这也是配置必须用 00 开头的原因。默认的 Ubuntu 配置中,LOCAL6 的日志也会保存到 /var/log/syslog 中,重复浪费空间,而且日志量很大的话,/var/log/syslog 默认的 logrotate 的配置会导致占用大量的磁盘。

配置里选择把日志保存在 /data/log 目录下,主要是为了单独配置 logrotate。访问量大的服务会产生海量日志,所以需要优化避免写爆磁盘。

这是一个每天不压缩大概会产生 100G 左右日志的推荐配置,保存为 /etc/logrotate.d/syslog-graylog

/data/log/*.log
{
rotate 40
daily
maxage 5
maxsize 10G
missingok
notifempty
compress
postrotate
reload rsyslog >/dev/null 2>&1 || true
endscript
}
  • daily 每天 rotate 一次,maxsize 10G 超过 10G 立即 rotate。
  • rotate 40maxage 5 保留最多 40 份,最多 5 天内的历史日志。过期的删除。
  • compress 用 gzip 压缩历史日志,不配置 delaycompress

这样大概保存最近 5 天的日志,如果日志量太大,就保存最近 40 x 10 共 400G 日志。天数根据需要配置,因为是副本,不用设置太长时间。rotate 40maxsize 10G 根据磁盘大小确定,GZIP 压缩比大概是 1/10,40 x 10 的配置大概占用 40G 磁盘。

Logrotate 默认是每天运行的,maxsize 10G 其实不起作用,可以配置 cron 每小时手动执行下,这样能及时 rotate 日志并压缩。把下面文件保存为 /etc/cron.hourly/syslog-graylog

#!/bin/sh

logrotate /etc/logrotate.d/syslog-graylog

注意权限

chmod +x /etc/cron.hourly/syslog-graylog

测试

测试可以使用命令行工具 logger-p 指定 facility 和 priority,-t 指定 ident

logger -p local6.error -t programname test

一切正常的话 Graylog 中就会显示这条日志了。

另外说下 macOS,虽说提供了 syslog 接口,不过坑很多,配置可以在 /etc/syslog.conf 里添加,比如

local6.* @127.0.0.1:1514

然后重启下服务

sudo launchctl unload /System/Library/LaunchDaemons/com.apple.syslogd.plist
sudo launchctl load /System/Library/LaunchDaemons/com.apple.syslogd.plist

不过 macOS 的 syslog 还受 /etc/asl.conf 影响,比如 notice 级别以下的日志是全部忽略,根本就不会处理到 /etc/syslog.conf 的。优点是可以在 Console 里查看 syslog。

处理

Graylog 通过 Input 搜集日志,每个 Input 单独配置 Extractors 用来做字段转换。

Graylog 中日志搜索的基本单位是 Stream,每个 Stream 可以有自己单独的 Elastic Index Set,也可以共享一个 Index Set。用 Set 是因为日志的保存会使用一个前缀然后滚动创建新的 Index。Stream 通过配置条件匹配日志,满足条件的日志添加 stream ID 标识字段并保存到对应的 Elastic Index Set 中。同一个 Input 中的日志可以属于不同的 Stream,不同 Input 中的日志可以属于同一个 Stream,就是同一条日志也可以属于多个 Stream。

系统会有一个默认的 Stream,所有日志默认都会保存到这个 Stream 中,除非匹配了某个 Stream,并且这个 Stream 里配置了不保存日志到默认 Stream。

下图是日志处理流程图

后文会分别分享各个步骤需要注意的一些地方。

Extractor

Extractor 在 System / Input 中配置。Graylog 中很方便的一点就是可以加载一条日志,然后基于这个实际的例子进行配置并能直接看到结果。

内置的 Extractor 基本可以完成各种字段提取和转换的任务,但是也有些限制。在应用里写日志的时候就需要考虑到这些限制。

  • 只有字符串类型的字段可以应用 Extractor。比如不能将 Linux Epoch 时间戳通过 Extractor 转成 Elastic 支持的时间格式。
  • Extractor 的输入只能是单个字段,输出根据用的方法的不同是可以生成多个字段,比如 JSON 可以解析 JSON 并提取所有字段。
  • 字段层级只有一级,也就是日志字段的值不能是复合类型,只能是字符串,数字,时间等。比如使用 JSON Extractor,如果 JSON 中有嵌套的 Object 和 Array 需要选择一种方式展平结构。

Input 可以配置多个 Extractors,按照顺序依次执行。

Stream

Gralog 安装好后会包含一个默认 Stream,可以通过菜单 Streams 创建更多的 Stream。新创建的 Stream 是暂停状态,需要在配置完成后手动启动。

一个 Stream 唯一属于一个 Index Set,但是多个 Streams 可以共享同一个 Index Set。如果共享 Index,那么因为底层 Elastic 的原因会有一个限制: 同一个字段的类型不能一会是字符串,一会是数字,也就是类型必须一致。这个的影响有:

  • 在写入日志的时候,如果当前 Elastic Index 中已经存在该字段同时类型不符合,那么冲突的日志会被丢弃,这个错误可以在 System / Overview 中查看。
  • 因为上面一条的原因,一个 Index 中某个字段是什么类型取决于该 Index 中第一条含该字段的日志。这样一个 Index Set 中,某个字段的类型可能会不一致,在跨 Index 做数据汇总时会导致出错,比如选择了很长的一个时间跨度。

如果 Graylog 是多个项目共享的话,是很难避免不同项目间字段类型冲突的,所以建议是不相关的日志不要共享 Index Set。这里推荐个人使用的一个策略:

  • 为不同的项目创建单独的 Stream 和 Index Set,并且选择不保存到默认 Stream 中。
  • 如果需要将某个项目中的一部分日志发往其它服务,比如把错误发到 Sentry,单独创建 Stream 加过滤条件,和该项目的 Stream 共享 Index Set。这个会在下一篇系列文章中提到。
  • 如果项目中某些日志很重要,需要有不同的存储策略,或者是需要保存更长时间,那么单独创建 Stream 并使用单独的 Index Set

这样各个项目间不会产生冲突,又能单独配置存储。

Index Set

Index Set 通过菜单 System / Indices 创建。日志存储的性能,可靠性和过期策略都通过 Index Set 来配置。

性能和可靠性就是配置 Elastic Index 的一些参数,主要是

  • Shards: 每个 Index 分多少片,每一片可以保存在 Elastic 集群中不同的机器上。日志存储和查询的瓶颈一般是磁盘 IO,通过分片可以将 IO 压力分摊到多台机器。
  • Replicas: 每个 Shard 额外保存多少个副本,当有机器出现故障,只要集群内能凑齐每个 Shard 中至少一个副本就不会有任何影响。当然可靠性是靠存储的冗余来实现的,需要消耗更多的磁盘空间。

已经有很多 Elastic 的文章介绍如何进行配置了,就不详细说明。如果集群比较小,不超过3台机器,那么 Shards 可以填 3,而 Replicas 的配置

  • 如果没有配置 Elastic 集群填 0
  • 如果可以接受集群中节点故障导致部分日志暂时无法搜索到,甚至是永久丢失,或者磁盘比较紧张,填 0
  • 如果对可靠性要求高,也有充足的磁盘空间,填 1

过期策略主要根据日志量,磁盘空间,需要查询的时间跨度来决定,Graylog 提供了三种 Index 滚动方案:

  • 按时间
  • 按 Index 中日志数量
  • 按 Index 的占用磁盘大小

通过配置要保留的 Index 数量来删除老的日志。

Pipelines

除了上面提到的日志处理流程,Graylog 还提供了 Pipeline 脚本实现更灵活的日志处理方案。这里不详细阐述,只介绍如果使用 Pipelines 来过滤不需要的日志。

Graylog 中只要日志发到了 Input,常规流程中是没有办法丢弃日志,最终一定会写入到 Elastic 中。有时候可能一些配置错误,比如打开了 DEBUG 级别的日志,导致大量没用的日志占用大量资源。虽然可以单独创建单独的 Stream 和 Index Set 并通过配置过期策略来快速丢弃日志,但日志还是在磁盘上走了一遍。这时就需要 Pipelines 出场了。

下面是丢弃 level > 6 的所有日志的 Pipeline Rule 的例子

rule "discard debug messages"
when
to_long($message.level) > 6
then
drop_message();
end

然后可以创建 Pipeline 关联 Streams 和规则了。

要注意的是,如果 Pipeline Rule 想使用 Extractors 应用之后的字段的话,需要在 System / Configuration 里调整 Message Processors Configuration 的顺序,Pipeline Processor 要放在 Message Filter Chain 后面。

输出

日志集中保存到 Graylog 后就可以方便的使用搜索了。不过有时候还是需要对数据进行近一步的处理。主要有两个途径:

  • 直接访问 Elastic 中保存的数据
  • 通过 Graylog 的 Output 转发到其它服务

访问 Elastic

Graylog 支持一些简单的统计,如果想做更复杂的统计,推荐使用 Grafana

集成很简单,在 Grafana 添加下 Elasticsearch 数据源。Access 推荐 proxy,这样不用在公网暴露 Elastic 的端口,只要 Grafana 所在机器能通过 Url 访问就可以了。

Index name 根据 Graylog Index Set 中设置的前缀配置。Version 根据安装的 Elastic 版本选择。

然后创建图标选择 Elastic 的 Data Source 就可以了。

如果还有更复杂的需求,可以使用 Elastic 的各种语言的库,比如 Jupter Notebook 搭配 Python。

Graylog Output

在 Graylog 中,可以选择将某个 Stream 通过 Output 转发给其他服务。内置了 GELF Output 使用 GELF 格式通过 TCP 或者 UDP 发到其它服务。在 Graylog Market 中可以搜索到很多现成的 Output Plugin,Github 上也有很多现成例子可以仿照实现自己的 Output Plugin。不过 Plugin 需要用 Java 实现,然后要部署到 Graylog 的所有节点,修改调试都很不方便。如果 Market 中找不到现成的 Plugin,更推荐使用内置的 GELF Output,很多 GELF 库是支持作为服务接收 GELF 消息的,比如 Golang 的 go-gelf,使用 gelf.NewReader 就可以创建一个 UDP 服务。

package main

import (
"log"

"gopkg.in/Graylog2/go-gelf.v2/gelf"
)

func handleMessage(m *gelf.Message) {
// TODO: handle m
}

func runUDPServer() {
gelfReader, err := gelf.NewReader(":12201")
if err != nil {
log.Fatal(err)
}
for {
message, err := gelfReader.ReadMessage()
if err != nil {
log.Error(err)
}
go func() {
defer func() {
if r := recover(); r != nil {
log.Error(r)
}
}()
handleMessage(message)
}()
}
}

func main() {
runUDPServer()
}

--

--