commit 107dde65bfb9ad9d37a4549019b42dd76eff9129 Author: Chen Chi Date: Sun Sep 1 00:03:12 2024 +0800 chore: Add initial project files and dependencies diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fe7afda --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> Go +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# log files +*.log +# zip files +*.zip \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d5075a3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,17 @@ +Permission is hereby granted, without written agreement and without +license or royalty fees, to use, copy, modify, and distribute this +software and its documentation for any purpose, provided that the +above copyright notice and the following two paragraphs appear in +all copies of this software. + +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR +DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES +ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN +IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. + +THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, +BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS +ON AN "AS IS" BASIS, AND THE COPYRIGHT HOLDER HAS NO OBLIGATION TO +PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. diff --git a/README.md b/README.md new file mode 100644 index 0000000..11c489d --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# LogX + +logx 是一个基于 `log/slog` 的日志库,提供了控制台输出、文件输出、日志级别控制等功能。 + +## 安装 + +```bash +go get -u github.com/yeqown/logx +``` + +## 使用 + +```go diff --git a/console_handler.go b/console_handler.go new file mode 100644 index 0000000..16f7224 --- /dev/null +++ b/console_handler.go @@ -0,0 +1,57 @@ +package logx + +import ( + "io" + "log/slog" + "os" + + "github.com/fatih/color" +) + +type ConsoleHandler struct { + slog.Handler + w io.Writer +} + +func NewConsoleHandler(level slog.Level) *ConsoleHandler { + return &ConsoleHandler{ + Handler: slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ + Level: level, + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if a.Key == slog.LevelKey { + level := a.Value.Any().(slog.Level) + switch level { + case slog.LevelDebug: + a.Value = slog.StringValue(color.BlueString(level.String())) + case slog.LevelInfo: + a.Value = slog.StringValue(color.GreenString(level.String())) + case slog.LevelWarn: + a.Value = slog.StringValue(color.YellowString(level.String())) + case slog.LevelError: + a.Value = slog.StringValue(color.RedString(level.String())) + } + } + return a + }, + }), + w: os.Stdout, + } +} + +func (h *ConsoleHandler) Close() error { + return nil +} + +func (h *ConsoleHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &ConsoleHandler{ + Handler: h.Handler.WithAttrs(attrs), + w: h.w, + } +} + +func WithConsoleHandler(level slog.Level) Option { + return func(l *Logger) { + h := NewConsoleHandler(level) + l.handlers = append(l.handlers, h) + } +} diff --git a/example/example.go b/example/example.go new file mode 100644 index 0000000..9d1fae8 --- /dev/null +++ b/example/example.go @@ -0,0 +1,91 @@ +package main + +import ( + "context" + "log/slog" + "time" + + "gitea.cccvno1.com/chenchi/logx" +) + +func main() { + // 创建一个新的日志记录器 + logger := logx.New( + logx.WithConsoleHandler(slog.LevelDebug), + logx.WithFileHandler("app.log", 10, 5, 30, true, slog.LevelInfo), + logx.WithContextExtractor(customContextExtractor), + ) + + // 设置全局日志记录器 + logx.SetGlobalLogger(logger) + + // 使用全局日志记录器 + logx.Info("Application started") + + // 创建一个带有跟踪ID的上下文 + ctx := context.WithValue(context.Background(), "trace_id", "abc123") + + // 使用带有上下文的日志记录器 + contextLogger := logx.WithContext(ctx) + contextLogger.Debug("This is a debug message with context") + contextLogger.Info("This is an info message with context") + contextLogger.Warn("This is a warning message with context") + contextLogger.Error("This is an error message with context") + + // 使用不同的日志级别 + logx.Debug("This is a debug message") + logx.Info("This is an info message") + logx.Warn("This is a warning message") + logx.Error("This is an error message") + + // 使用结构化日志 + logx.Info("User logged in", + slog.String("username", "john_doe"), + slog.Int("user_id", 12345), + slog.Time("login_time", time.Now()), + ) + + // 模拟一个错误情况 + if err := simulateError(); err != nil { + logx.Error("An error occurred", slog.String("error", err.Error())) + } + + // 关闭日志记录器 + if err := logger.Close(); err != nil { + logx.Error("Failed to close logger", slog.String("error", err.Error())) + } +} + +func customContextExtractor(ctx context.Context) []slog.Attr { + return []slog.Attr{ + slog.String("trace_id", getTraceID(ctx)), + slog.String("user_id", getUserID(ctx)), + } +} + +func getTraceID(ctx context.Context) string { + if traceID, ok := ctx.Value("trace_id").(string); ok { + return traceID + } + return "" +} + +func getUserID(ctx context.Context) string { + if userID, ok := ctx.Value("user_id").(string); ok { + return userID + } + return "" +} + +func simulateError() error { + // 模拟一个错误 + return &customError{message: "Something went wrong"} +} + +type customError struct { + message string +} + +func (e *customError) Error() string { + return e.message +} diff --git a/file_handler.go b/file_handler.go new file mode 100644 index 0000000..45bde55 --- /dev/null +++ b/file_handler.go @@ -0,0 +1,40 @@ +package logx + +import ( + "log/slog" + + "gopkg.in/natefinch/lumberjack.v2" +) + +type FileHandler struct { + slog.JSONHandler + logger *lumberjack.Logger +} + +func WithFileHandler(path string, maxSize int, maxBackups int, maxAge int, compress bool, level slog.Level) Option { + return func(l *Logger) { + logger := &lumberjack.Logger{ + Filename: path, + MaxSize: maxSize, + MaxBackups: maxBackups, + MaxAge: maxAge, + Compress: compress, + } + h := &FileHandler{ + JSONHandler: *slog.NewJSONHandler(logger, &slog.HandlerOptions{Level: level}), + logger: logger, + } + l.handlers = append(l.handlers, h) + } +} + +func (h *FileHandler) Close() error { + return h.logger.Close() +} + +func (h *FileHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return &FileHandler{ + JSONHandler: *h.JSONHandler.WithAttrs(attrs).(*slog.JSONHandler), + logger: h.logger, + } +} diff --git a/global.go b/global.go new file mode 100644 index 0000000..d8f79e4 --- /dev/null +++ b/global.go @@ -0,0 +1,54 @@ +package logx + +import ( + "context" + "sync" +) + +var ( + globalLogger *Logger + once sync.Once +) + +func SetGlobalLogger(l *Logger) { + once.Do(func() { + globalLogger = l + }) +} + +func WithContext(ctx context.Context) *Logger { + if globalLogger != nil { + return globalLogger.WithContext(ctx) + } + return nil +} + +func Debug(msg string, args ...any) { + if globalLogger != nil { + globalLogger.Debug(msg, args...) + } +} + +func Info(msg string, args ...any) { + if globalLogger != nil { + globalLogger.Info(msg, args...) + } +} + +func Warn(msg string, args ...any) { + if globalLogger != nil { + globalLogger.Warn(msg, args...) + } +} + +func Error(msg string, args ...any) { + if globalLogger != nil { + globalLogger.Error(msg, args...) + } +} + +func Fatal(msg string, args ...any) { + if globalLogger != nil { + globalLogger.Fatal(msg, args...) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..afdf59c --- /dev/null +++ b/go.mod @@ -0,0 +1,14 @@ +module gitea.cccvno1.com/chenchi/logx + +go 1.23.0 + +require ( + github.com/fatih/color v1.17.0 + gopkg.in/natefinch/lumberjack.v2 v2.2.1 +) + +require ( + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + golang.org/x/sys v0.18.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..009d851 --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +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.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= diff --git a/logger.go b/logger.go new file mode 100644 index 0000000..c6be2b5 --- /dev/null +++ b/logger.go @@ -0,0 +1,115 @@ +package logx + +import ( + "context" + "log/slog" + "os" + "time" +) + +type Logger struct { + handlers []Handler + ctxExtractor ContextExtractor +} + +type Handler interface { + slog.Handler + Close() error +} + +type Option func(*Logger) + +type ContextExtractor func(context.Context) []slog.Attr + +func New(opts ...Option) *Logger { + l := &Logger{ + ctxExtractor: defaultContextExtractor, + } + for _, opt := range opts { + opt(l) + } + return l +} + +func (l *Logger) WithContext(ctx context.Context) *Logger { + newLogger := &Logger{ + handlers: make([]Handler, len(l.handlers)), + ctxExtractor: l.ctxExtractor, + } + attrs := l.ctxExtractor(ctx) + for i, h := range l.handlers { + newHandler := h.WithAttrs(attrs) + if handlerWithClose, ok := newHandler.(Handler); ok { + newLogger.handlers[i] = handlerWithClose + } else { + newLogger.handlers[i] = &handlerWrapper{newHandler} + } + } + return newLogger +} + +type handlerWrapper struct { + slog.Handler +} + +func (h *handlerWrapper) Close() error { + return nil +} + +func (l *Logger) Debug(msg string, args ...any) { + l.log(slog.LevelDebug, msg, args...) +} + +func (l *Logger) Info(msg string, args ...any) { + l.log(slog.LevelInfo, msg, args...) +} + +func (l *Logger) Warn(msg string, args ...any) { + l.log(slog.LevelWarn, msg, args...) +} + +func (l *Logger) Error(msg string, args ...any) { + l.log(slog.LevelError, msg, args...) +} + +func (l *Logger) Fatal(msg string, args ...any) { + l.log(slog.LevelError, msg, args...) + os.Exit(1) +} + +func (l *Logger) log(level slog.Level, msg string, args ...any) { + for _, h := range l.handlers { + if !h.Enabled(context.Background(), level) { + continue + } + r := slog.NewRecord(time.Now(), level, msg, 0) + r.Add(args...) + _ = h.Handle(context.Background(), r) + } +} + +func (l *Logger) Close() error { + for _, h := range l.handlers { + if err := h.Close(); err != nil { + return err + } + } + return nil +} + +func WithContextExtractor(extractor ContextExtractor) Option { + return func(l *Logger) { + l.ctxExtractor = extractor + } +} + +func defaultContextExtractor(ctx context.Context) []slog.Attr { + return []slog.Attr{ + slog.String("trace_id", getTraceID(ctx)), + } +} + +func getTraceID(ctx context.Context) string { + traceID := ctx.Value("trace_id") + return traceID.(string) +}