实现统一模型 ID 格式 (provider_id/model_name),支持跨协议模型标识和 Smart Passthrough。 核心变更: - 新增 pkg/modelid 包:解析、格式化、校验统一模型 ID - 数据库迁移:models 表使用 UUID 主键 + UNIQUE(provider_id, model_name) 约束 - Repository 层:FindByProviderAndModelName、ListEnabled 方法 - Service 层:联合唯一校验、provider ID 字符集校验 - Conversion 层:ExtractModelName、RewriteRequestModelName/RewriteResponseModelName 方法 - Handler 层:统一模型 ID 路由、Smart Passthrough、Models API 本地聚合 - 新增 error-responses、unified-model-id 规范 测试覆盖: - 单元测试:modelid、conversion、handler、service、repository - 集成测试:统一模型 ID 路由、Smart Passthrough 保真性、跨协议转换 - 迁移测试:UUID 主键、UNIQUE 约束、级联删除 OpenSpec: - 归档 unified-model-id 变更到 archive/2026-04-21-unified-model-id - 同步 11 个 delta specs 到 main specs - 新增 error-responses、unified-model-id 规范文件
118 lines
3.3 KiB
Go
118 lines
3.3 KiB
Go
package logger
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"time"
|
||
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
)
|
||
|
||
// stdoutWriter 包装 os.Stdout,忽略 Sync() 错误。
|
||
// 在非 TTY 环境(如 go test)中,os.Stdout 被重定向为 pipe,
|
||
// 底层 fsync 会返回 "bad file descriptor"。zap 社区标准做法。
|
||
type stdoutWriter struct{}
|
||
|
||
func (stdoutWriter) Write(p []byte) (int, error) { return os.Stdout.Write(p) }
|
||
func (stdoutWriter) Sync() error { return nil }
|
||
|
||
// Config 日志配置
|
||
type Config struct {
|
||
Level string // 日志级别: debug, info, warn, error
|
||
Path string // 日志文件目录,为空则仅输出到 stdout
|
||
MaxSize int // 单个日志文件最大尺寸 (MB)
|
||
MaxBackups int // 保留的旧日志文件最大数量
|
||
MaxAge int // 保留旧日志文件的最大天数
|
||
Compress bool // 是否压缩旧日志文件
|
||
}
|
||
|
||
// New 根据配置创建 zap.Logger
|
||
// 如果 Path 为空,仅输出到 stdout;
|
||
// 如果 Path 已设置,同时输出到 stdout 和文件(文件使用 JSON 格式,stdout 使用 console 格式)
|
||
func New(cfg Config) (*zap.Logger, error) {
|
||
level, err := parseLevel(cfg.Level)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// stdout encoder — console 格式
|
||
stdoutEncoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
|
||
TimeKey: "ts",
|
||
LevelKey: "level",
|
||
NameKey: "logger",
|
||
CallerKey: "caller",
|
||
FunctionKey: zapcore.OmitKey,
|
||
MessageKey: "msg",
|
||
StacktraceKey: "stacktrace",
|
||
LineEnding: zapcore.DefaultLineEnding,
|
||
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||
EncodeDuration: zapcore.StringDurationEncoder,
|
||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||
})
|
||
|
||
stdoutCore := zapcore.NewCore(
|
||
stdoutEncoder,
|
||
zapcore.AddSync(stdoutWriter{}),
|
||
level,
|
||
)
|
||
|
||
// 仅 stdout 模式
|
||
if cfg.Path == "" {
|
||
return zap.New(stdoutCore, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)), nil
|
||
}
|
||
|
||
// 文件 encoder — JSON 格式
|
||
fileEncoder := zapcore.NewJSONEncoder(zapcore.EncoderConfig{
|
||
TimeKey: "ts",
|
||
LevelKey: "level",
|
||
NameKey: "logger",
|
||
CallerKey: "caller",
|
||
FunctionKey: zapcore.OmitKey,
|
||
MessageKey: "msg",
|
||
StacktraceKey: "stacktrace",
|
||
LineEnding: zapcore.DefaultLineEnding,
|
||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||
EncodeDuration: zapcore.StringDurationEncoder,
|
||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||
})
|
||
|
||
rotateWriter := newRotateWriter(cfg)
|
||
fileCore := zapcore.NewCore(
|
||
fileEncoder,
|
||
zapcore.AddSync(rotateWriter),
|
||
level,
|
||
)
|
||
|
||
core := zapcore.NewTee(stdoutCore, fileCore)
|
||
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel)), nil
|
||
}
|
||
|
||
// parseLevel 将字符串解析为 zapcore.Level
|
||
func parseLevel(s string) (zapcore.Level, error) {
|
||
switch s {
|
||
case "debug":
|
||
return zapcore.DebugLevel, nil
|
||
case "info":
|
||
return zapcore.InfoLevel, nil
|
||
case "warn":
|
||
return zapcore.WarnLevel, nil
|
||
case "error":
|
||
return zapcore.ErrorLevel, nil
|
||
default:
|
||
return zapcore.InfoLevel, nil
|
||
}
|
||
}
|
||
|
||
// logFileName 生成当日日志文件名: nex-YYYY-MM-DD.log
|
||
func logFileName() string {
|
||
return "nex-" + time.Now().Format("2006-01-02") + ".log"
|
||
}
|
||
|
||
// logFilePath 拼接完整日志文件路径
|
||
func logFilePath(dir string) string {
|
||
return filepath.Join(dir, logFileName())
|
||
}
|