如果你的应用运行在分布式架构上,你很可能会使用集中式日志系统来收集它们的日志,其中我们使用比较广泛的一个工具就是fluentd,包括在容器化时代用来收集 Kubernetes 集群应用日志, fluentd也是使用非常多的。
本文我们将解释它是如何工作的,以及如何根据需求来调整 fluentd 配置。
基本概念
输入
tail -f myapp.log
我们要对一个文件进行长期的 tail,每当有什么日志信息被添加到文件中,它就会显示在屏幕上。这在 fluentd 中叫做输入插件,tail 只是其中之一,但还有很多其他可用的插件。
| grep "what I want"
在这里,我们从尾部 Ω-f
的输出中,只过滤包含我们想要的字符串的日志行,在 fluentd 中这叫做过滤插件。
> example.log
在这里,我们将 grep 命令过滤后的输出保存到一个名为 example.log 的文件中。在 fluentd 中,这就是输出插件,除了写到文件之外,fluentd 还有很多插件可以把你的日志输出到其他地方。
这就是 fluentd 的最基本的运行流程,你可以读取日志,然后处理,然后把它发送到另一个地方做进一步的分析。接下来让我们用一个小 demo 来实践这些概念,看看这3个插件是如何在一起工作的。
在这个demo 中,我们将使用 fluentd 来读取 docker 应用日志。
fluentd/
├── etc/
│ └── fluentd.conf
├── log/
│ └── kong.log
└── output/
其中的 output/
是 fluentd 写入日志文件的目录,在 log/kong.log 中,有一些来自本地运行的 kong 容器的日志,它们都是 docker 格式的日志。
{
"log":"2019/07/31 22:19:52 [notice] 1#0: start worker process 32\n",
"stream":"stderr",
"time":"2019-07-31T22:19:52.3754634Z"
}
这个文件的每一行都是一个 json 文件,这就是 docker 默认驱动的日志格式。我们将对这个文件进行 tail 和解析操作,它有应用日志和访问日志混合在一起。我们的目标是只获取访问日志。etc/fluentd.conf 是我们的 fluentd 配置,其中有一个输入和一个输出部分,我们稍后会仔细来分析,首先运行 fluentd 容器。
$ chmod 777 output/
$ docker run -ti --rm \
-v $(pwd)/etc:/fluentd/etc \
-v $(pwd)/log:/var/log/ \
-v $(pwd)/output:/output \
fluent/fluentd:v1.11-debian-1 -c /fluentd/etc/fluentd-simplified-finished.conf -v
注意上面的运行命令和我们要挂载的卷
etc/ 是挂载在容器内部的 /fluentd/etc/ 目录下的,以覆盖 fluentd 的默认配置。
log/ 挂载到 /var/log/,最后在容器里面挂载到 /var/log/kong.log。
output/ 挂载到 /output,以便能够看到 fluentd 写入磁盘的内容。
运行容器后,会出现如下所示的错误信息:
2020-10-16 03:35:28 +0000 [info]: #0 fluent/log.rb:327:info: fluentd worker is now running worker=0
Fluentd 配置
input
部分<source>
@type tail
path "/var/log/*.log"
tag "ninja.*"
read_from_head true
<parse>
@type "json"
time_format "%Y-%m-%dT%H:%M:%S.%NZ"
time_type string
</parse>
</source>
我们来仔细查看下这几个配置:
然后是输出 output 部分的配置。
# Output
<match **>
@type file
path /output/example.log
<buffer>
timekey 1d
timekey_use_utc true
timekey_wait 1m
</buffer>
</match>
在这个配置中,有两个重要的部分。
**<match>:这表示我们要匹配 fluentd 中的所有标签,我们这里只有一个,就是上面输入插件创建的那个。
path /output/example:这是保存缓冲区的目录名,也是每个日志文件的开头名称。
output
├── example
│ ├── buffer.b5b1c174b5e82c806c7027bbe4c3e20fd.log
│ └── buffer.b5b1c174b5e82c806c7027bbe4c3e20fd.log.meta
├── example.20190731.log
└── example.20200510.log
有了这个配置,我们就有了一个非常简单的输入/输出管道了。
现在我们可以来看看 fluentd 创建的一个文件中的一些日志 example.20200510.log
。
2020-05-10T17:04:17+00:00 ninja.var.log.kong.log {"log":"2020/05/10 17:04:16 [warn] 35#0: *4 [lua] globalpatches.lua:47: sleep(): executing a blocking 'sleep' (0.004 seconds), context: init_worker_by_lua*\n","stream":"stderr"}
2020-05-10T17:04:17+00:00 ninja.var.log.kong.log {"log":"2020/05/10 17:04:16 [warn] 33#0: *2 [lua] globalpatches.lua:47: sleep(): executing a blocking 'sleep' (0.008 seconds), context: init_worker_by_lua*\n","stream":"stderr"}
2020-05-10T17:04:17+00:00 ninja.var.log.kong.log {"log":"2020/05/10 17:04:17 [warn] 32#0: *1 [lua] mesh.lua:86: init(): no cluster_ca in declarative configuration: cannot use node in mesh mode, context: init_worker_by_lua*\n","stream":"stderr"}
2020-05-10T17:04:30+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:04:30 +0000] \"GET / HTTP/1.1\" 404 48 \"-\" \"curl/7.59.0\"\n","stream":"stdout"}
2020-05-10T17:05:38+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:05:38 +0000] \"GET /users HTTP/1.1\" 401 26 \"-\" \"curl/7.59.0\"\n","stream":"stdout"}
2020-05-10T17:06:24+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:06:24 +0000] \"GET /users HTTP/1.1\" 499 0 \"-\" \"curl/7.59.0\"\n","stream":"stdout"}
注意上面的日志,每行都有3列,格式为:
<time of the log> <tag of the log> <the content of the log>
注意:标签都是 “ninja” 字符串加上目录路径和文件名,之间使用”. “分隔。
现在我们已经在 fluentd 中实现了日志的收集,接下来让我们对它进行一些过滤操作。
到目前为止,我们已经实现了前面那条命令的2个部分,tail -f /var/log/*.log 和 > example.log 工作正常,但是如果你看一下输出,我们有访问日志和应用日志混合在一起,现在我们需要实现 grep ‘what I want’ 过滤。
在这个例子中,我们只想要访问日志,丢弃其他的日志行。比如说,通过 HTTP 进行 grepping 会给我们提供所有的访问日志,并将应用日志排除在外,下面的配置就可以做到这一点。
<filter ninja.var.log.kong**>
@type grep
<regexp>
key log
pattern /HTTP/
</regexp>
</filter>
我们来分析下这个过滤配置:
现在我们停止并再次运行容器。我们应该在输出日志中看到一些不同的日志了,没有应用日志,只有访问日志数据。
2020-05-10T17:04:30+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:04:30 +0000] \"GET / HTTP/1.1\" 404 48 \"-\" \"curl/7.59.0\"\n","stream":"stdout"}
2020-05-10T17:05:38+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:05:38 +0000] \"GET /users HTTP/1.1\" 401 26 \"-\" \"curl/7.59.0\"\n","stream":"stdout"}
2020-05-10T17:06:24+00:00 ninja.var.log.kong.log {"log":"172.17.0.1 - - [10/May/2020:17:06:24 +0000] \"GET /users HTTP/1.1\" 499 0 \"-\" \"curl/7.59.0\"\n","strea
为了熟悉我们的配置,下面让我们添加一个解析器插件来从访问日志中提取一些其他有用的信息。在 grep 过滤器后使用下面配置。
<filter ninja.var.log.kong** >
@type parser
key_name log
<parse>
@type nginx
</parse>
</filter>
同样我们来仔细查看下这个配置:
@type parser:过滤器的类型是 parser 解析器。
我们将对日志内容的 log key 进行解析。
由于这些都是 nginx 的访问日志,我们将使用 @type nginx 的解析器。
现在我们的管道是下面这个样子了。
我们再次重新运行容器,现在的访问日志应该是这样的了。
2020-05-10T17:04:30+00:00 ninja.var.log.kong.log {"remote":"172.17.0.1","host":"-","user":"-","method":"GET","path":"/","code":"404","size":"48","referer":"-","agent":"curl/7.59.0","http_x_forwarded_for":""}
这是之前日志中的第一个访问日志,现在日志内容完全不同了,我们的键从日志流,变成了 remote、host、user、method、path、code、size、referer、agent 以及 http_x_forwarded_for。如果我们要把这个保存到 Elasticsearch 中,我们将能够通过 method=GET 或其他组合进行过滤了。
当然我们还可以更进一步,在 remote 字段中使用 geoip 插件来提取我们我们 API 的客户端的地理位置信息,大家可以自行测试,不过需要注意的时候需要我们的镜像中包含这些插件。
总结
现在我们知道了如何用 docker 来让 fluentd 读取配置文件,我们了解了一些常用的 fluentd 配置,能够从一些日志数据中来过滤、解析提取有用的信息。
原文来自「高效运维」|侵删