日志系统
Cherry 的日志基于 zap 封装,支持多份命名配置、按节点切换、文件轮转和结构化输出。
import clog "github.com/cherry-game/cherry/logger"关于包名与别名
包名是 cherryLogger,框架内部和示例里普遍用 clog 作为 import 别名。别名不是强制的,cherryLogger 也能用,但为了和框架源码、示例保持一致,建议统一用 clog。
基本用法
包级函数直接可用,不需要任何初始化:
clog.Debug("调试信息")
clog.Infof("[nodeID = %s] 服务启动中...", app.NodeID())
clog.Warnf("[component = %s] OnStop 出错: %s", name, err)
// 结构化日志:kv 以空格分隔输出
clog.Infow("请求失败", "url", url, "status", 500)三族方法覆盖 7 个级别:
| 级别 | 无格式 | 格式化 | 结构化 |
|---|---|---|---|
| Debug | Debug | Debugf | Debugw |
| Info | Info | Infof | Infow |
| Warn | Warn | Warnf | Warnw |
| Error | Error | Errorf | Errorw |
| DPanic | DPanic | DPanicf | DPanicw |
| Panic | Panic | Panicf | Panicw |
| Fatal | Fatal | Fatalf | Fatalw |
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(...) 时框架会做两件事:
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> 段配置文件:
{
"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,也可以按名字单独取一份:
logger := clog.NewLogger("game_log")
logger.Infof("这条日志走 game_log 配置")同名会命中缓存返回同一个实例。
配置字段
logger.<name> 段的全部字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
level | string | debug | 最低启用级别 |
stack_level | string | error | 记录堆栈的最低级别 |
encoder_type | string | console | 编码器:console(kv)或 json |
enable_console | bool | true | 同步写 stderr |
enable_write_file | bool | false | 启用文件输出与轮转 |
max_age | int | 7 | 轮转文件保留天数 |
time_format | string | 15:04:05.000 | 时间戳 layout |
print_caller | bool | true | 输出调用位置 file:line |
rotation_time | int | 86400 | 轮转间隔,单位秒 |
file_link_path | string | logs/debug.log | 指向当前日志的软链接 |
file_path_format | string | logs/debug_%Y%m%d%H%M.log | 轮转文件命名,strftime 语法 |
include_stdout | bool | false | 额外挂载 stdout |
include_stderr | bool | false | 额外挂载 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 表,支持并存多个:
manager := clog.NewManager()
lg := manager.GetOrCreateLogger("my_log", zap.AddCallerSkip(1))包级的 defaultManager 就是这么一个实例。需要隔离的场景(单元测试、一个进程多个 Application)可以自建 Manager。
还可以注册 Wrapper 在构建时包裹日志 core,用于采样、过滤或埋点:
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 的写入器上:
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),但框架目前没有暴露这两个配置项。需要的话得自己接。
完整示例
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()
}