Skip to content

日志系统 ​

Cherry 的日志基于 zap 封装,支持多份命名配置、按节点切换、文件轮转和结构化输出。

import clog "github.com/cherry-game/cherry/logger"

关于包名与别名

包名是 cherryLogger,框架内部和示例里普遍用 clog 作为 import 别名。别名不是强制的,cherryLogger 也能用,但为了和框架源码、示例保持一致,建议统一用 clog。

基本用法 ​

包级函数直接可用,不需要任何初始化:

go
clog.Debug("调试信息")
clog.Infof("[nodeID = %s] 服务启动中...", app.NodeID())
clog.Warnf("[component = %s] OnStop 出错: %s", name, err)

// 结构化日志:kv 以空格分隔输出
clog.Infow("请求失败", "url", url, "status", 500)

三族方法覆盖 7 个级别:

级别无格式格式化结构化
DebugDebugDebugfDebugw
InfoInfoInfofInfow
WarnWarnWarnfWarnw
ErrorErrorErrorfErrorw
DPanicDPanicDPanicfDPanicw
PanicPanicPanicfPanicw
FatalFatalFatalfFatalw

Panic 与 Fatal 的行为

Panic 记录后 panic,Fatal 记录后调用 os.Exit(1)。框架在配置缺失等致命场景会直接 Panicf——比如 db 配置段缺失、logger 段找不到都会让进程起不来。

默认 logger ​

有两层初始化,理解这点可以避免「没有配置时日志去哪了」的困惑。

第一层:包加载期(无需配置) ​

logger 包被 import 时就构建了一个写往标准错误的控制台 logger:

var defaultManager = NewManager()                        // manager.go
m.defaultLogger = NewConfigLogger(defaultConsoleConfig()) // NewManager 内部
var DefaultLogger = defaultManager.DefaultLogger()        // logger.go

所以在任何配置加载之前,clog.Info(...) 就已经可用,输出到 stderr,级别 debug。这让框架早期启动阶段也能打日志。

第二层:应用启动期(读取 profile) ​

cherry.Configure(...) 时框架会做两件事:

go
clog.SetPrintLevel(clog.GetLevel(cprofile.PrintLevel()))
clog.SetNodeLogger(node)

SetNodeLogger 读取当前节点的 ref_logger,如果非空,就用 profile 里对应的配置替换 DefaultLogger。

按节点切换日志配置 ​

这是 Cherry 日志的核心机制:每个节点可以引用不同的日志配置。

链路是:

profile.json → node.<type>[].__settings__.ref_logger
             → GetOrCreateLogger(name)
             → NewConfigWithName(name)
             → profile 里的 logger.<name> 段

配置文件:

json
{
    "node": {
        "gate": [
            {
                "node_id": "^gc-gate-\\d+$",
                "enabled": true,
                "__settings__": {
                    "ref_logger": "gate_log"
                }
            }
        ]
    },

    "logger": {
        "gate_log": {
            "level": "debug",
            "encoder_type": "console",
            "enable_console": true,
            "enable_write_file": true,
            "file_link_path": "./logs/gate.log",
            "file_path_format": "./logs/%Y%m%d/gate.log",
            "max_age": 7,
            "rotation_time": 86400
        }
    }
}

于是 gate 节点用 gate_log、game 节点用 game_log,各自的级别、输出位置、保留策略互不影响。

文件名变量替换

file_link_path 和 file_path_format 里可以用两个占位符,框架会按当前节点替换:

  • %nodetype — 节点类型
  • %nodeid — 节点 id

比如 ./logs/%nodetype/%nodeid.log,gate 节点写出来就是 ./logs/gate/gc-gate-1.log。多节点共享一份配置时很有用。

业务代码取命名 logger ​

除了包级的 DefaultLogger,也可以按名字单独取一份:

go
logger := clog.NewLogger("game_log")
logger.Infof("这条日志走 game_log 配置")

同名会命中缓存返回同一个实例。

配置字段 ​

logger.<name> 段的全部字段:

字段类型默认值说明
levelstringdebug最低启用级别
stack_levelstringerror记录堆栈的最低级别
encoder_typestringconsole编码器:console(kv)或 json
enable_consolebooltrue同步写 stderr
enable_write_fileboolfalse启用文件输出与轮转
max_ageint7轮转文件保留天数
time_formatstring15:04:05.000时间戳 layout
print_callerbooltrue输出调用位置 file:line
rotation_timeint86400轮转间隔,单位秒
file_link_pathstringlogs/debug.log指向当前日志的软链接
file_path_formatstringlogs/debug_%Y%m%d%H%M.log轮转文件命名,strftime 语法
include_stdoutboolfalse额外挂载 stdout
include_stderrboolfalse额外挂载 stderr(与 enable_console 同开时去重)

级别拼错会静默变成 debug

GetLevel 对无法识别的级别值返回 DebugLevel。把 info 写成 inf 不会报错,只会让你的日志突然变得很吵,排查时容易被误导。

输出格式 ​

encoder_type: console 时用框架自定义的 KV 编码器,字段顺序固定:

10:20:30.123	INFO	/path/to/main.go:20	nodeid=game-1 nodetype=game	请求失败	url=http://example.com status=500
└─────┬────┘	└─┬┘	└───────┬───────┘	└──────────┬──────────┘	└──┬─┘	└────────────┬────────────┘
     时间      级别	  调用位置	       公共字段(空格分隔)	        消息	    动态字段(kv)

头部分段之间用 TAB 连接,kv 段内部用空格连接。

  • 公共字段:nodeid / nodetype 由框架自动注入,方便多节点日志汇总后区分来源
  • 动态字段:Infow / Errorw 传入的键值对
  • 堆栈(触发 stack_level 时)会换行追加在最后

改成 encoder_type: json 则输出 JSON 格式,适合接入 ELK 等日志系统。

多 logger 与自定义 ​

底层的 Manager 维护一个按名字缓存的 logger 表,支持并存多个:

go
manager := clog.NewManager()
lg := manager.GetOrCreateLogger("my_log", zap.AddCallerSkip(1))

包级的 defaultManager 就是这么一个实例。需要隔离的场景(单元测试、一个进程多个 Application)可以自建 Manager。

还可以注册 Wrapper 在构建时包裹日志 core,用于采样、过滤或埋点:

go
clog.RegisterWrapper(myWrapper)

其他常用函数 ​

函数说明
NewLogger(name, opts...)按名字创建 / 获取 logger
SetFileNameVar(key, value)设置文件名变量(%nodetype / %nodeid)
SetCommonField(key, value)追加公共字段
SetCommonFields(map)批量追加公共字段
Flush()刷出缓冲
Enable(level)判断某级别是否启用
PrintLevel(level) / SetPrintLevel(level)控制台打印级别
GetLevel(string)字符串转级别

进程退出前 Flush

写文件的场景下,退出前调用 clog.Flush() 保证缓冲落盘。框架在正常关闭流程里会处理,但自己 os.Exit 的地方需要留意。

文件轮转 ​

文件输出由内置的 rotatelogs 负责(logger/rotatelogs,file-rotatelogs 的移植版),框架把它接到 zap 的写入器上:

go
hook, err := rotatelogs.New(
	config.FilePathFormat,
	rotatelogs.WithLinkName(config.FileLinkPath),
	rotatelogs.WithMaxAge(time.Hour*24*time.Duration(config.MaxAge)),
	rotatelogs.WithRotationTime(time.Second*time.Duration(config.RotationTime)),
)

配置字段与轮转参数的对应关系:

配置对应
file_path_format轮转文件命名模式
file_link_path指向最新日志的软链接名
rotation_time切割周期(秒)
max_age保留天数

rotatelogs 本身还支持按大小、按文件数保留(WithRotationSize / WithRotationCount),但框架目前没有暴露这两个配置项。需要的话得自己接。

完整示例 ​

go
package main

import (
	"github.com/cherry-game/cherry"
	clog "github.com/cherry-game/cherry/logger"
)

func main() {
	app := cherry.Configure("./config/profile.json", "game-1", false, cherry.Cluster)

	clog.Infof("节点 %s 启动", app.NodeID())
	clog.Infow("连接数据库",
		"host", "127.0.0.1",
		"db", "dev_game",
	)

	app.Startup()

	clog.Info("节点退出")
	clog.Flush()
}