将go-zero日志推送至ELK中

本文示例代码地址:gitee.com/dn-jinmin/z...

1. 实现分析

想要让go-zero与elk结合,在实现的方式上有两种

  1. 将go-zero的日志输出到本地的日志文件中,通过filebeat采集推送至elk中进行分析

  2. 自定义日志输出存储位置,由elk基于存储位置收集日志信息

实现难度上第一种最为简单,第二种相对有难度。如果第二种方案是基于redis那么相对而言性能会更好,但是你得先理解go-zero的日志机制,因为这需要基于go-zero的日志机制才能实现。

2. 方案1基于本地日志方式

日志用例代码zlog_test.go

Go 复制代码
func TestFile_Log(t *testing.T) {
   logx.SetUp(logx.LogConf{
      ServiceName: "file.log",
      Mode:        "file",
      Encoding:    "json",
      Path:        "./zlog",
   })

   // -------------------------------- info --------------------------

   logx.Info("测试", "info")
   logx.Infof("测试  %v", "info")

   // -------------------------------- error --------------------------

   logx.Error("测试", "error")
   logx.Errorf("测试  %v", "error")

   // -------------------------------- debug --------------------------

   logx.Debug("测试", "debug")
   logx.Debugf("测试  %v", "debug")
}

执行后会在代码的同级生成对应的日志

Plain 复制代码
~zlog
  - zlog_test.go
  - zlog
    - access.log
    - error.log
    - severe.log
    - slow.log
    - stat.log

无论那种日志文件,日志中的内容大体如下格式

Go 复制代码
{"@timestamp":"2024-03-15T15:11:31.230+08:00","caller":"zlog/zlog_test.go:26","content":"测试info","level":"info"}

因此我们接下来是需要编写好filebeat与logstash的配置信息

filebeat的配置信息

zlog.yml

YAML 复制代码
filebeat.inputs:
  - type: log
    enabled: true
    paths:

    # 如果值为ture,那么fields存储在输出文档的顶级位置
    fields_under_root: true
    fields:
      type: zlog.file

output.logstash:
  hosts: ["192.168.117.80:5044"]

logstash的配置信息

logstash.conf

YAML 复制代码
input{
    beats {
        port => 5044
    }
}

filter {
    grok {
         match => {
            "message" => "^{"@timestamp":"%{DATA:timestamp}","caller":"%{DATA:caller}","content":"%{DATA:content}","level":"%{DATA:level}"}"
        }
    }
}
output{
    # 标准输出到控制台
    stdout{
        codec => rubydebug
    }

    # 按需
    # elasticsearch {
    #    hosts => ["http://192.168.117.80:9200"]
    #    index => "zlog.file"
    #    user => "admin"
    #    password => "000000"
    # }
}

效果

如下是logstash的输出

JSON 复制代码
{
     "timestamp" => "2024-03-15T16:09:02.210+08:00",
         "input" => {
        "type" => "log"
    },
        "caller" => "zlog/zlog_test.go:26",
          "type" => "zlog.file",
       "message" => "{"@timestamp":"2024-03-15T16:09:02.210+08:00","caller":"zlog/zlog_test.go:26","content":"测试info","level":"info"}",
           "log" => {
          "file" => {
            "path" => "/root/easy-im/pkg/zlog/zlog/access.log"
        },
        "offset" => 234
    },
           "ecs" => {
        "version" => "1.12.0"
    },
       "content" => "测试info",
      "@version" => "1",
          "tags" => [
        [0] "beats_input_codec_plain_applied"
    ],
    "@timestamp" => 2024-03-07T11:32:08.019Z,
         "level" => "info",
         "agent" => {
            "hostname" => "localhost.localdomain",
                  "id" => "a7b9c31e-9955-4044-9900-70b8a2b0fb4a",
                "type" => "filebeat",
             "version" => "7.17.4",
        "ephemeral_id" => "4c935724-a2bd-4c65-96a2-9a969b6cf9fb",
                "name" => "localhost.localdomain"
    },
          "host" => {
        "name" => "localhost.localdomain"
    }
}

3. 自定义日志输出方式

该方式我们采取 go-zero 将日志推送至redis中 ,然后logstash从redis中采集日志。该方式的难点在于你需要理解go-zero中日志的处理。该节点跳过理解分析过程,放在后面第4点。

3.1 核心代码

目录结构; redis_io_writer 与 redis_logx_writer 是基于go-zero的两种实现,推荐用redis_io_writer 方式

Go 复制代码
- zlog
  - redis_io_writer.go
  - redis_logx_writer.go
  - tool.go

redis_io_writer.go

Go 复制代码
package zlog

import (
   "io"

   "github.com/zeromicro/go-zero/core/stores/redis"
)

type redisIoWriter struct {
   redisKey string
   redis    *redis.Redis
}

func NewRedisIoWriter(redisKey string, redisCfg redis.RedisConf) io.Writer {
   return &redisIoWriter{
      redisKey: redisKey,
      redis:    redis.MustNewRedis(redisCfg),
   }
}

func (r *redisIoWriter) Write(p []byte) (n int, err error) {
   go r.redis.Rpush(r.redisKey, string(p))

   return 0, err
}

redis_logx_writer.go

Go 复制代码
package zlog

import (
   "encoding/json"
   "fmt"
   "github.com/zeromicro/go-zero/core/logx"
   "github.com/zeromicro/go-zero/core/stores/redis"
   "time"
)

const (
   levelAlert  = "alert"
   levelInfo   = "info"
   levelError  = "error"
   levelSevere = "severe"
   levelFatal  = "fatal"
   levelSlow   = "slow"
   levelStat   = "stat"
   levelDebug  = "debug"
)

const (
   callerKey    = "caller"
   contentKey   = "content"
   durationKey  = "duration"
   levelKey     = "level"
   spanKey      = "span"
   timestampKey = "@timestamp"
   traceKey     = "trace"
   truncatedKey = "truncated"
)

type redisLogxWriter struct {
   redisKey string
   redis    *redis.Redis
}

func NewRedisLogxWriter(redisKey string, redisCfg redis.RedisConf) logx.Writer {
   return &redisLogxWriter{
      redisKey: redisKey,
      redis:    redis.MustNewRedis(redisCfg),
   }
}

func (n *redisLogxWriter) Alert(v any) {
   n.outPut(v, levelAlert)
}

func (n *redisLogxWriter) Close() error {
   return nil
}

func (n *redisLogxWriter) Debug(v any, fields ...logx.LogField) {
   n.outPut(v, levelDebug, fields...)
}

func (n *redisLogxWriter) Error(v any, fields ...logx.LogField) {
   n.outPut(v, levelError, fields...)
}

func (n *redisLogxWriter) Info(v any, fields ...logx.LogField) {
   n.outPut(v, levelInfo, fields...)
}

func (n *redisLogxWriter) Severe(v any) {
   n.outPut(v, levelSevere)
}

func (n *redisLogxWriter) Slow(v any, fields ...logx.LogField) {
   n.outPut(v, levelSlow, fields...)
}

func (n *redisLogxWriter) Stack(v any) {
   n.outPut(v, levelError)
}

func (n *redisLogxWriter) Stat(v any, fields ...logx.LogField) {
   n.outPut(v, levelStat, fields...)
}

func (n *redisLogxWriter) outPut(v any, level string, fields ...logx.LogField) {
   // 根据日志处理数据信息

   // 日志内容
   content := make(map[string]any)

   content[contentKey] = v
   for _, field := range fields {
      t := logx.Field(field.Key, field.Value)
      content[t.Key] = t.Value
   }

   // 增加时间
   content[timestampKey] = time.Now().UnixNano()
   // 等级
   content[levelKey] = level

   f, ok := getCallerFrame(6)
   if ok {
      content[callerKey] = fmt.Sprintf("%v:%v", f.File, f.Line)
   }

   // 格式化

   b, err := json.Marshal(content)
   if err != nil {
      return
   }

   data := string(b)

   // 输出
   go n.redis.Rpush(n.redisKey, data)
}

tool.go

Go 复制代码
// 获取代码的执行行数
func getCallerFrame(skip int) (frame runtime.Frame, ok bool) {
   pc := make([]uintptr, 1)
   numFrames := runtime.Callers(skip, pc)
   if numFrames < 1 {
      return
   }

   frame, _ = runtime.CallersFrames(pc).Next()
   return frame, frame.PC != 0
}

3.2 logstash的配置

测试用例

Go 复制代码
func getRedisIoWriter() io.Writer {
   return NewRedisIoWriter("redis.io.writer", redis.RedisConf{
      Host:        "192.168.117.80:16379",
      Type:        "node",
      Pass:        "",
      Tls:         false,
      NonBlock:    false,
      PingTimeout: 0,
   })
}

func Test_redisIoWriter_Write(t *testing.T) {
   io := getRedisIoWriter()
   logx.SetWriter(logx.NewWriter(io))

   // -------------------------------- info --------------------------

   logx.Info("测试", "info")
   logx.Infof("测试  %v", "info")
  
   for {
   }
}

func getRedisLogxWrite() logx.Writer {
   return NewRedisLogxWriter("redis.logx.writer", redis.RedisConf{
      Host:        "192.168.117.80:16379",
      Type:        "node",
      Pass:        "",
   })
}

func TestNewRedisLogxWriter(t *testing.T) {
   writer := getRedisLogxWrite()
   logx.SetWriter(writer)

   // -------------------------------- info --------------------------

   logx.Info("测试", "info")
   logx.Infof("测试  %v", "info")
  
   for {
   }
}

如上用例执行之后那么在redis中是可以看到相应的日志信息的

然后修改logstash的配置信息即可

YAML 复制代码
input{
    beats {
        port => 5044
    }

    redis {
         data_type => "list"
         key => "redis.io.writer"
         host => "192.168.117.80"
         password => ""
         port => 16379
         db => 0
         threads => 2
         type => "redis.io.writer"
    }
    redis {
         data_type => "list"
         key => "redis.logx.writer"
         host => "192.168.117.80"
         password => ""
         port => 16379
         db => 0
         threads => 2
         type => "redis.logx.writer"
    }
}

filter {
    grok {
         match => {
            "message" => "^{"@timestamp":"%{DATA:timestamp}","caller":"%{DATA:caller}","content":"%{DATA:content}","level":"%{DATA:level}"}"
        }
    }
}
output{
    # 标准输出到控制台
    stdout{
        codec => rubydebug
    }

    # 按需
    # elasticsearch {
    #    hosts => ["http://192.168.117.80:9200"]
    #    index => "zlog.file"
    #    user => "admin"
    #    password => "000000"
    # }
}

4. 如何基于go-zero的日志扩展?

躺平先摸鱼,晚点补

5. 关于logstash中grok的内容

关于logstash中grok的内容,在填写前先去这个网址根据日志类型来编辑定义grokconstructor.appspot.com/do/match#re...

相关推荐
wn53129 分钟前
【Go - 类型断言】
服务器·开发语言·后端·golang
m0_6355022033 分钟前
Spring Cloud Gateway组件
网关·微服务·负载均衡·过滤器
希冀1231 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper1 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文2 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people2 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政8 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师9 小时前
spring获取当前request
java·后端·spring
Java小白笔记11 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
nbsaas-boot11 小时前
微服务之间的安全通信
安全·微服务·架构