当前位置: 首页 > news >正文

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 中使用而不需要额外的同步措施。

下面是一个简单的例子:

package mainimport ("github.com/sirupsen/logrus"
)func main() {// 创建一个新的Loggerlogger := logrus.New()// 设置日志输出格式为JSONlogger.Formatter = &logrus.JSONFormatter{}// 设置日志级别logger.Level = logrus.InfoLevel// 记录一条信息级别的日志logger.Info("这是一条信息级别的日志")// 记录一条错误级别的日志logger.Error("这是一条错误级别的日志")// 记录带有字段的结构化日志logger.WithFields(logrus.Fields{"animal":

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

3.合理的日志分类

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

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

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的表,有统一的格式),例如可以用下面的格式:

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.系统日志代码

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.InfoLevelreturn logger
}

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

type Formatter interface {Format(*Entry) ([]byte, error)
}

代码如下:

type consoleLogFormatter struct{}// Format 实现 logrus.Formatter 接口的 Format 方法。
func (f *consoleLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {var b *bytes.Bufferif 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
}

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

// Printf 记录一条日志
func Printf(v string) {consoleLog.Info(v)
}

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

3.2.运营日志代码

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

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

type LogType int// 日志类型枚举,每一个类型对应独立的文件
const (Admin LogType = iotaAPPLICATION
)var logName = map[LogType]string{Admin:       "admin",APPLICATION: "application",
}var (logs       map[string]*logrus.Logger
)

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

type businessLogFormatter struct{}// Format 实现 logrus.Formatter 接口的 Format 方法。
func (f *businessLogFormatter) Format(entry *logrus.Entry) ([]byte, error) {var b *bytes.Bufferif 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.InfoLevelreturn logger
}

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

func init() {logs = make(map[string]*logrus.Logger)for _, logType := range logName {logger := createBusinessLog(logType)logs[logType] = logger}
}

日志打印接口

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。调用代码示例:

log.Info(log.APPLICATION, "key1", "value1", "key2", "value2", "key3", "value3")

完整代码请移步:

--> go游戏服务器

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 【Go】-Gin框架
  • 什么是Aware注入?
  • Python之异常处理与程序调试(Exception Handling and Program Debugging in Python)
  • 银河麒麟v10-sp3 -x86系统创建新分区扩展lvm
  • java基础-IO(6)转换流InputStreamReader、OutputStreamWriter
  • 内存分区学习
  • Qt使用绿色pdf阅读器打开文件
  • java spring定时任务-动态任务
  • 认知杂谈59《实力为王:用硬本事赢得尊重,开启人生逆袭路》
  • Python3中函数的用法
  • linux-用户与权限管理-组管理
  • 防患于未然,智能监控新视角:EasyCVR视频平台在高校安全防控中的关键角色
  • 一维稳态与非稳态导热的详细分析
  • 通信工程学习:什么是IP-CAN(IP连接接入网)
  • ETL_场景练习
  • 5分钟即可掌握的前端高效利器:JavaScript 策略模式
  • Apache的80端口被占用以及访问时报错403
  • canvas 高仿 Apple Watch 表盘
  • classpath对获取配置文件的影响
  • JavaScript服务器推送技术之 WebSocket
  • JAVA之继承和多态
  • SQLServer之创建数据库快照
  • web标准化(下)
  • 发布国内首个无服务器容器服务,运维效率从未如此高效
  • 浮现式设计
  • 简单基于spring的redis配置(单机和集群模式)
  • 三分钟教你同步 Visual Studio Code 设置
  • 适配mpvue平台的的微信小程序日历组件mpvue-calendar
  • 微服务入门【系列视频课程】
  • 我从编程教室毕业
  • 协程
  • 云栖大讲堂Java基础入门(三)- 阿里巴巴Java开发手册介绍
  • 正则学习笔记
  • ()、[]、{}、(())、[[]]命令替换
  • (06)金属布线——为半导体注入生命的连接
  • (C语言)共用体union的用法举例
  • (补充):java各种进制、原码、反码、补码和文本、图像、音频在计算机中的存储方式
  • (附源码)springboot高校宿舍交电费系统 毕业设计031552
  • (附源码)ssm考生评分系统 毕业设计 071114
  • (黑马出品_高级篇_01)SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式
  • (三分钟)速览传统边缘检测算子
  • (算法设计与分析)第一章算法概述-习题
  • (转)JVM内存分配 -Xms128m -Xmx512m -XX:PermSize=128m -XX:MaxPermSize=512m
  • (转)winform之ListView
  • ***详解账号泄露:全球约1亿用户已泄露
  • .gitignore文件使用
  • .net 重复调用webservice_Java RMI 远程调用详解,优劣势说明
  • .net/c# memcached 获取所有缓存键(keys)
  • .NET学习教程二——.net基础定义+VS常用设置
  • []我的函数库
  • [AIGC] 使用Curl进行网络请求的常见用法
  • [Android] Amazon 的 android 音视频开发文档
  • [AS3]URLLoader+URLRequest+JPGEncoder实现BitmapData图片数据保存
  • [C++]类和对象【上篇】
  • [Effective C++读书笔记]0012_复制对象时勿忘其每一部分