新增日志模块
This commit is contained in:
@@ -4,9 +4,23 @@ import (
|
||||
"os"
|
||||
|
||||
"github.com/user/devpack/cmd/devpack/commands"
|
||||
"github.com/user/devpack/internal/logging"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// 初始化日志
|
||||
logOpts := logging.Options{
|
||||
Level: "info",
|
||||
LogFile: "devpack.log",
|
||||
Verbose: false,
|
||||
Quiet: false,
|
||||
NoColor: false,
|
||||
}
|
||||
_, err := logging.Init(logOpts)
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if err := commands.Execute(); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
3
go.mod
3
go.mod
@@ -12,8 +12,11 @@ require (
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
|
||||
14
go.sum
14
go.sum
@@ -1,3 +1,4 @@
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -7,6 +8,7 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
@@ -19,15 +21,24 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
|
||||
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
@@ -61,6 +72,9 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||
|
||||
219
internal/logging/logger.go
Normal file
219
internal/logging/logger.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Logger 封装 zerolog,提供全局日志能力
|
||||
type Logger struct {
|
||||
zl zerolog.Logger // 底层 zerolog 实例
|
||||
level zerolog.Level // 当前日志级别
|
||||
logFile *os.File // 日志文件句柄(可选)
|
||||
quiet bool // 静默模式:不输出到控制台
|
||||
verbose bool // 详细模式:启用 Debug 级别
|
||||
}
|
||||
|
||||
// Options 创建 Logger 时的配置选项
|
||||
type Options struct {
|
||||
Level string // 日志级别: trace|debug|info|warn|error|fatal (默认 info)
|
||||
LogFile string // 日志文件路径(空则不写文件)
|
||||
Verbose bool // 详细模式,覆盖 Level 为 debug
|
||||
Quiet bool // 静默模式,禁用控制台输出
|
||||
NoColor bool // 禁用彩色输出
|
||||
}
|
||||
|
||||
// 全局 Logger 实例,初始化前使用零值安全地丢弃日志
|
||||
var global = &Logger{zl: zerolog.Nop()}
|
||||
|
||||
// G 返回全局 Logger 实例
|
||||
func G() *Logger {
|
||||
return global
|
||||
}
|
||||
|
||||
// Init 根据选项初始化全局 Logger,应在程序启动时调用一次
|
||||
func Init(opts Options) (*Logger, error) {
|
||||
logger, err := New(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
global = logger
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// New 创建一个新的 Logger 实例
|
||||
func New(opts Options) (*Logger, error) {
|
||||
// 解析日志级别
|
||||
level := zerolog.InfoLevel
|
||||
if opts.Verbose {
|
||||
level = zerolog.DebugLevel
|
||||
} else if opts.Level != "" {
|
||||
parsed, err := zerolog.ParseLevel(opts.Level)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的日志级别 %q: %w", opts.Level, err)
|
||||
}
|
||||
level = parsed
|
||||
}
|
||||
|
||||
// 构建输出目标(Writers)
|
||||
var writers []io.Writer
|
||||
|
||||
// 控制台输出(非静默模式时启用)
|
||||
if !opts.Quiet {
|
||||
console := zerolog.ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: opts.NoColor,
|
||||
}
|
||||
writers = append(writers, console)
|
||||
}
|
||||
|
||||
// 文件输出(指定了日志文件时启用)
|
||||
var logFile *os.File
|
||||
if opts.LogFile != "" {
|
||||
dir := filepath.Dir(opts.LogFile)
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return nil, fmt.Errorf("无法创建日志目录 %s: %w", dir, err)
|
||||
}
|
||||
f, err := os.OpenFile(opts.LogFile, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无法打开日志文件 %s: %w", opts.LogFile, err)
|
||||
}
|
||||
logFile = f
|
||||
// 文件始终使用 JSON 格式,不带颜色
|
||||
writers = append(writers, f)
|
||||
}
|
||||
|
||||
// 若无任何输出目标,丢弃所有日志
|
||||
var writer io.Writer
|
||||
switch len(writers) {
|
||||
case 0:
|
||||
writer = io.Discard
|
||||
case 1:
|
||||
writer = writers[0]
|
||||
default:
|
||||
writer = zerolog.MultiLevelWriter(writers...)
|
||||
}
|
||||
|
||||
zl := zerolog.New(writer).
|
||||
Level(level).
|
||||
With().
|
||||
Timestamp().
|
||||
Logger()
|
||||
|
||||
return &Logger{
|
||||
zl: zl,
|
||||
level: level,
|
||||
logFile: logFile,
|
||||
quiet: opts.Quiet,
|
||||
verbose: opts.Verbose,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 关闭日志文件句柄(如果有的话),应在程序退出时调用
|
||||
func (l *Logger) Close() {
|
||||
if l.logFile != nil {
|
||||
_ = l.logFile.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 各级别日志方法 ----
|
||||
|
||||
// Trace 输出 Trace 级别日志
|
||||
func (l *Logger) Trace(msg string) {
|
||||
l.zl.Trace().Msg(msg)
|
||||
}
|
||||
|
||||
// Tracef 输出格式化 Trace 级别日志
|
||||
func (l *Logger) Tracef(format string, args ...any) {
|
||||
l.zl.Trace().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// Debug 输出 Debug 级别日志
|
||||
func (l *Logger) Debug(msg string) {
|
||||
l.zl.Debug().Msg(msg)
|
||||
}
|
||||
|
||||
// Debugf 输出格式化 Debug 级别日志
|
||||
func (l *Logger) Debugf(format string, args ...any) {
|
||||
l.zl.Debug().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// Info 输出 Info 级别日志
|
||||
func (l *Logger) Info(msg string) {
|
||||
l.zl.Info().Msg(msg)
|
||||
}
|
||||
|
||||
// Infof 输出格式化 Info 级别日志
|
||||
func (l *Logger) Infof(format string, args ...any) {
|
||||
l.zl.Info().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// Warn 输出 Warn 级别日志
|
||||
func (l *Logger) Warn(msg string) {
|
||||
l.zl.Warn().Msg(msg)
|
||||
}
|
||||
|
||||
// Warnf 输出格式化 Warn 级别日志
|
||||
func (l *Logger) Warnf(format string, args ...any) {
|
||||
l.zl.Warn().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// Error 输出 Error 级别日志
|
||||
func (l *Logger) Error(msg string) {
|
||||
l.zl.Error().Msg(msg)
|
||||
}
|
||||
|
||||
// Errorf 输出格式化 Error 级别日志
|
||||
func (l *Logger) Errorf(format string, args ...any) {
|
||||
l.zl.Error().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// Fatal 输出 Fatal 级别日志并终止程序
|
||||
func (l *Logger) Fatal(msg string) {
|
||||
l.zl.Fatal().Msg(msg)
|
||||
}
|
||||
|
||||
// Fatalf 输出格式化 Fatal 级别日志并终止程序
|
||||
func (l *Logger) Fatalf(format string, args ...any) {
|
||||
l.zl.Fatal().Msgf(format, args...)
|
||||
}
|
||||
|
||||
// ---- 带字段的结构化日志 ----
|
||||
|
||||
// WithField 附加一个键值对字段,返回可链式调用的事件
|
||||
func (l *Logger) WithField(key string, value any) *zerolog.Event {
|
||||
return l.zl.Info().Any(key, value)
|
||||
}
|
||||
|
||||
// WithError 附加一个错误对象,返回 Error 级别事件
|
||||
func (l *Logger) WithError(err error) *zerolog.Event {
|
||||
return l.zl.Error().Err(err)
|
||||
}
|
||||
|
||||
// WithDuration 附加一个耗时字段,常用于性能日志
|
||||
func (l *Logger) WithDuration(d time.Duration) *zerolog.Event {
|
||||
return l.zl.Info().Dur("duration", d)
|
||||
}
|
||||
|
||||
// ---- 工具方法 ----
|
||||
|
||||
// IsDebug 返回当前是否启用了 Debug 级别
|
||||
func (l *Logger) IsDebug() bool {
|
||||
return l.level <= zerolog.DebugLevel
|
||||
}
|
||||
|
||||
// IsQuiet 返回当前是否处于静默模式
|
||||
func (l *Logger) IsQuiet() bool {
|
||||
return l.quiet
|
||||
}
|
||||
|
||||
// IsVerbose 返回当前是否处于详细模式
|
||||
func (l *Logger) IsVerbose() bool {
|
||||
return l.verbose
|
||||
}
|
||||
1
internal/logging/logger_test.go
Normal file
1
internal/logging/logger_test.go
Normal file
@@ -0,0 +1 @@
|
||||
package logging
|
||||
Reference in New Issue
Block a user