Files
meowrain eab464060b feat: add Docker support and logging enhancements
- Introduced Dockerfile and .dockerignore for containerization of the backend service.
- Added logging configuration with support for daily rolling logs and customizable log levels.
- Enhanced the server to utilize structured logging with zap, including detailed request and error logging.
- Updated configuration to include logging parameters and proxy settings.
- Implemented tests for logging configuration and proxy settings.
2026-02-15 18:24:48 +08:00

226 lines
4.8 KiB
Go

package logging
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"gopkg.in/natefinch/lumberjack.v2"
)
type Config struct {
AppName string
Environment string
BasePath string
Level string
MaxSizeMB int
MaxBackups int
MaxAgeDays int
Compress bool
ConsoleEnabled bool
}
func New(cfg Config) (*zap.Logger, func() error, error) {
cfg = finalizeConfig(cfg)
infoWriter, err := newDailyRollingWriter(cfg, "info.log")
if err != nil {
return nil, nil, err
}
errorWriter, err := newDailyRollingWriter(cfg, "error.log")
if err != nil {
_ = infoWriter.Close()
return nil, nil, err
}
level := parseLevel(cfg.Level)
encoderConfig := zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalLevelEncoder,
EncodeTime: zapcore.TimeEncoderOfLayout("2006-01-02 15:04:05.000"),
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
}
jsonEncoder := zapcore.NewJSONEncoder(encoderConfig)
consoleEncoder := zapcore.NewConsoleEncoder(encoderConfig)
cores := make([]zapcore.Core, 0, 3)
cores = append(cores, zapcore.NewCore(
jsonEncoder,
zapcore.AddSync(io.Writer(infoWriter)),
zap.LevelEnablerFunc(func(l zapcore.Level) bool {
return l >= level && l < zapcore.ErrorLevel
}),
))
cores = append(cores, zapcore.NewCore(
jsonEncoder,
zapcore.AddSync(io.Writer(errorWriter)),
zap.LevelEnablerFunc(func(l zapcore.Level) bool {
return l >= maxLevel(level, zapcore.ErrorLevel)
}),
))
if cfg.ConsoleEnabled {
cores = append(cores, zapcore.NewCore(
consoleEncoder,
zapcore.AddSync(os.Stdout),
zap.LevelEnablerFunc(func(l zapcore.Level) bool {
return l >= level
}),
))
}
logger := zap.New(
zapcore.NewTee(cores...),
zap.AddCaller(),
zap.AddCallerSkip(1),
zap.AddStacktrace(zapcore.ErrorLevel),
zap.Fields(
zap.String("app", cfg.AppName),
zap.String("env", cfg.Environment),
),
)
cleanup := func() error {
var finalErr error
if err := infoWriter.Close(); err != nil && finalErr == nil {
finalErr = err
}
if err := errorWriter.Close(); err != nil && finalErr == nil {
finalErr = err
}
if err := logger.Sync(); err != nil && finalErr == nil {
finalErr = err
}
return finalErr
}
return logger, cleanup, nil
}
type dailyRollingWriter struct {
mu sync.Mutex
cfg Config
fileName string
dayKey string
writer *lumberjack.Logger
}
func newDailyRollingWriter(cfg Config, fileName string) (*dailyRollingWriter, error) {
w := &dailyRollingWriter{
cfg: cfg,
fileName: fileName,
}
if err := w.ensureWriter(time.Now()); err != nil {
return nil, err
}
return w, nil
}
func (w *dailyRollingWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
if err := w.ensureWriter(time.Now()); err != nil {
return 0, err
}
return w.writer.Write(p)
}
func (w *dailyRollingWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
if w.writer == nil {
return nil
}
err := w.writer.Close()
w.writer = nil
return err
}
func (w *dailyRollingWriter) ensureWriter(now time.Time) error {
dayKey := now.Format("2006-01-02")
if w.writer != nil && w.dayKey == dayKey {
return nil
}
monthDir := now.Format("2006-01")
logDir := filepath.Join(w.cfg.BasePath, monthDir, dayKey)
if err := os.MkdirAll(logDir, 0o755); err != nil {
return fmt.Errorf("mkdir log dir failed: %w", err)
}
next := &lumberjack.Logger{
Filename: filepath.Join(logDir, w.fileName),
MaxSize: w.cfg.MaxSizeMB,
MaxBackups: w.cfg.MaxBackups,
MaxAge: w.cfg.MaxAgeDays,
Compress: w.cfg.Compress,
}
if w.writer != nil {
_ = w.writer.Close()
}
w.writer = next
w.dayKey = dayKey
return nil
}
func finalizeConfig(cfg Config) Config {
if strings.TrimSpace(cfg.AppName) == "" {
cfg.AppName = "lsp-gateway"
}
if strings.TrimSpace(cfg.Environment) == "" {
cfg.Environment = "dev"
}
if strings.TrimSpace(cfg.BasePath) == "" {
cfg.BasePath = filepath.Join(".", "logs", cfg.AppName)
}
if strings.TrimSpace(cfg.Level) == "" {
cfg.Level = "info"
}
if cfg.MaxSizeMB <= 0 {
cfg.MaxSizeMB = 100
}
if cfg.MaxBackups <= 0 {
cfg.MaxBackups = 31
}
if cfg.MaxAgeDays <= 0 {
cfg.MaxAgeDays = 31
}
return cfg
}
func parseLevel(level string) zapcore.Level {
switch strings.ToLower(strings.TrimSpace(level)) {
case "debug":
return zapcore.DebugLevel
case "info":
return zapcore.InfoLevel
case "warn", "warning":
return zapcore.WarnLevel
case "error":
return zapcore.ErrorLevel
default:
return zapcore.InfoLevel
}
}
func maxLevel(a, b zapcore.Level) zapcore.Level {
if a > b {
return a
}
return b
}