Go微服务,第10部分:集中式日志记录

在Go微服务博客系列的这一部分中,我们将介绍基于Logrus,Docker Gelf日志驱动程序和“作为服务的日志记录” Loggly服务的Go微服务的日志记录策略。

简介

日志。你永远不知道你有多想念他们,直到你真的想念他们。日志为您的团队提供关于记录什么,何时记录以及如何记录的指导方针,可能是制作可维护应用程序的关键因素之一。然后,微服务产生了。

虽然处理单片应用程序的一个或几个不同日志文件通常是可管理的(尽管存在例外......),但考虑对基于微服务的应用程序执行相同的操作,每个应用程序可能产生数百甚至数千个服务容器,每个容器都生成日志。如果你没有一个解决方案来以一种结构良好的方式收集和汇总你的日志,那么就不要考虑做大的事情。

值得庆幸的是,很多聪明人已经想到了这点,以前称为ELK的堆栈可能是开源社区中最知名的一个。弹性搜索、日志隐藏和Kibana组成了弹性堆栈,我推荐它用于内部部署和云部署。但是,可能已经有数十篇有关ELK的博客文章,因此在这个特别的博客中,我们将基于四个部分探讨一个LaaS(日志记录即服务)解决方案,以满足我们的集中式日志记录需求:

内容

  1. Logrus —— Go的日志框架
  2. Docker GELF驱动程序 —— Greylog扩展日志格式的日志记录驱动程序
  3. “Gelftail” —— 我们将在此博客文章中构建的轻量级日志聚合器。当然,我们会用Go编写它。
  4. Loggly —— 一家LaaS提供商。提供类似的功能来管理和处理类似服务的日志数据。

解决方案概述

源代码

完成的源代码可以从GitHub克隆:

> git clone https://github.com/callistaenterprise/goblog.git
> git checkout P10

1. Logrus - Go的日志API

通常,我们的Go微服务到现在为止都是使用“fmt”或“log”包进行日志记录的,无论是stdout还是stderr。我们希望给予我们更精细的日志级别和格式控制。在Java世界中,我们中的很多(大多数)都处理过诸如log4j,logback和slf4j之类的框架。Logrus是我们此博客系列的首选日志API,它大致提供了与我刚才提到的API级别、格式化、挂钩等相同类型的功能。

使用Logrus

logrus的好处之一是它实现了我们迄今为止用于登录的相同接口 —— fmtlog。这意味着我们可以或多或少地使用logrus作为替代品。在获取logrus源代码之前,首先要确保你的GOPATH是正确的,这样它就会被安装到你的GOPATH中:

> go get github.com/sirupsen/logrus

更新来源

我们会以古老的方式来做这件事。对于/ common/ accountservice/ vipservice 分别使用IDE或文本编辑器进行全局搜索和替换,其中fmt.*log.*logrus.*替换。现在你应该有很多的logrus.Printlnlogrus.Printf 调用。即使这样做很好,我还是建议使用logrus来支持诸如INFO、WARN、DEBUG等细节。例如:

FMT

日志

logrus

的println

的println

Infoln

printf的

printf的

Infof

错误

Errorln

有一个例外是用于产生错误实例的fmt.Error。不要替换fmt.Error

使用goimports更新导入

鉴于我们已经用logrus.Println(以及其他日志记录功能)替换了大量log.Printlnfmt.Println,现在我们有很多未使用的导入会导致编译错误。我们可以使用一个很小的工具 —— goimports,可以在命令行上下载并执行(或集成到你选择的IDE中),而不是一次一个地修复文件。

再次确保你的GOPATH是正确的。然后使用go get 来下载goimports:

go get golang.org/x/tools/cmd/goimports

这会将goimports安装到你的$ GOPATH / bin文件夹中。接下来,你可以转到accountservicevipservice 服务的根目录,例如:

cd $GOPATH/src/github.com/callistaenterprise/goblog/accountservice

然后,运行goimports,使用“-w”标志递归修复导入,该标志直接将更改应用于源文件。

$GOPATH/bin/goimports -w **/*.go

重复我们所有的微服务代码,包括/ common文件夹。

运行去构建 以确保服务编译。

go build

配置Logrus

如果我们完全不配置Logrus,它将以纯文本形式输出日志语句。例如:

logrus.Infof("Starting our service...")

它会输出:

INFO[0000] Starting our service...

其中0000 是服务启动后的秒数。这不是我想要的,我想要一个日期时间,所以我们必须提供一个格式化程序。

init()函数是那种设置的好方法:

func init() {
logrus.SetFormatter(&logrus.TextFormatter{
TimestampFormat: "2006-01-02T15:04:05.000",
FullTimestamp: true,
})
}

新的输出:

INFO[2017-07-17T13:22:49.164] Starting our service...

好多了。但是,在我们的微服务用例中,我们希望日志语句易于解析,以便我们最终可以将它们发送到我们选择的LaaS中,并对日志语句进行索引、排序、分组、聚合等。因此,我们希望只要我们没有以独立模式(即-profile = dev)运行微服务,就改用JSON格式化程序。

让我们稍微改变一下init()代码,以便它会使用JSON格式化程序,除非传递“-profile = dev”标志。

func init() {
    profile := flag.String("profile", "test", "Environment profile")
if *profile == "dev" {
logrus.SetFormatter(&logrus.TextFormatter{
TimestampFormat: "2006-01-02T15:04:05.000",
FullTimestamp: true,
})
} else {
logrus.SetFormatter(&logrus.JSONFormatter{})
}
}

输出:

{"level":"info","msg":"Starting our service...","time":"2017-07-17T16:03:35+02:00"}

就是这样。请随时阅读Logrus 文档以获取更全面的示例。

应该清楚的是,标准Logrus记录器不提供你可能从其他平台使用的细粒度控制 —— 例如,通过配置将输出从给定的更改为DEBUG。但是,可以创建范围记录器实例,这可以实现更细粒度的配置,例如:

var LOGGER = logrus.Logger{}   // <-- Create logger instance
func init() {
// Some other init code...
// Example 1 - using global logrus API
logrus.Infof("Successfully initialized")
// Example 2 - using logger instance
LOGGER.Infof("Successfully initialized")
}

(示例代码,不再回购)

通过使用LOGGER实例,可以更精细地配置应用程序级日志记录。但是,我已经选择使用logrus.*来进行博客系列的这部分内容的“全局”日志记录。

2. Docker Gelf驱动程序

什么是GELF?它是Greylog扩展日志格式的缩写,是logstash的标准格式。实际上,它将日志数据构造为JSON。在Docker的上下文中,我们可以配置一个Docker集群模式服务来使用各种驱动程序进行日志记录,这实际上意味着在一个容器中写入stdout或stderr的所有内容都是由Docker引擎“接收”的,并由已配置的日志驱动程序处理。这种处理包括添加许多关于容器、群集节点、服务等特定于Docker的元数据。示例消息可能如下所示:

{
      "version":"1.1",
      "host":"swarm-manager-0",
      "short_message":"Starting HTTP service at 6868",
      "timestamp":1.487625824614e+09,
      "level":6,
      "_command":"./vipservice-linux-amd64 -profile=test",
      "_container_id":"894edfe2faed131d417eebf77306a0386b43027e0bdf75269e7f9dcca0ac5608",
      "_container_name":"vipservice.1.jgaludcy21iriskcu1fx9nx2p",
      "_created":"2017-02-20T21:23:38.877748337Z",
      "_image_id":"sha256:1df84e91e0931ec14c6fb4e559b5aca5afff7abd63f0dc8445a4e1dc9e31cfe1",
      "_image_name":"someprefix/vipservice:latest",
      "_tag":"894edfe2faed"
}

让我们来看看如何在copyall.sh中更改我们的“ docker service create”命令来使用GELF驱动程序:

docker service create \
--log-driver=gelf \
--log-opt gelf-address=udp://192.168.99.100:12202 \
--log-opt gelf-compression-type=none \
--name=accountservice --replicas=1 --network=my_network -p=6767:6767 someprefix/accountservice
  • -log-driver = gelf告诉Docker使用gelf驱动程序
  • -log-opt gelf-address告诉Docker发送所有日志语句的位置。在gelf的情况下,我们将使用UDP协议并告诉Docker将日志语句发送到定义的IP:端口上的服务。该服务通常是诸如logstash之类的东西,但在我们的案例中,我们将在下一部分中构建自己的小日志聚合服务。
  • -log-opt gelf-compression-type告诉Docker在发送日志语句之前是否使用压缩。为了简单起见,在博客里这部分没有压缩。

这些不多也不少!任何由accountservice类型创建的微服务实例现在都会将写入stdout / stderr的所有内容发送到配置的端点。请注意,这意味着我们不能再使用docker日志的containerid命令来检查给定服务的日志,因为(默认)日志记录驱动程序不再被使用。

我们应该将这些gelf日志驱动程序配置语句添加到我们的shell脚本中的所有docker服务创建命令中,例如copyall.sh

不过,这种设置有一个不完善的问题——对群集管理器使用硬编码的ip地址。令人遗憾的是,即使我们将“gelftail”服务部署为Docker集群模式服务,我们也不能在声明服务时使用它的逻辑名称来解决此问题。我们或许可以用DNS或类似的方式解决这个缺点,如果你知道怎么做的话,请在评论中告诉我们。

使用Gelf与Logrus挂钩

如果你确实需要使你的日志记录不受容器和协调器更多的限制,那么可以选择使用Logrus 的gelf插件来使用hook执行GELF日志记录。在该设置中,Logrus将自行格式化日志语句为GELF格式,也可以通过配置将它们传输到UDP地址,就像使用Docker GELF驱动程序时一样。然而,默认情况下Logrus没有关于在容器中运行的概念,所以我们基本上必须弄清楚如何自己填充那些丰富的元数据,可能使用对Docker Remote API或操作系统函数的调用。

强烈建议使用Docker GELF驱动程序。尽管它将您的日志记录与Docker群集模式联系在一起,但其他容器编排器可能也支持从容器中收集stdout/stderr日志,并将其转发到中央日志记录服务。

3.使用“Gelftail”收集和聚合日志记录

发送所有日志语句的UDP服务器通常是Logstash或类似的,它提供了对日志语句的转换、聚合、过滤等功能的强大控制,然后将它们存储在后端,如弹性搜索或将它们推送到LaaS。

然而,Logstash并不完全是轻量级的,为了让事情变得简单(而且很有趣),我们将编写我们自己的小日志聚合器,我称它为“gelftail”。这个名字来源于一个事实:一旦我为我的所有服务配置了Docker GELF驱动程序,我就无法再看到记录的内容了!我决定编写一个简单的UDP服务器来接收发送给它的所有数据并转储到stdout,然后我可以使用docker日志来查看。例如,来自所有服务的所有日志语句的流。不是很实用,但至少比没有看到任何日志要好。

接下来,下一步自然就是将这个“gelftail”程序附加到LaaS后端,应用一些转换、语句批处理等,这正是我们马上要开发的内容!

Gelftail

在root / goblog文件夹中,创建一个名为gelftail的新目录。按照下面的说明创建必要的文件和文件夹。

mdkir $GOPATH/src/github.com/callistaenterprise/goblog/gelftail
mdkir $GOPATH/src/github.com/callistaenterprise/goblog/gelftail/transformer
mdkir $GOPATH/src/github.com/callistaenterprise/goblog/gelftail/aggregator
cd $GOPATH/src/github.com/callistaenterprise/goblog/gelftail
touch gelftail.go
touch transformer/transform.go
touch aggregator/aggregator.go

Gelftail按照这些方式工作:

  1. 启动UDP服务器(Docker GELF驱动程序将日志输出发送到的服务器)。
  2. 对于每个UDP数据包,我们假定它是来自Logrus的JSON格式的输出。我们将做一些解析来提取实际的级别short_message属性,并稍微转换 原始日志消息,以便它包含这些属性作为根级别元素。
  3. 接下来,我们将使用缓冲的go通道作为逻辑“发送队列”,也就是我们的聚合器 goroutine正在读取的 。对于每个收到的日志消息,它都会检查当前缓冲区是否大于1 kb。
  4. 如果缓冲区足够大,它将使用聚合语句对Loggly HTTP上载端点执行HTTP POST ,清除缓冲区并开始构建新的批处理。

使用经典的企业集成模式(以某种非惯用的方式)表达,它看起来像这样:

源代码

该程序将被分成三个文件。从一个 包和一些导入的gelftail.go开始:

package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"net"
"net/http"
"os"
    "io/ioutil"
"github.com/Sirupsen/logrus"
)

在注册Loggly(本博客系列的首选LaaS)时,我们会得到一个身份验证令牌,你必须将其视为机密。任何访问您的令牌的人至少都可以将日志语句发送到您的帐户中。所以,请确保你使用 .gitignore token.txt 或你为文件选的任何名称。当然,可以使用第7部分中的配置服务器并将身份验证令牌存储为加密属性。就目前而言,我尽可能简单地保存成文本文件。

因此,让我们为我们的LaaS令牌添加一个占位符,并尝试从磁盘加载该令牌的init()函数。如果不成功,我们也可以记录日志和问题。

var authToken = ""
var port *string
func init() {
data, err := ioutil.ReadFile("token.txt")
    if err != nil {
        msg := "Cannot find token.txt that should contain our Loggly token"
        logrus.Errorln(msg)
        panic(msg)
    }
    authToken = string(data)
    port = flag.String("port", "12202", "UDP port for the gelftail")
    flag.Parse()
}

我们还使用一个标志为UDP服务器提供一个可选的端口号。接下来,我们需要声明main()函数来开始。

func main() {
logrus.Println("Starting Gelf-tail server...")
ServerConn := startUDPServer(*port)   // Remember to dereference the pointer for our "port" flag
defer ServerConn.Close()
var bulkQueue = make(chan []byte, 1)  // Buffered channel to put log statements ready for LaaS upload into
go aggregator.Start(bulkQueue, authToken)          // Start goroutine that'll collect and then upload batches of log statements
go listenForLogStatements(ServerConn, bulkQueue)   // Start listening for UDP traffic
logrus.Infoln("Started Gelf-tail server")
wg := sync.WaitGroup{}
wg.Add(1)
wg.Wait()              // Block indefinitely
}

非常简单,启动UDP服务器,声明用于传递处理过的消息的通道,并启动“聚合器”。startUDPServer(* port)函数不是很有趣,因此我们将跳过listenForLogStatements(..)

func listenForLogStatements(ServerConn *net.UDPConn, bulkQueue chan[]byte) {
buf := make([]byte, 8192)                        // Buffer to store UDP payload into. 8kb should be enough for everyone, right Bill? :D
var item map[string]interface{}                  // Map to put unmarshalled GELF json log message into
for {
            n, _, err := ServerConn.ReadFromUDP(buf)     // Blocks until data becomes available, which is put into the buffer.
            if err != nil {
                logrus.Errorf("Problem reading UDP message into buffer: %v\n", err.Error())
                continue                                 // Log and continue if there are problms
            }
            err = json.Unmarshal(buf[0:n], &item)        // Try to unmarshal the GELF JSON log statement into the map
            if err != nil {                              // If unmarshalling fails, log and continue. (E.g. filter)
                logrus.Errorln("Problem unmarshalling log message into JSON: " + err.Error())
                item = nil
                continue
            }
            // Send the map into the transform function
            processedLogMessage, err := transformer.ProcessLogStatement(item)    
            if err != nil {
                logrus.Printf("Problem parsing message: %v", string(buf[0:n]))
            } else {
                bulkQueue <- processedLogMessage          // If processing went well, send on channel to aggregator
            }
            item = nil
    }
}

依据代码中的注释,该transformer.go文件也不是那么令人兴奋,它只是从一个JSON属性中读取一些东西,并将其转移到“根”GELF消息上。所以让我们跳过这个。

最后,深入了解/ goblog / gelftail / aggregator / aggregator.go中的“聚合器”代码,该代码处理来自bulkQueue通道的最终日志消息,汇总并上传到Loggly:

var client = &http.Client{}
var logglyBaseUrl = "https://logs-01.loggly.com/inputs/%s/tag/http/"
var url string
func Start(bulkQueue chan []byte, authToken string) {
        url = fmt.Sprintf(logglyBaseUrl, authToken) // Assemble the final loggly bulk upload URL using the authToken  
        buf := new(bytes.Buffer)
        for {
                msg := <-bulkQueue                 // Blocks here until a message arrives on the channel.
                buf.Write(msg)
                buf.WriteString("\n")              // Loggly needs newline to separate log statements properly.
                size := buf.Len()
                if size > 1024 {                   // If buffer has more than 1024 bytes of data...
                        sendBulk(*buf)  // Upload!
                        buf.Reset()
                }
        }
}

我只是喜欢Go代码的简单性!使用bytes.Buffer,我们只需输入一个永恒循环,在msg:= <-bulkQueue处阻塞,直到接收到一个消息(未缓冲的)通道。我们将内容+换行符写入缓冲区,然后检查缓冲区是否大于我们预先确定的1kb阈值。如果是,我们调用sendBulk函数并清除缓冲区。sendBulk只是对Loggly执行一个标准的HTTP POST。

生成,Dockerfile,部署

当然,我们将“gelftail”部署为Docker群模式服务,就像其他服务一样。为此,我们需要一个Dockerfile:

FROM iron/base
EXPOSE 12202/udp
ADD gelftail-linux-amd64 /
ADD token.txt /
ENTRYPOINT ["./gelftail-linux-amd64", "-port=12202"]

token.txt是一个带有Loggly授权令牌的简单文本文件,更多信息请参阅本文的第4部分。

构建和部署应该很简单。我们将在root / goblog目录中添加一个新的.sh脚本:

#!/bin/bash
export GOOS=linux
export CGO_ENABLED=0
cd gelftail;go get;go build -o gelftail-linux-amd64;echo built `pwd`;cd ..
export GOOS=darwin
docker build -t someprefix/gelftail gelftail/
docker service rm gelftail
docker service create --name=gelftail -p=12202:12202/udp --replicas=1 --network=my_network someprefix/gelftail

这应该会运行几秒钟。通过跟踪自己的stdout日志来验证gelftail是否已成功启动。使用docker ps查找它的容器ID ,然后使用docker logs 检查日志

> docker logs -f e69dff960cec
time="2017-08-01T20:33:00Z" level=info msg="Starting Gelf-tail server..." 
time="2017-08-01T20:33:00Z" level=info msg="Started Gelf-tail server"

如果你在使用另一个记录日志的服务执行某些操作,则该服务的日志输出现在应该显示在上面的尾部。我们将accountservice扩展为两个实例:

> docker service scale accountservice=2

上面的尾部docker日志现在应该输出如下内容:

time="2017-08-01T20:36:08Z" level=info msg="Starting accountservice" 
time="2017-08-01T20:36:08Z" level=info msg="Loading config from http://configserver:8888/accountservice/test/P10\n" 
time="2017-08-01T20:36:08Z" level=info msg="Getting config from http://configserver:8888/accountservice/test/P10\n"

这完全是为了“gelftail”。让我们通过快速浏览Loggly来结束这篇博文。

4. Loggly

有很多“Logging as a Service”服务提供商,我大概选择了一个(例如Loggly),它好像有一个适合演示目的的免费层,一个漂亮的GUI和一组丰富的用于上传日志语句的选项。

关于如何将日志导入Loggly的选项有很多(参见链接页面左侧的列表)。我决定使用HTTP / S事件API,它允许我们通过换行分隔小批量发送多个日志语句。

入门

我建议遵循他们的入门指南,这可以归结为:

  1. 创建一个帐户(免费套餐适用于演示/试用目的)。
  2. 获取授权令牌。将其保存在安全的地方并复制粘贴到/goblog/gelftail/token.txt中
  3. 决定如何“上传”你的日志。如上所述,我选择使用HTTP / S POST API。
  4. 配置你的服务/日志记录驱动程序/ logstash / gelftail等,并使用你选择的上传模式。

利用Loggly的所有功能已经超出了这个博客的范围。我只修改了它们的仪表板和过滤功能,我认为这是LaaS提供商的标准功能。

几个例子

在第一个屏幕截图中,我放大了35分钟的时间,在这段时间里,我明确地过滤了“accountservice”和“info”消息:

正如所见,可以非常容易地自定义列、过滤值、时间段等。

在下一个示例中,我查看的是相同的时间段,但只查看“error”日志语句:

虽然这些示例用例非常简单,但是当你拥有数十个运行1-n个实例的微服务时,真正的实用性就显现出来了。这时,LaaS强大的索引、过滤和其他功能就真正成为了微服务操作模型的基本部分。

总结

在本系列博文的第10部分中,我们介绍了集中式日志记录:为什么它很重要,如何在Go服务中执行结构化日志记录,如何从您的容器协调器中使用日志驱动程序,最后在将它们上传到日志即服务提供者之前对日志语句进行预处理。

在下一部分,我们将使用Netflix Hystrix为我们的微服务增加断路器和弹性。

本文的版权归 Aaroncang 所有,如需转载请联系作者。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏乐沙弥的世界

Linux 主机网络接入配置

网络配置是我们在安装好操作系统之后,需要解决的第一步。现时代没有接入网络的主机已然等同于一堆废铁。在网络配置的过程中,通常我们需要配置本机IP地址,缺省网关,D...

2800
来自专栏Java开发

INTELLIJ IDEA插件安装(阿里的编码约束)

在项目中,有的小伙伴第一次使用IDEA进行开发,想装开发插件(编码约束,lombk等)不知如何下手,下面小编就以安装Alibaba Java Coding Gu...

1452
来自专栏IMWeb前端团队

Nodejs进阶:核心模块net入门与实例讲解

模块概览 net模块是同样是nodejs的核心模块。在http模块概览里提到,http.Server继承了net.Server,此外,http客户端与http服...

3006
来自专栏Django中文社区

搭建开发环境

本教程使用的开发环境 本教程写作时开发环境的系统平台为 Windows 10 (64 位),Python 版本为 3.5.2 (64 位),Django 版本为...

3705
来自专栏技术博文

PHP连接MySQL数据库的三种方式(mysql、mysqli、pdo)

PHP与MySQL的连接有三种API接口,分别是:PHP的MySQL扩展 、PHP的mysqli扩展 、PHP数据对象(PDO) ,下面针对以上三种连接方式做下...

5904
来自专栏黑泽君的专栏

安装最新版本的Oracle公司的虚拟机软件 VirtualBox + 安装虚拟机 Windows XP 系统 + 安装 Oracle 11g 软件 + 出现 ERROR: ORA-12541: TNS

  VirtualBox的下载链接:https://www.virtualbox.org/wiki/Downloads

2201
来自专栏nice_每一天

理解 IntelliJ IDEA 的项目配置和Web部署

IDEA 中最重要的各种设置项,就是这个 Project Structre 了,关乎你的项目运行,缺胳膊少腿都不行。最近公司正好也是用之前自己比较熟悉的IDEA...

3592
来自专栏向治洪

在Windows下搭建React Native Android开发环境

安装JDK 从Java官网下载JDK并安装。请注意选择x86还是x64版本。 推荐将JDK的bin目录加入系统PATH环境变量。 安装Android S...

2706
来自专栏云原生架构实践

JHipster生成单体架构的应用示例

因为这个例子是生成单体架构的应用,所以这里选择默认选项Monolithic application,也就是单体架构的应用。

8032

如何自动地将代码从Git平台部署至组件容器

将源代码从Git平台部署至组件容器有很多种可以选择的方法,包括重新部署整个容器,通过卷即时重新部署,或者使用“git clone”的方法。但是,当这个过程自动化...

2359

扫码关注云+社区

领取腾讯云代金券