- 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.
226 lines
4.8 KiB
Go
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
|
|
}
|