feat: 新增日志、配置文件和环境变量
This commit is contained in:
92
logger/encoder.go
Normal file
92
logger/encoder.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// EncoderType represents the log encoder type
|
||||
type EncoderType string
|
||||
|
||||
const (
|
||||
// JSONEncoder encodes logs as JSON
|
||||
JSONEncoder EncoderType = "json"
|
||||
// ConsoleEncoder encodes logs as human-readable text
|
||||
ConsoleEncoder EncoderType = "console"
|
||||
)
|
||||
|
||||
// NewEncoderConfig returns a default encoder config
|
||||
func NewEncoderConfig() zapcore.EncoderConfig {
|
||||
return zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
FunctionKey: zapcore.OmitKey,
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
// newConsoleEncoderConfig returns a colored encoder config for console output
|
||||
func newConsoleEncoderConfig() zapcore.EncoderConfig {
|
||||
return zapcore.EncoderConfig{
|
||||
TimeKey: "time",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
FunctionKey: zapcore.OmitKey,
|
||||
MessageKey: "msg",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: CustomLevelEncoder, // 自定义带颜色的级别编码
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
}
|
||||
|
||||
// NewJSONEncoder creates a new JSON encoder
|
||||
func NewJSONEncoder() zapcore.Encoder {
|
||||
return zapcore.NewJSONEncoder(NewEncoderConfig())
|
||||
}
|
||||
|
||||
// NewConsoleEncoder creates a new console encoder with colors
|
||||
func NewConsoleEncoder() zapcore.Encoder {
|
||||
return zapcore.NewConsoleEncoder(newConsoleEncoderConfig())
|
||||
}
|
||||
|
||||
// NewEncoder creates an encoder based on the given type
|
||||
// Defaults to console encoder if type is invalid
|
||||
func NewEncoder(encoderType EncoderType) zapcore.Encoder {
|
||||
switch encoderType {
|
||||
case JSONEncoder:
|
||||
return NewJSONEncoder()
|
||||
case ConsoleEncoder:
|
||||
return NewConsoleEncoder()
|
||||
default:
|
||||
return NewConsoleEncoder()
|
||||
}
|
||||
}
|
||||
|
||||
// CustomLevelEncoder creates a custom level encoder with colors and styles
|
||||
func CustomLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
||||
switch l {
|
||||
case zapcore.DebugLevel:
|
||||
enc.AppendString("\x1b[37m" + l.CapitalString() + "\x1b[0m") // 白色
|
||||
case zapcore.InfoLevel:
|
||||
enc.AppendString("\x1b[32m" + l.CapitalString() + "\x1b[0m") // 绿色
|
||||
case zapcore.WarnLevel:
|
||||
enc.AppendString("\x1b[33m" + l.CapitalString() + "\x1b[0m") // 黄色
|
||||
case zapcore.ErrorLevel:
|
||||
enc.AppendString("\x1b[31m" + l.CapitalString() + "\x1b[0m") // 红色
|
||||
case zapcore.FatalLevel:
|
||||
enc.AppendString("\x1b[35m" + l.CapitalString() + "\x1b[0m") // 紫色
|
||||
default:
|
||||
enc.AppendString(l.CapitalString())
|
||||
}
|
||||
}
|
||||
180
logger/logger.go
Normal file
180
logger/logger.go
Normal file
@@ -0,0 +1,180 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"git.hujye.com/infrastructure/go-web-gin/config"
|
||||
"git.hujye.com/infrastructure/go-web-gin/env"
|
||||
"git.hujye.com/infrastructure/go-web-gin/web"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
type Logger struct {
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
var (
|
||||
instance *Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// parseLogLevel converts string log level to zapcore.Level
|
||||
func parseLogLevel(level string) zapcore.Level {
|
||||
switch strings.ToLower(level) {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "info":
|
||||
return zapcore.InfoLevel
|
||||
case "warn", "warning":
|
||||
return zapcore.WarnLevel
|
||||
case "error":
|
||||
return zapcore.ErrorLevel
|
||||
case "fatal":
|
||||
return zapcore.FatalLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// GetLogger returns the singleton Logger instance
|
||||
// Logger configuration is based on RUN_ENV environment variable and config file
|
||||
func GetLogger() *Logger {
|
||||
once.Do(func() {
|
||||
runEnv := env.GetRunEnv()
|
||||
cfg := config.Get()
|
||||
|
||||
// Production and Development use JSON encoder
|
||||
// Local uses console encoder for better readability
|
||||
var encoderType EncoderType
|
||||
if runEnv == env.Production || runEnv == env.Development {
|
||||
encoderType = JSONEncoder
|
||||
} else {
|
||||
encoderType = ConsoleEncoder
|
||||
}
|
||||
|
||||
// Create encoder
|
||||
encoder := NewEncoder(encoderType)
|
||||
|
||||
// Create writer syncs (console + file if enabled)
|
||||
var writerSyncs zapcore.WriteSyncer
|
||||
if cfg.Log.OutputToFile {
|
||||
// Create rotating file writer with lumberjack
|
||||
fileWriter := &lumberjack.Logger{
|
||||
Filename: cfg.Log.Filename,
|
||||
MaxSize: cfg.Log.MaxSize,
|
||||
MaxBackups: cfg.Log.MaxBackups,
|
||||
MaxAge: cfg.Log.MaxAge,
|
||||
Compress: cfg.Log.Compress,
|
||||
}
|
||||
// Write to both console and file
|
||||
writerSyncs = zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stderr), zapcore.AddSync(fileWriter))
|
||||
} else {
|
||||
// Write to console only
|
||||
writerSyncs = zapcore.AddSync(os.Stderr)
|
||||
}
|
||||
|
||||
// Parse log level from config
|
||||
logLevel := parseLogLevel(cfg.App.LogLevel)
|
||||
|
||||
// Create core with custom encoder
|
||||
core := zapcore.NewCore(
|
||||
encoder,
|
||||
writerSyncs,
|
||||
logLevel,
|
||||
)
|
||||
|
||||
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
|
||||
instance = &Logger{
|
||||
logger: logger,
|
||||
}
|
||||
})
|
||||
return instance
|
||||
}
|
||||
|
||||
// getUserFields extracts user information from context and returns zap fields
|
||||
func getUserFields(ctx context.Context) []zap.Field {
|
||||
if ctx == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
fields := []zap.Field{}
|
||||
if userID := web.GetUserID(ctx); userID != "" {
|
||||
fields = append(fields, zap.String("user_id", userID))
|
||||
}
|
||||
if userName := web.GetUserName(ctx); userName != "" {
|
||||
fields = append(fields, zap.String("user_name", userName))
|
||||
}
|
||||
if trace := web.GetTrace(ctx); trace != "" {
|
||||
fields = append(fields, zap.String("trace", trace))
|
||||
}
|
||||
if fromIP := web.GetFromIP(ctx); fromIP != "" {
|
||||
fields = append(fields, zap.String("from_ip", fromIP))
|
||||
}
|
||||
|
||||
if len(fields) == 0 {
|
||||
return nil
|
||||
}
|
||||
return fields
|
||||
}
|
||||
|
||||
// Info logs an info message
|
||||
func (l *Logger) Info(ctx context.Context, msg string, values ...interface{}) {
|
||||
allFields := getUserFields(ctx)
|
||||
formattedMsg := fmt.Sprintf(msg, values...)
|
||||
if len(values) > 0 {
|
||||
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||
}
|
||||
l.logger.Info(formattedMsg, allFields...)
|
||||
}
|
||||
|
||||
// Error logs an error message
|
||||
func (l *Logger) Error(ctx context.Context, msg string, values ...interface{}) {
|
||||
allFields := getUserFields(ctx)
|
||||
formattedMsg := fmt.Sprintf(msg, values...)
|
||||
if len(values) > 0 {
|
||||
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||
}
|
||||
l.logger.Error(formattedMsg, allFields...)
|
||||
}
|
||||
|
||||
// Debug logs a debug message
|
||||
func (l *Logger) Debug(ctx context.Context, msg string, values ...interface{}) {
|
||||
allFields := getUserFields(ctx)
|
||||
formattedMsg := fmt.Sprintf(msg, values...)
|
||||
if len(values) > 0 {
|
||||
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||
}
|
||||
l.logger.Debug(formattedMsg, allFields...)
|
||||
}
|
||||
|
||||
// Warn logs a warning message
|
||||
func (l *Logger) Warn(ctx context.Context, msg string, values ...interface{}) {
|
||||
allFields := getUserFields(ctx)
|
||||
formattedMsg := fmt.Sprintf(msg, values...)
|
||||
if len(values) > 0 {
|
||||
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||
}
|
||||
l.logger.Warn(formattedMsg, allFields...)
|
||||
}
|
||||
|
||||
// Fatal logs a fatal message and exits
|
||||
func (l *Logger) Fatal(ctx context.Context, msg string, values ...interface{}) {
|
||||
allFields := getUserFields(ctx)
|
||||
formattedMsg := fmt.Sprintf(msg, values...)
|
||||
if len(values) > 0 {
|
||||
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||
}
|
||||
l.logger.Fatal(formattedMsg, allFields...)
|
||||
}
|
||||
|
||||
// Sync flushes any buffered log entries
|
||||
func (l *Logger) Sync() error {
|
||||
return l.logger.Sync()
|
||||
}
|
||||
166
logger/logger_test.go
Normal file
166
logger/logger_test.go
Normal file
@@ -0,0 +1,166 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"git.hujye.com/infrastructure/go-web-gin/web"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// LoggerTestSuite is the test suite for Logger
|
||||
type LoggerTestSuite struct {
|
||||
suite.Suite
|
||||
logger *Logger
|
||||
}
|
||||
|
||||
// SetupSuite runs once before all tests
|
||||
func (s *LoggerTestSuite) SetupSuite() {
|
||||
s.logger = GetLogger()
|
||||
}
|
||||
|
||||
// TearDownSuite runs once after all tests
|
||||
func (s *LoggerTestSuite) TearDownSuite() {
|
||||
s.logger.Sync()
|
||||
}
|
||||
|
||||
// SetupTest runs before each test
|
||||
func (s *LoggerTestSuite) SetupTest() {
|
||||
// Reset logger instance if needed
|
||||
}
|
||||
|
||||
// TearDownTest runs after each test
|
||||
func (s *LoggerTestSuite) TearDownTest() {
|
||||
// Cleanup after each test if needed
|
||||
}
|
||||
|
||||
// TestGetLogger tests the GetLogger function
|
||||
func (s *LoggerTestSuite) TestGetLogger() {
|
||||
logger := GetLogger()
|
||||
s.NotNil(logger, "GetLogger should return a non-nil logger")
|
||||
s.NotNil(logger.logger, "logger field should be initialized")
|
||||
}
|
||||
|
||||
// TestGetLoggerSingleton tests that GetLogger returns the same instance
|
||||
func (s *LoggerTestSuite) TestGetLoggerSingleton() {
|
||||
logger1 := GetLogger()
|
||||
logger2 := GetLogger()
|
||||
s.Equal(logger1, logger2, "GetLogger should return the same instance")
|
||||
}
|
||||
|
||||
// TestInfoWithNilContext tests Info method with nil context
|
||||
func (s *LoggerTestSuite) TestInfoWithNilContext() {
|
||||
s.NotPanics(func() {
|
||||
s.logger.Info(nil, "Test info message")
|
||||
}, "Info should not panic with nil context")
|
||||
}
|
||||
|
||||
// TestErrorWithNilContext tests Error method with nil context
|
||||
func (s *LoggerTestSuite) TestErrorWithNilContext() {
|
||||
s.NotPanics(func() {
|
||||
s.logger.Error(nil, "Test error message")
|
||||
}, "Error should not panic with nil context")
|
||||
}
|
||||
|
||||
// TestDebugWithNilContext tests Debug method with nil context
|
||||
func (s *LoggerTestSuite) TestDebugWithNilContext() {
|
||||
s.NotPanics(func() {
|
||||
s.logger.Debug(nil, "Test debug message")
|
||||
}, "Debug should not panic with nil context")
|
||||
}
|
||||
|
||||
// TestWarnWithNilContext tests Warn method with nil context
|
||||
func (s *LoggerTestSuite) TestWarnWithNilContext() {
|
||||
s.NotPanics(func() {
|
||||
s.logger.Warn(nil, "Test warn message")
|
||||
}, "Warn should not panic with nil context")
|
||||
}
|
||||
|
||||
// TestInfoWithContext tests Info method with context containing user info
|
||||
func (s *LoggerTestSuite) TestInfoWithContext() {
|
||||
ctx := context.Background()
|
||||
ctx = web.SetUserID(ctx, "test-user-123")
|
||||
ctx = web.SetUserName(ctx, "testuser")
|
||||
ctx = web.SetTrace(ctx, "trace-test-456")
|
||||
ctx = web.SetFromIP(ctx, "192.168.1.1")
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Info(ctx, "Test info with context")
|
||||
}, "Info should not panic with valid context")
|
||||
}
|
||||
|
||||
// TestErrorWithContext tests Error method with context containing user info
|
||||
func (s *LoggerTestSuite) TestErrorWithContext() {
|
||||
ctx := context.Background()
|
||||
ctx = web.SetUserID(ctx, "error-user-789")
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Error(ctx, "Test error with context")
|
||||
}, "Error should not panic with valid context")
|
||||
}
|
||||
|
||||
// TestInfoWithFormattedMessage tests Info method with formatted message
|
||||
func (s *LoggerTestSuite) TestInfoWithFormattedMessage() {
|
||||
ctx := context.Background()
|
||||
ctx = web.SetUserID(ctx, "format-user-001")
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Info(ctx, "User %s logged in from %s", "admin", "10.0.0.1")
|
||||
}, "Info should handle formatted messages correctly")
|
||||
}
|
||||
|
||||
// TestErrorWithFormattedMessage tests Error method with formatted message
|
||||
func (s *LoggerTestSuite) TestErrorWithFormattedMessage() {
|
||||
s.NotPanics(func() {
|
||||
s.logger.Error(nil, "Error code: %d, message: %s", 500, "Internal Server Error")
|
||||
}, "Error should handle formatted messages correctly")
|
||||
}
|
||||
|
||||
// TestAllLogLevels tests all log level methods
|
||||
func (s *LoggerTestSuite) TestAllLogLevels() {
|
||||
ctx := context.Background()
|
||||
ctx = web.SetUserID(ctx, "level-test-user")
|
||||
ctx = web.SetUserName(ctx, "leveltest")
|
||||
ctx = web.SetTrace(ctx, "trace-level-test")
|
||||
ctx = web.SetFromIP(ctx, "10.20.30.40")
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Debug(ctx, "Debug message")
|
||||
s.logger.Info(ctx, "Info message")
|
||||
s.logger.Warn(ctx, "Warn message")
|
||||
s.logger.Error(ctx, "Error message")
|
||||
// Skip Fatal as it will exit the program
|
||||
}, "All log levels should work correctly")
|
||||
}
|
||||
|
||||
// TestEmptyContext tests with empty context (no user info)
|
||||
func (s *LoggerTestSuite) TestEmptyContext() {
|
||||
ctx := context.Background()
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Info(ctx, "Message with empty context")
|
||||
}, "Should handle empty context without user info")
|
||||
}
|
||||
|
||||
// TestPartialUserInfo tests with partial user information
|
||||
func (s *LoggerTestSuite) TestPartialUserInfo() {
|
||||
ctx := context.Background()
|
||||
// Only set user ID, not other fields
|
||||
ctx = web.SetUserID(ctx, "partial-user")
|
||||
|
||||
s.NotPanics(func() {
|
||||
s.logger.Info(ctx, "Message with partial user info")
|
||||
}, "Should handle partial user information")
|
||||
}
|
||||
|
||||
// TestLoggerSync tests the Sync method
|
||||
func (s *LoggerTestSuite) TestLoggerSync() {
|
||||
err := s.logger.Sync()
|
||||
// Sync may return an error for stderr in test environment, which is acceptable
|
||||
s.T().Logf("Sync returned error (may be expected for stderr): %v", err)
|
||||
}
|
||||
|
||||
// TestLoggerRunSuite runs the test suite
|
||||
func TestLoggerRunSuite(t *testing.T) {
|
||||
suite.Run(t, new(LoggerTestSuite))
|
||||
}
|
||||
74
logger/struct.go
Normal file
74
logger/struct.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
// LogMsg represents the standard log message structure
|
||||
type LogMsg struct {
|
||||
UserID string `json:"user_id"`
|
||||
UserName string `json:"user_name"`
|
||||
TraceID string `json:"trace"`
|
||||
IP string `json:"ip"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
// MarshalLogObject implements zapcore.ObjectMarshaler interface
|
||||
// This allows the encoder to properly encode LogMsg struct
|
||||
func (l LogMsg) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||
enc.AddString("user_id", l.UserID)
|
||||
enc.AddString("user_name", l.UserName)
|
||||
enc.AddString("trace", l.TraceID)
|
||||
enc.AddString("ip", l.IP)
|
||||
enc.AddString("msg", l.Msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// ToZapFields converts LogMsg to zap.Field slice for structured logging
|
||||
func (l *LogMsg) ToZapFields() []zap.Field {
|
||||
return []zap.Field{
|
||||
zap.String("user_id", l.UserID),
|
||||
zap.String("user_name", l.UserName),
|
||||
zap.String("trace", l.TraceID),
|
||||
zap.String("ip", l.IP),
|
||||
zap.String("msg", l.Msg),
|
||||
}
|
||||
}
|
||||
|
||||
// NewLogMsg creates a new LogMsg instance
|
||||
func NewLogMsg(msg string) *LogMsg {
|
||||
return &LogMsg{
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
// WithUserID sets the user ID
|
||||
func (l *LogMsg) WithUserID(userID string) *LogMsg {
|
||||
l.UserID = userID
|
||||
return l
|
||||
}
|
||||
|
||||
// WithUserName sets the user name
|
||||
func (l *LogMsg) WithUserName(userName string) *LogMsg {
|
||||
l.UserName = userName
|
||||
return l
|
||||
}
|
||||
|
||||
// WithTraceID sets the trace ID
|
||||
func (l *LogMsg) WithTraceID(traceID string) *LogMsg {
|
||||
l.TraceID = traceID
|
||||
return l
|
||||
}
|
||||
|
||||
// WithIP sets the IP address
|
||||
func (l *LogMsg) WithIP(ip string) *LogMsg {
|
||||
l.IP = ip
|
||||
return l
|
||||
}
|
||||
|
||||
// WithMsg sets the message
|
||||
func (l *LogMsg) WithMsg(msg string) *LogMsg {
|
||||
l.Msg = msg
|
||||
return l
|
||||
}
|
||||
Reference in New Issue
Block a user