Go日志系统

1.日志系统

服务器日志是服务器运行过程中记录的各种信息的集合,它们对于系统管理员和开发人员来说具有重要的意义。例如, 调试,监控,行为分析等等。

go自带一个log库,但与java生态存在同样的窘境,就是被第三方工具盖住了锋芒。例如java日志系统一般使用的是slfj坐门面,log4j或log4j2或logback做实现。

Go的log包提供了简单的日志记录功能,但它的输出格式和功能相对固定,不支持日志级别和结构化日志。如果要使用一些高级日志功能,可以采用一些第三方日志库,例如logrus,zerolog等。本文使用logrus进行演示。

2.logrus 介绍

logrus 是一个开源的 Go 语言日志库,它提供了一个简单、灵活且功能丰富的日志记录系统,被广泛用于 Go 应用程序中。以下是 logrus 的一些主要特性:

  1. 结构化日志logrus 支持结构化日志记录,这意味着你可以以键值对的形式记录日志,使得日志更易于解析和处理。

  2. 多种日志级别:它支持多种日志级别,包括调试(debug)、信息(info)、警告(warn)、错误(error)和致命(fatal)级别。这有助于在不同的环境中控制日志的详细程度。

  3. 自定义日志格式logrus 允许你自定义日志的输出格式,包括文本格式和 JSON 格式。你可以通过实现 Formatter 接口来创建自定义格式。

  4. 钩子(Hooks)logrus 支持钩子,这些钩子可以在日志记录事件发生时执行额外的动作,比如发送警报、记录到外部系统等。

  5. 日志轮转 :虽然 logrus 本身不直接支持日志轮转,但可以通过集成第三方库(如 lumberjack)来实现日志文件的自动轮转。

  6. 并发安全logrus 是并发安全的,可以在多个 goroutine 中使用而不需要额外的同步措施。

下面是一个简单的例子:

Go 复制代码
package main

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

func main() {
	// 创建一个新的Logger
	logger := logrus.New()

	// 设置日志输出格式为JSON
	logger.Formatter = &logrus.JSONFormatter{}

	// 设置日志级别
	logger.Level = logrus.InfoLevel

	// 记录一条信息级别的日志
	logger.Info("这是一条信息级别的日志")

	// 记录一条错误级别的日志
	logger.Error("这是一条错误级别的日志")

	// 记录带有字段的结构化日志
	logger.WithFields(logrus.Fields{
		"animal":

如果要实现日志翻页功能(每隔一天,或者超过XX大小自动翻页),需要结合lestrrat-go/file-rotatelogs工具。

3.合理的日志分类

在生产环境,主要有三大类日志,一种是系统日志,主要用于记录程序的行为,用于排查bug,行为监控等;一种则是运营日志,主要用于数据分析(如果是游戏服务器,当程序出现bug,可用于补偿或者回收)。最后一种是异常日志,用于修复bug。

对于系统日志,一般无需结构化输出,只有肉眼可分析即可。例如可以用下面的格式:

Go 复制代码
2024-09-08 19:46:54 [info] ----test1---
2024-09-08 19:46:54 [info] game server is starting ...
2024-09-08 19:48:21 [info] ----test2---
2024-09-08 19:48:21 [info] game server is starting ...
2024-09-08 19:50:14 [info] ----test3---
2024-09-08 19:50:14 [info] game server is starting ...

对于运营日志,如果服务器是分布式部署,需要将不同进程产生的运营日志统一采集到指定的目录,例如通过 ELK(Elasticsearch、Logstash、Kibana)或者hadoop。因此,运营日志一定是结构化日志(类似于mysql的表,有统一的格式),例如可以用下面的格式:

Go 复制代码
time|1725276165776|model|request|url|/var/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166035|model|request|url|/var/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166288|model|request|url|/array/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276166541|model|request|url|/array/queryUserGameVars|remoteIp|103.167.134.39, 172.71.214.147|localIp|127.0.0.1
time|1725276188600|model|request|url|/player/getProgress|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276188852|model|request|url|/player/getProgress|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276195164|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276195421|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276197467|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.147|localIp|127.0.0.1
time|1725276199553|model|request|url|/player/getArchives|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276206665|model|request|url|/template/create|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1
time|1725276206926|model|request|url|/template/create|remoteIp|103.167.134.39, 172.71.214.146|localIp|127.0.0.1

3.1.系统日志代码

Go 复制代码
func createConsoleLog() *logrus.Logger {
	logger := logrus.New()
	logger.Formatter = &consoleLogFormatter{}
	// 设置Logger的输出
	writer, _ := rotatelogs.New(
		"logs/app/"+"app.%Y%m%d",
		rotatelogs.WithMaxAge(time.Duration(24)*time.Hour),
	)
	logger.Out = writer
	// 设置Logger的日志级别
	logger.Level = logrus.InfoLevel
	return logger
}

logrus 默认提供了JSONFormatter和TextFormatter两种格式化工具,但日志格式跟笔者的习惯不是很吻合,所以使用了自定义的格式工具,只需实现下面的方法即可。

Go 复制代码
type Formatter interface {
	Format(*Entry) ([]byte, error)
}

代码如下:

Go 复制代码
type consoleLogFormatter struct{}

// Format 实现 logrus.Formatter 接口的 Format 方法。
func (f *consoleLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	// 按照自定义格式写入日志信息
	_, _ = fmt.Fprintf(b, "%s [%s] %s\n", entry.Time.Format(time.DateTime), entry.Level, entry.Message)
	return b.Bytes(), nil
}

程序调用的时候只需如下:

Go 复制代码
// Printf 记录一条日志
func Printf(v string) {
	consoleLog.Info(v)
}

需要注意的是,该方法只有一个字符串参数,外部调用需要将完整的字符串内容拼完再传进来。

3.2.运营日志代码

运营日志一般需要分模块,例如游戏里的商店、任务、抽奖等等,每个模块可以使用独立文件,或者全部模块放在同一个文件,通过类型进行区分,均可。

变量定义,申明日志类型,名称,以及用一个map保存已经创建好的日志对象

Go 复制代码
type LogType int

// 日志类型枚举,每一个类型对应独立的文件
const (
	Admin LogType = iota
	APPLICATION
)

var logName = map[LogType]string{
	Admin:       "admin",
	APPLICATION: "application",
}

var (
	logs       map[string]*logrus.Logger
)

使用自定义的格式化工具,创建logger对象

Go 复制代码
type businessLogFormatter struct{}

// Format 实现 logrus.Formatter 接口的 Format 方法。
func (f *businessLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {
	var b *bytes.Buffer
	if entry.Buffer != nil {
		b = entry.Buffer
	} else {
		b = &bytes.Buffer{}
	}

	_, _ = fmt.Fprintf(b, "%s", entry.Message)
	return b.Bytes(), nil
}


func createBusinessLog(name string) *logrus.Logger {
	logger := logrus.New()
	logger.Formatter = &businessLogFormatter{}
	writer, _ := rotatelogs.New(
		"logs/"+name+"/"+name+".%Y%m%d",
		rotatelogs.WithMaxAge(time.Duration(24)*time.Hour),
	)
	logger.Out = writer
	// 设置Logger的日志级别
	logger.Level = logrus.InfoLevel
	return logger
}

在初始化时,使用map缓存名称与对应的日志对象

Go 复制代码
func init() {
	logs = make(map[string]*logrus.Logger)

	for _, logType := range logName {
		logger := createBusinessLog(logType)
		logs[logType] = logger
	}
}

日志打印接口

Go 复制代码
func Info(name LogType, args ...interface{}) {
	if len(args)%2 != 0 {
		panic("log arguments must be odd number")
	}
	logger := logs[logName[name]]

	sb := &strings.Builder{}
	sb.WriteString("time|")
	sb.WriteString(fmt.Sprintf("%d", time.Now().UnixNano()/1000000))
	sb.WriteString("|")
	for i := 0; i < len(args); i += 2 {
		key, ok := args[i].(string)
		if !ok {
			panic(fmt.Sprintf("key is not a string: %v", args[i]))
		}
		value := args[i+1]
		sb.WriteString(key)
		sb.WriteString("|")
		sb.WriteString(fmt.Sprintf("%v", value))
		sb.WriteString("|")
	}
	sb.WriteString("\n")
	logger.Info(sb.String())
}

该函数比较复杂,函数的第一个参数代表日志的类型,第二个参数是一个变长参数,因为要拼接key,value的格式,需要变长参数的数量必须是偶数。将数组的奇数项作为key,偶数项作为value。调用代码示例:

Go 复制代码
log.Info(log.APPLICATION, "key1", "value1", "key2", "value2", "key3", "value3")

3.3.系统异常日志

异常日志没什么好讲的,最重要的是保留完整的堆栈日志,能够通过日志分析程序异常

Go 复制代码
func createErrorLog() *logrus.Logger {
	logger := logrus.New()
	logger.Formatter = &logrus.TextFormatter{ForceColors: true}
	writer, _ := rotatelogs.New(
		"logs/err/"+"error.%Y%m%d",
		rotatelogs.WithMaxAge(time.Duration(24)*time.Hour),
	)
	logger.Out = writer
	logger.Level = logrus.InfoLevel
	return logger
}

调用时只需传递异常参数即可

Go 复制代码
func Error(err error) {
	if err != nil {
		stack := make([]byte, 1024)
		n := runtime.Stack(stack, false)
		errorLog.WithFields(logrus.Fields{
			"error": err,
		}).Error(string(stack[:n]))
	}
}

完整代码请移步:

--> go游戏服务器

相关推荐
每天写点bug3 分钟前
【go每日一题】:并发任务调度器
开发语言·后端·golang
一个不秃头的 程序员5 分钟前
代码加入SFTP Go ---(小白篇5)
开发语言·后端·golang
基哥的奋斗历程44 分钟前
初识Go语言
开发语言·后端·golang
ZVAyIVqt0UFji7 小时前
go-zero负载均衡实现原理
运维·开发语言·后端·golang·负载均衡
唐墨12311 小时前
golang自定义MarshalJSON、UnmarshalJSON 原理和技巧
开发语言·后端·golang
老大白菜12 小时前
FastAPI vs Go 性能对比分析
开发语言·golang·fastapi
千年死缓15 小时前
golang结构体转map
开发语言·后端·golang
zyh_03052115 小时前
GO--堆(have TODO)
数据结构·算法·golang
翔云12345617 小时前
raft: Failed to contact
golang·raft·gc
ahhhhaaaa-1 天前
【AI图像生成网站&Golang】项目架构
开发语言·架构·golang