1.日志系统
服务器日志是服务器运行过程中记录的各种信息的集合,它们对于系统管理员和开发人员来说具有重要的意义。例如, 调试,监控,行为分析等等。
go自带一个log库,但与java生态存在同样的窘境,就是被第三方工具盖住了锋芒。例如java日志系统一般使用的是slfj坐门面,log4j或log4j2或logback做实现。
Go的log
包提供了简单的日志记录功能,但它的输出格式和功能相对固定,不支持日志级别和结构化日志。如果要使用一些高级日志功能,可以采用一些第三方日志库,例如logrus,zerolog等。本文使用logrus进行演示。
2.logrus
介绍
logrus
是一个开源的 Go 语言日志库,它提供了一个简单、灵活且功能丰富的日志记录系统,被广泛用于 Go 应用程序中。以下是 logrus
的一些主要特性:
-
结构化日志 :
logrus
支持结构化日志记录,这意味着你可以以键值对的形式记录日志,使得日志更易于解析和处理。 -
多种日志级别:它支持多种日志级别,包括调试(debug)、信息(info)、警告(warn)、错误(error)和致命(fatal)级别。这有助于在不同的环境中控制日志的详细程度。
-
自定义日志格式 :
logrus
允许你自定义日志的输出格式,包括文本格式和 JSON 格式。你可以通过实现Formatter
接口来创建自定义格式。 -
钩子(Hooks) :
logrus
支持钩子,这些钩子可以在日志记录事件发生时执行额外的动作,比如发送警报、记录到外部系统等。 -
日志轮转 :虽然
logrus
本身不直接支持日志轮转,但可以通过集成第三方库(如lumberjack
)来实现日志文件的自动轮转。 -
并发安全 :
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游戏服务器