Files
gochat/backend/pkg/logger/logger.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

114 lines
2.6 KiB
Go

package logger
import (
"os"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Logger wraps zap.SugaredLogger for structured logging.
// Corresponds to Chatwoot's Rails Logger but with structured output.
var globalLogger *zap.SugaredLogger
// Config for logger initialization
type Config struct {
Level string // debug, info, warn, error
Format string // json, console
Output string // stdout, stderr, or file path
ErrorOutput string // stderr or file path
}
// Init initializes the global logger based on config.
func Init(cfg Config) error {
level, err := parseLevel(cfg.Level)
if err != nil {
return err
}
encoderConfig := zap.NewProductionEncoderConfig()
encoderConfig.TimeKey = "timestamp"
encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
encoderConfig.EncodeLevel = zapcore.CapitalLevelEncoder
var encoder zapcore.Encoder
if cfg.Format == "console" {
encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
encoder = zapcore.NewConsoleEncoder(encoderConfig)
} else {
encoder = zapcore.NewJSONEncoder(encoderConfig)
}
outputWriter, err := getOutput(cfg.Output)
if err != nil {
return err
}
errorWriter, err := getOutput(cfg.ErrorOutput)
if err != nil {
return err
}
core := zapcore.NewCore(
encoder,
zapcore.NewMultiWriteSyncer(zapcore.AddSync(outputWriter)),
level,
)
// Also send error-level logs to error output
errorCore := zapcore.NewCore(
encoder,
zapcore.AddSync(errorWriter),
zapcore.ErrorLevel,
)
combinedCore := zapcore.NewTee(core, errorCore)
zapLogger := zap.New(combinedCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
globalLogger = zapLogger.Sugar()
return nil
}
// L returns the global sugared logger
func L() *zap.SugaredLogger {
if globalLogger == nil {
// Fallback: if Init wasn't called, use a default logger
zapLogger, _ := zap.NewProduction()
globalLogger = zapLogger.Sugar()
}
return globalLogger
}
// Sync flushes any buffered log entries. Should be called before program exit.
func Sync() {
if globalLogger != nil {
_ = globalLogger.Sync()
}
}
func parseLevel(levelStr string) (zapcore.Level, error) {
switch levelStr {
case "debug":
return zapcore.DebugLevel, nil
case "info":
return zapcore.InfoLevel, nil
case "warn":
return zapcore.WarnLevel, nil
case "error":
return zapcore.ErrorLevel, nil
default:
return zapcore.InfoLevel, nil
}
}
func getOutput(output string) (*os.File, error) {
switch output {
case "stdout":
return os.Stdout, nil
case "stderr":
return os.Stderr, nil
default:
return os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
}
}