likes
comments
collection
share

go语言在项目中日志可以这样处理,帮助你快速定位业务工单问题

作者站长头像
站长
· 阅读数 2

go语言在项目中日志可以这样处理,帮助你快速定位业务工单问题

本文呢并不是教你如何去构建一个日志处理组件,而是针对日志的使用我们可以自定义的优化一波,当然前提是根据自己的项目而定义【目前业界的日志处理机制已经很优秀了,没必要再造轮子】

1. 背景

在团队整体项目没有采用如链路跟踪与各种监控的情况下,我们对于生产环境下提交的问题工单,在解决的时候往往就需要依据于 【数据】【日志】【代码流程】这三个方面来分析问题,其中【日志】就成为在项目中问题定位的非常重要的因素。但对于日志的记录往往因团队的不统一导致各有千秋,并且日志信息繁多无法区分日志。

比如:工单情况是 xxx编号的商品下架失败

如下为示例日志

{"@timestamp":"2024-03-15T15:11:31.230+08:00","caller":"zlog/zlog_test.go:26","content":"商品添加失败","level":"error"}
{"@timestamp":"2024-03-15T16:09:02.210+08:00","caller":"zlog/zlog_test.go:26","content":"商品添加失败","level":"error"}
{"@timestamp":"2024-03-15T16:09:02.210+08:00","caller":"zlog/zlog_test.go:26","content":"商品下架失败11111","level":"error"}

根据如上的日志分析呢是存在难点的,首先日志信息的定位需要基于@timestamp,然后基于日志信息验证是否为当前该请求的执行日志,再确定当前所获取的日志信息是否为当前这一次完整请求的日志信息,过程呢是比较繁琐并且日志信息不好确认是否为当次请求。

总结而言日志情况就是:

  1. 无明确的相关参数用于查找
  2. 难以确定工单对应请求的相关日志

--

在技术的背景下:

  1. 无确定统一的日志结构
  2. 无链路跟踪的机制
  3. 就单纯的有一些日志

2. 如何解决呢

首先从实际的项目业务来讲我们需要确定最关心,最关键的两个操作,其中第二点最重要

  1. 如何根据工单的信息找到相关日志
  2. 如确定工单与之对应请求的所有日志

根据如上的流程以及信息分析呢,在业务中我们可以这样定义数据结构,即存入到日志中的。

package tlog

type Content struct {
    TraceId   string `json:"tlog_traceId"`
    Path      string `json:"tlog_path"`
    RelatedId string `json:"tlog_relatedId"`
    Msg       string `json:"tlog_msg"`
    Label     string `json:"tlog_label"`
    Level     string `json:"tlog_level"`
}

在上面的结构中

  • traceId:用于记录整个请求的链路,即当次的所有请求中涉及的日志都会统一traceId记录,例如:1234567
  • path:代码执行的位置
  • relatedId:相关任务的关联id
    • msg:消息内容
  • label:标签,因在项目中可能分为移动端和管理端,日志信息可能记录在统一个位置已做区分
  • level:日志级别

其中针对前面提到的问题重点关键字段是traceId与relatedId;

  • traceId:用于获取某一次请求的所有日志信息
  • relatedId:用于确定工单相关的日志信息有哪些

这样定义之后,那么我们在程序中的问题排查过程就改为了;根据工单确定发生问题的时间,以relatedId+时间为条件查询所有相关的日志,再以traceId获取对应请求的所有日志以做分析,这样就可以快速的定位。

3. 如何处理呢

我们可以在自己使用的日志组件上进一步的封装即可,以zap日志处理为例。

3.1. 日志输出

首先我们定义日志写入writer接口,用于扩展多样化的输入地址,同时还定义loggerWriter实现writer,作为默认的输出

package tlog

import (
  "context"
  "fmt"
)

type LoggerWriter interface {
   ////
   // Write
   //  @Description: 日志写处理
   //
   Write(ctx context.Context, content *Content)
}

type loggerWriter struct{}

func NewLoggerWriter() LoggerWriter {
  return &loggerWriter{}
}

func (l *loggerWriter) Write(ctx context.Context, content *Content) {
  fmt.Printf("{traceId: %v, label: %v, level: %v, path: %v, relatedId: %v, msg: %v}\n", content.TraceId, content.Label, content.Level, content.Path, content.RelatedId, content.Msg)
}

3.2. 日志处理

在整个日志中因为需要获取到请求的traceId,从代码上最好的就是基于context进行传递是最好的方式,因此在获取与设置的本质代码是这样

// 用于作为context的key
type LogContext struct{}

// 从context中获取
context.WithValue(ctx, LogContext{}, "TraceId")

// 读取
traceId, ok := ctx.Value(LogContext{}).(string)
if !ok {
    traceId = "TraceId"
}

具体代码:

先定义一个用于生成traceId的方法, 直接用随机

package tlog

import (
   "crypto/rand"
   "fmt"
   "strconv"
   "strings"

   "gitee.com/dn-jinmin/gen-id/utils"
)

const idLen = 8

func RandId() string {
   b := make([]byte, idLen)
   _, err := rand.Read(b)
   if err != nil {
      return Randn(idLen)
   }

   return fmt.Sprintf("%x%x%x%x", b[0:2], b[2:4], b[4:6], b[6:8])
}

func Randn(n int) string {
   n = n * 2

   var ret strings.Builder
   ret.Grow(n)
   utils.RandIntHandler(10, n, func(num, i int) {
      ret.Write([]byte(strconv.Itoa(num)))
   })
   return ret.String()
}

定义好配置信息

package tlog

type Config struct {
   // 系统的开发模式
   Mode int
   // 日志标签
   Label string

   LogWriteLimit int
   // 日志的Logger对象
   LoggerWriter []LoggerWriter
}

在配置信息中LoggerWriter采用数组,是因为可以对多个方向进行输入

完善日志的核心实现

package tlog

import (
   "context"
   "fmt"
)

var (
   globalLogger  = []LoggerWriter{NewLoggerWriter()}
   globalLabel   = "tlog"
   globalMode    = DEV
   LogWriteLimit = 3

   defaultLogger = NewLoggerWriter()
)

type LogContext struct{}

// mode
// 日志的模式;0.所有信息都会写入到Logger中、1.所有信息会输出在控制台,2.只将Info,Error输出到Logger中
const (
  All = iota + 1
  DEV
  PROD
)

// level
const (
  INFO  = "info"
  ERROR = "error"
  DEBUG = "debug"
)
func Init(cfg *Config) {
  if cfg.Mode > 0 {
    globalMode = cfg.Mode
  }
  if cfg.Label != "" {
    globalLabel = cfg.Label
  }

  if len(cfg.LoggerWriter) > 0 {
    globalLogger = cfg.LoggerWriter
  }

  if cfg.LogWriteLimit > 0 {
    LogWriteLimit = cfg.LogWriteLimit
  }
}

func TraceStart(ctx context.Context) context.Context {
  return context.WithValue(ctx, LogContext{}, TraceId())
}

func TraceId() string {
  return RandId()
}

// 日志刷新
func Write(ctx context.Context, level string, relatedId, msg string) {
  if !shallLog(level) {
    return
  }

  traceId, ok := ctx.Value(LogContext{}).(string)
  if !ok {
    traceId = TraceId()
  }

  var path string
  f, ok := getCallerFrame(getCallerSkipOffset(ctx))
  if ok {
    path = fmt.Sprintf("%s:%v", f.File, f.Line)
  }

  content := &Content{
    TraceId:   traceId,
    Path:      path,
    RelatedId: relatedId,
    Msg:       msg,
    Label:     globalLabel,
    Level:     level,
  }

  limit := make(chan struct{}, LogWriteLimit)
  for _, writer := range globalLogger {
    limit <- struct{}{}
    go func() {
      writer.Write(ctx, content)

      <-limit
    }()
  }
}

// 日志是否写入
func shallLog(level string) bool {
  switch globalMode {
  case All:
    return true
  case DEV:
    return true
  case PROD:
    if level != DEBUG {
      return true
    }
  }
  return false
}

对外的方法以info为示例

package tlog

// 记录Info日志: 建议用context传参类型,可以根据任务id记录当次请求
func Info(relatedId string, msg interface{}) {
   InfoCtx(context.Background(), relatedId, fmt.Sprintf("%v", msg))
}

// 记录Info日志: 建议用context传参类型,可以根据任务id记录当次请求
func Infof(relatedId, format string, a ...any) {
   InfofCtx(context.Background(), relatedId, fmt.Sprintf(format, a...))
}

func InfoCtx(ctx context.Context, relatedId string, msg interface{}) {
   Write(ctx, INFO, relatedId, fmt.Sprintf("%v", msg))
}

func InfofCtx(ctx context.Context, relatedId, format string, a ...any) {
   Write(ctx, INFO, relatedId, fmt.Sprintf(format, a...))
}

示例测试

package main

import (
   "context"
   "errors"
   "time"

  "gitee.com/dn-jinmin/tlog"
)

func main() {
   // 初始化
   tlog.Init(&tlog.Config{
      Mode:  tlog.All,
      Label: "tlog示例",
   })

   ctx := tlog.TraceStart(context.Background())

   tlog.InfoCtx(ctx, "", "tlog示例")

   service := NewOrderService()
   service.GetOrder(ctx, "1234567890")

   time.Sleep(time.Second)
}

// ------------- service ---------------------

type OrderService struct {
   *OrderModel
}

func NewOrderService() *OrderService {
   return &OrderService{
      OrderModel: NewOrderModel(),
   }
}
func (s *OrderService) GetOrder(ctx context.Context, orderId string) (interface{}, error) {

   tlog.DebugfCtx(ctx, orderId, "orderId %v", orderId)

   data, err := s.OrderModel.GetOrder(ctx, orderId)
   if err != nil {
      tlog.ErrorfCtx(ctx, orderId, "orderId %v, err %v", orderId, err)
   }

   tlog.InfofCtx(ctx, orderId, "orderId %v, data %v", orderId, data)

   return "订单数据", errors.New("示例的异常")
}

// -------------- model ---------------------

type OrderModel struct{}

func NewOrderModel() *OrderModel {
   return &OrderModel{}
}
func (m *OrderModel) GetOrder(ctx context.Context, orderId string) (interface{}, error) {
   tlog.InfofCtx(ctx, orderId, "orderId %v", orderId)
   return "订单数据", errors.New("示例的异常")
}

效果

{traceId: b21fd73c9afefef1, label: tlog示例, level: info, path: D:/01.project/01.go/src/gitee.com/dn-jinmin/tlog/example/main.go:56, relatedId: 1234567890, msg: orderId 1234567890, data 订单数据}
{traceId: b21fd73c9afefef1, label: tlog示例, level: info, path: D:/01.project/01.go/src/gitee.com/dn-jinmin/tlog/example/main.go:28, relatedId: , msg: tlog示例}
{traceId: b21fd73c9afefef1, label: tlog示例, level: error, path: D:/01.project/01.go/src/gitee.com/dn-jinmin/tlog/example/main.go:53, relatedId: 1234567890, msg: orderId 1234567890, err 示例的异常}
{traceId: b21fd73c9afefef1, label: tlog示例, level: info, path: D:/01.project/01.go/src/gitee.com/dn-jinmin/tlog/example/main.go:69, relatedId: 1234567890, msg: orderId 1234567890}
{traceId: b21fd73c9afefef1, label: tlog示例, level: debug, path: D:/01.project/01.go/src/gitee.com/dn-jinmin/tlog/example/main.go:49, relatedId: 1234567890, msg: orderId 1234567890}

3.3. 基于zap扩展示例

package main

import (
   "context"
   "gitee.com/dn-jinmin/tlog"
   "go.uber.org/zap"
)

type zapWriter struct {
   *zap.Logger
}

func NewZapWriter() *zapWriter {
   l, _ := zap.NewProduction()
   return &zapWriter{Logger: l}
}

func (z *zapWriter) Write(ctx context.Context, content *tlog.Content) {
   switch content.Level {
   case tlog.INFO:
      z.Logger.Info(content.Msg,
         zap.String("traceId", content.TraceId),
         zap.String("relatedId", content.RelatedId),
         zap.String("level", content.Level),
         zap.String("path", content.Path),
         zap.String("label", content.Label),
      )
   }
}

修改示例的初始化就好了

package main

func main() {
   // 初始化
   tlog.Init(&tlog.Config{
      Mode:  tlog.All,
      Label: "tlog示例",
      LoggerWriter: []tlog.LoggerWriter{NewZapWriter()},
   })

   ctx := tlog.TraceStart(context.Background())

   tlog.InfoCtx(ctx, "", "tlog示例")

   service := NewOrderService()
   service.GetOrder(ctx, "1234567890")

   time.Sleep(time.Second)
}