Beats:使用 Elastic Stack 记录 Golang 应用日志

当今可用的丰富编程语言为程序员提供了用于构建应用程序的大量工具。无论是像 Java 这样的老牌巨头,还是像 Go 这样的新兴公语言,应用程序都需要在部署后进行监视。在本文中,你将学习如何将Golang日志发送到ELK Stack和Logz.io。

通常可以通过查看其日志来了解应用程序的运行状况。但是,日志数据具有随时间呈指数增长的趋势。当更多应用程序部署并分布在多台服务器上时,尤其如此。 Elastic Stack 具有存储大量数据并快速,轻松地进行搜索的功能,在这里很方便。

在本文中,你将学习如何导入 Go 应用程序编写的日志。 Go 编程语言(也称为 Golang 或 GoLang)是一种相对较新的但成熟的通用编程语言,在编程社区和主要的云提供商中得到了广泛的采用。

在之前的文章 “使用 Filebeat 进行日志结构化”,我已经介绍了如果直接使用 Filebeat 把日志进行结构化。在那篇文章中,我们使用了 python 应用作为一个例子。在今天的例子中,我将以一个 Golang 的例子来进行展示。

 

GoLang 日志概述

你可以使用几种不同的选项从 Go 程序把日志写到文件。 其中之一是 Logrus 库,它非常易于使用,并具有编写信息丰富的日志并能够轻松将其发送到 Elasticsearch 所需的所有功能。

首先,通过在终端中运行以下命令来获取 logrus 软件包。这个包由于在谷歌的网站上,直接使用如下的语句将不能正确下载:

go get github.com/sirupsen/logrus

我们可以参阅文章通过设置代理的办法来进行安装:

# 启用 Go Modules 功能
export GO111MODULE=on
# 配置 GOPROXY 环境变量
export GOPROXY=https://goproxy.io

在 terminal 中打入上面的命令后,再打入如下的命令:

go get github.com/sirupsen/logrus

我们的 Go 程序非常之简单:

main.go

package main

import (
  "os"
  log "github.com/sirupsen/logrus"
)

func main() {

  log.SetFormatter(&log.JSONFormatter{
    FieldMap: log.FieldMap{
      log.FieldKeyTime:  "@timestamp",
      log.FieldKeyMsg:   "message",
    },
  })
  log.SetLevel(log.TraceLevel)

  file, err := os.OpenFile("out.log", os.O_RDWR | os.O_CREATE | os.O_APPEND, 0666)
  if err == nil {
    log.SetOutput(file)
  }
  defer file.Close()

  fields := log.Fields{"userId": 12}
  log.WithFields(fields).Info("User logged in!")

  fields = log.Fields{"userId": 12}
  log.WithFields(fields).Info("Sent a message!")

  fields = log.Fields{"userId": 12}
  log.WithFields(fields).Info("Failed to get a message!")

  fields = log.Fields{"userId": 12}
  log.WithFields(fields).Info("User logged out!")
}

上面是一个很简单的 Go 语言应用程序。此代码片段将打开一个用于写入的文件,并将其设置为 logrus记 录器的目标。 现在,当你调用log.Info(...)时,你记录的信息将被写入该文件。 但是,你也可以(可选)使用其他相关信息(例如用户标识符)来丰富日志数据,这些信息可以通过提供其他上下文来帮助解决问题。它将生成几个日志,并保存到一个叫做 out.log 的文件中。

我们可以使用如下的命令来进行运行:

go run main.go

我们可以查看当前目录下的文件:

$ pwd
/Users/liuxg/go/es_logs
$ go run main.go
liuxg:es_logs liuxg$ ls
main.go out.log

在上面,我们可以看到一个叫做 out.log 的文件。它的内容如下:

$ cat out.log 
{"@timestamp":"2020-10-16T14:37:44+08:00","level":"info","message":"User logged in!","userId":12}
{"@timestamp":"2020-10-16T14:37:44+08:00","level":"info","message":"Sent a message!","userId":12}
{"@timestamp":"2020-10-16T14:37:44+08:00","level":"info","message":"Failed to get a message!","userId":12}
{"@timestamp":"2020-10-16T14:37:44+08:00","level":"info","message":"User logged out!","userId":12}

从上面,我们可以看出来,这是一个非常结构化的日志。它具有 JSON 结构,因为在程序开始时设置了 JSON 格式化程序。 当该选项可用时,以 JSON 格式格式化日志可以更轻松地将它们发送到 Elasticsearch,而无需其他配置,因为 JSON 属性和值直接映射到Elasticsearch 中的字段。 相反,你必须告诉 Elasticsearch 如何从没有明显结构的文本日志中解析数据。

如此轻松地将日志写入 JSON 格式的文件中(并根据需要包含其他字段的可能性),使你处于将日志发送到 Elasticsearch 的良好位置。

Logrus

与几乎所有其他日志记录库一样,Logrus 允许您使用许多不同的严重性级别(包括信息,警告,错误等)来编写日志。 这是通过调用适当的函数(例如Info())来完成的。 也可以配置最低级别。

例如,当你调用 log.SetLevel(log.TraceLevel) 时,将只写入级别为 trace 或更高级别的日志。由于 Trace 是最低级别,因此此调用指示你要写入所有日志,而不管它们的级别如何。 例如,可以将其更改为 log.InfoLevel 以忽略具有跟踪或调试级别的日志。

 

将 GoLang 日志运送到 ELK

将日志写入文件有很多好处。 该过程既快速又健壮,并且应用程序无需了解最终将最终用于日志的存储类型的任何信息。 Elasticsearch 提供了 Beats,可以帮助你从各种来源(包括文件)收集数据并将其可靠而有效地运送到 Elasticsearch。 一旦日志数据进 Elasticsearch,你就可以使用 Kibana 对其进行分析。

发送到 Elasticsearch 的日志数据需要解析,以便 Elasticsearch 可以正确构造它。 Elasticsearch 能够轻松处理 JSON 数据。 你可以为其他格式设置更复杂的解析。

我们在 filebeat 的安装目录下创建如下的文件:

filebeat_json.yml

filebeat.inputs:
- type: log
  enabled: true
  paths:
    - /Users/liuxg/go/es_logs/out.log
  json:
    keys_under_root: true
    overwrite_keys: true
    message_key: 'message'

processors:
  - decode_json_fields:
      fields: ['message']
      target: json
 
setup.template.enabled: false
setup.ilm.enabled: false
 
output.elasticsearch:
  hosts: ["localhost:9200"]
  index: "logs_json"
  bulk_max_size: 1000

请注意:你必须用你自己的路径替换上面的 paths。

我们使用如下的命令来把数据导入:

$ ls filebeat_json.yml 
filebeat_json.yml

$ ./filebeat -e -c ./filebeat_json.yml

在运行完上面的命令后,我们使用如下的命令来检查已经生产的索引 logs_json:

GET _cat/indices

上面的命令显示:

green  open .apm-custom-link               CO_6a4_ISAGSswWYjZrNjQ 1 0  0    0    208b    208b
yellow open logs_json                      Is-RVM33T920Ffd35I0ZUw 1 1  4    0   5.6kb   5.6kb
green  open .kibana_task_manager_1         PACoKKQ9SC6n7ui8YZe0DQ 1 0  6 1845 239.9kb 239.9kb
green  open .kibana-event-log-7.9.1-000001 hL1iRZ8cRX6qe4of7TPYMQ 1 0  1    0   5.5kb   5.5kb
green  open .apm-agent-configuration       w5BmEwunQ96q6KdABWW3pA 1 0  0    0    208b    208b
green  open .kibana_1                      Rnt_C5o5RquYYgkvBZ3xJQ 1 0 13    2  10.4mb  10.4mb

我们可以看见一个已经生成的 logs_json 索引。

我们可以使用如下的命令来检查它的内容:

GET logs_json/_search

上面的命令显示:

{
  "took" : 0,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 4,
      "relation" : "eq"
    },
    "max_score" : 1.0,
    "hits" : [
      {
        "_index" : "logs_json",
        "_type" : "_doc",
        "_id" : "mePsNXUBuukB9WUDNLdT",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-10-16T06:37:44.000Z",
          "userId" : 12,
          "input" : {
            "type" : "log"
          },
          "ecs" : {
            "version" : "1.5.0"
          },
          "host" : {
            "name" : "liuxg"
          },
          "agent" : {
            "type" : "filebeat",
            "version" : "7.9.1",
            "hostname" : "liuxg",
            "ephemeral_id" : "2c0b6672-cb9e-4074-bee4-c550622d273a",
            "id" : "a1e3e46b-ca14-457d-bbfb-97133166b5b9",
            "name" : "liuxg"
          },
          "level" : "info",
          "log" : {
            "offset" : 0,
            "file" : {
              "path" : "/Users/liuxg/go/es_logs/out.log"
            }
          },
          "message" : "User logged in!"
        }
      },
      {
        "_index" : "logs_json",
        "_type" : "_doc",
        "_id" : "muPsNXUBuukB9WUDNLdT",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-10-16T06:37:44.000Z",
          "ecs" : {
            "version" : "1.5.0"
          },
          "host" : {
            "name" : "liuxg"
          },
          "log" : {
            "offset" : 98,
            "file" : {
              "path" : "/Users/liuxg/go/es_logs/out.log"
            }
          },
          "level" : "info",
          "message" : "Sent a message!",
          "userId" : 12,
          "input" : {
            "type" : "log"
          },
          "agent" : {
            "hostname" : "liuxg",
            "ephemeral_id" : "2c0b6672-cb9e-4074-bee4-c550622d273a",
            "id" : "a1e3e46b-ca14-457d-bbfb-97133166b5b9",
            "name" : "liuxg",
            "type" : "filebeat",
            "version" : "7.9.1"
          }
        }
      },
      {
        "_index" : "logs_json",
        "_type" : "_doc",
        "_id" : "m-PsNXUBuukB9WUDNLdT",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-10-16T06:37:44.000Z",
          "message" : "Failed to get a message!",
          "input" : {
            "type" : "log"
          },
          "agent" : {
            "id" : "a1e3e46b-ca14-457d-bbfb-97133166b5b9",
            "name" : "liuxg",
            "type" : "filebeat",
            "version" : "7.9.1",
            "hostname" : "liuxg",
            "ephemeral_id" : "2c0b6672-cb9e-4074-bee4-c550622d273a"
          },
          "ecs" : {
            "version" : "1.5.0"
          },
          "host" : {
            "name" : "liuxg"
          },
          "log" : {
            "offset" : 196,
            "file" : {
              "path" : "/Users/liuxg/go/es_logs/out.log"
            }
          },
          "userId" : 12,
          "level" : "info"
        }
      },
      {
        "_index" : "logs_json",
        "_type" : "_doc",
        "_id" : "nOPsNXUBuukB9WUDNLdT",
        "_score" : 1.0,
        "_source" : {
          "@timestamp" : "2020-10-16T06:37:44.000Z",
          "level" : "info",
          "message" : "User logged out!",
          "userId" : 12,
          "input" : {
            "type" : "log"
          },
          "agent" : {
            "type" : "filebeat",
            "version" : "7.9.1",
            "hostname" : "liuxg",
            "ephemeral_id" : "2c0b6672-cb9e-4074-bee4-c550622d273a",
            "id" : "a1e3e46b-ca14-457d-bbfb-97133166b5b9",
            "name" : "liuxg"
          },
          "ecs" : {
            "version" : "1.5.0"
          },
          "host" : {
            "name" : "liuxg"
          },
          "log" : {
            "file" : {
              "path" : "/Users/liuxg/go/es_logs/out.log"
            },
            "offset" : 303
          }
        }
      }
    ]
  }
}

从上面我们可以看出来,我们已经成功地把日志导入到 Elasticsearch  中了。