Beats: 使用 Filebeat 进行日志结构化

结构化日志背后的想法很简单:让应用程序直接编写 JSON 对象,而不是让应用程序将需要通过正则表达式解析的日志写入到你索引到 Elasticsearch 的 JSON 对象中。

举例来说,假设你正在编写 Python Web 应用程序,并且正在使用标准库进行记录。 用户登录后,你可能会使用如下所示的日志记录语句:

createlogs.py

import logging

user = {
  "name": "liuxg",
  "id": "1"
}

session_id = "91e5b9d"

logging.basicConfig( filename="test.log", level=logging.DEBUG )

logging.debug( "User '{}' (id: {}) successfully logged in. Session id: {}"
               .format(user["name"], user["id"], session_id) )

logging.debug("User '{}' (id: {}) changed state to verified."
              .format(user["name"], user["id"]))

上面的 python 应用将生成如下的日志信息:

DEBUG:root:User 'liuxg' (id: 1) successfully logged in. Session id: 91e5b9d
DEBUG:root:User 'liuxg' (id: 1) changed state to verified.

自从 printf 以来,这种日志记录方法就很流行。那时,你通常只有一台服务器,并且会拖尾并 grep 日志文件,一切都很好。

但是,时代已经变了,如今,你很有可能拥有数十,数百或数千个服务器/虚拟机/容器来创建大量日志,因此你将它们集中在  Elasticsearch 中并使用其神奇的功能(这就是它的感受)搜索和汇总功能以在它们之间进行导航。

只有把上面的数据进行结构化处理,才能在 Elasticsearch 中发挥更好的效效果,因此你可以在各个字段上进行搜索和汇总。因此,在建立索引之前,你通常会使用 Logstash 出色的 Grok 过滤器将应用程序日志解析为JSON对象,但这意味着您必须编写和维护Grok 模式并花费 CPU 周期来进行解析。当然你也可以使用 Filebeat 结合 pipeline processors 来完成这项工作。对于还不是很熟悉的开发者来说,请参阅我之前的文字:

现在,让我们尝试使用结构化日志记录的相同示例。尽管通常不在标准库中,但是所有主要的编程语言都具有使结构化日志记录变得容易的库。 James Turnbull在他的博客文章中创建了一个列表,其中还详细介绍了如何为Rails应用程序执行此操作。在Python中,有一个 structlog 库,我们将在这里使用它。

我们可以使用如下的方法来进行安装:

pip2 install structlog

如果你想在开发环境中看到彩色的输出,那么你可以使用如下的方法进行安装:

pip2 install structlog colorama

我们使用同样分方法,来进行生产日志:

createlogs_1.py

import logging
import structlog

user = {
  "name": "liuxg",
  "id": "1"
}

session_id = "91e5b9d"

log = structlog.get_logger()

log = log.bind(user='arthur', id=42, verified=False)
log.msg('logged_in')
log.msg('changed_state', verified=True)

上面的应用将生成如下的日志:

2020-06-11 14:50.41 logged_in                      id=42 user=arthur verified=False
2020-06-11 14:50.41 changed_state                  id=42 user=arthur verified=True

需要注意的一件事是,代码的重复性较低,它鼓励开发人员包括所有数据,而不是仅在编写代码时才包含重要的数据。 还要注意,以这种格式,对于开发人员来说,日志行仍然相当容易遵循。 但是,在投入生产时,使用 JSON 渲染器会更有意义:

createlogs_2.py

import logging
import structlog
from structlog import wrap_logger, PrintLogger, wrap_logger
from structlog.processors import JSONRenderer

file = open('json_logs', 'w')
log = wrap_logger(PrintLogger(file), processors=[JSONRenderer()])

log = log.bind(user_name='arthur', id=42, verified=False)
log.msg('logged_in')
log.msg('changed_state', verified=True)

file.close

上面的应用运行后,生产一个叫做 json_logs 的文件:

{"user_name": "arthur", "id": 42, "verified": false, "event": "logged_in"}
{"user_name": "arthur", "id": 42, "verified": true, "event": "changed_state"}

这种 JSON 格式的文件人眼难以理解,但是具有的优点是,数据已经按照 Elasticsearch 喜欢的格式进行了结构化。

Filebeat 是一个用Go语言编写的开源日志传送器,可以将日志行发送到Logstash和Elasticsearch。 它提供了“至少一次”保证的数据传输,因此你永远不会丢失日志行,并且它使用了背压敏感协议,因此不会使你的管道过载。 还包括基本过滤和多行关联。

如果你的日志就像上面的示例一样,Filebeat 每行存储一个JSON对象,它还可以本地解码 JSON 对象。

这是一个示例配置文件,该文件配置Filebeat来拾取文件并将 JSON 对象发送到 Elasticsearch:

filebeat_json.yml
 

filebeat.inputs:
- type: log
  enabled: true
  tags: ["i", "love", "json"]
  json.message_key: event
  json.keys_under_root: true
  json.add_error_key: true
  fields:
    planet: liuxg
  paths:
    - /Users/liuxg/python/logs/json_logs

output.elasticsearch:
  hosts: ["localhost:9200"]
  index: "json_logs1"

setup.ilm.enabled: false
setup.template.name: json_logs1
setup.template.pattern: json_logs1

在运行 Filebeat 之前,我们可以在 Kibana 中执行如下的命令:

PUT json_logs1
{
  "mappings": {
    "properties": {
      "event": {
        "type": "keyword"
      },
      "id": {
        "type": "long"
      },
      "user_name": {
        "type": "keyword"
      },
      "verified": {
        "type": "boolean"
      }
    }
  }
}  

在这里,我们定义这个索引的 mapping。

我们接着执行运行 Filebeat:

./filebeat -e -c ~/python/logs/filebeat_json.yml

那么在我们的 Kibana 中,我们可以查询到最新生成的文档:

GET json_logs1/_search
{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 2,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "json_logs1",
        "_type" : "_doc",
        "_id" : "uw-jonIBB4HethT_mOIZ",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-06-11T09:08:48.550Z",
          "user_name" : "arthur",
          "verified" : false,
          "tags" : [
            "i",
            "love",
            "json"
          ],
          "fields" : {
            "planet" : "liuxg"
          },
          "ecs" : {
            "version" : "1.5.0"
          },
          "agent" : {
            "ephemeral_id" : "0c9b96dd-76c8-45c5-96ef-00859f9e12dc",
            "hostname" : "liuxg",
            "id" : "be15712c-94be-41f4-9974-0b049dc95750",
            "version" : "7.7.0",
            "type" : "filebeat"
          },
          "id" : 42,
          "event" : "logged_in",
          "log" : {
            "offset" : 0,
            "file" : {
              "path" : "/Users/liuxg/python/logs/json_logs"
            }
          },
          "input" : {
            "type" : "log"
          },
          "host" : {
            "name" : "liuxg"
          }
        }
      },
      {
        "_index" : "json_logs1",
        "_type" : "_doc",
        "_id" : "vA-jonIBB4HethT_mOIZ",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-06-11T09:08:48.551Z",
          "event" : "changed_state",
          "input" : {
            "type" : "log"
          },
          "log" : {
            "offset" : 75,
            "file" : {
              "path" : "/Users/liuxg/python/logs/json_logs"
            }
          },
          "user_name" : "arthur",
          "id" : 42,
          "verified" : true,
          "tags" : [
            "i",
            "love",
            "json"
          ],
          "fields" : {
            "planet" : "liuxg"
          },
          "ecs" : {
            "version" : "1.5.0"
          },
          "host" : {
            "name" : "liuxg"
          },
          "agent" : {
            "version" : "7.7.0",
            "type" : "filebeat",
            "ephemeral_id" : "0c9b96dd-76c8-45c5-96ef-00859f9e12dc",
            "hostname" : "liuxg",
            "id" : "be15712c-94be-41f4-9974-0b049dc95750"
          }
        }
      }
    ]
  }
}

如你所见,Filebeat 自动添加一个时间戳。 请注意,这是读取日志行的时间,可能与应用程序写入日志行的时间不同。 如果需要更好的准确性,可以设置 structlog 库以生成时间戳。

Filebeat 还会自动添加一些元数据(例如主机名),并使其易于通过配置文件添加自定义字段和标签。 这意味着应用程序不必担心从环境中添加元数据。

这就是你所需要的。 简单的事情应该很简单:-)