Add structured logging and centralized error handling

- Initialize slog in the serve command with terminal/file support
- Introduce `AppError` with HTTP status for unified service-layer errors
- Replace ad-hoc `api.Error` calls with `api.RespondError`
- Wrap internal errors with `model.NewInternalError` and add reference
  IDs
- Add `RequestID` middleware and switch router from `gin.Default` to
  `gin.New`
This commit is contained in:
2026-07-04 16:24:22 +08:00
parent a78d43b166
commit 1dfccf513a
16 changed files with 281 additions and 134 deletions
+38
View File
@@ -3,6 +3,7 @@ package config
import (
"errors"
"fmt"
"log/slog"
"net"
"time"
)
@@ -12,6 +13,7 @@ type Config struct {
Database DatabaseConfig `mapstructure:"database"`
Storage StorageConfig `mapstructure:"storage"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
@@ -54,6 +56,30 @@ type JWTConfig struct {
RefreshTTL time.Duration `mapstructure:"refresh_ttl"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
FilePath string `mapstructure:"file_path"`
FileLevel string `mapstructure:"file_level"`
}
// ParseLogLevel converts a string to slog.Level.
func ParseLogLevel(s string) (slog.Level, error) {
if s == "" {
return slog.LevelInfo, nil
}
switch s {
case "debug":
return slog.LevelDebug, nil
case "info":
return slog.LevelInfo, nil
case "warn":
return slog.LevelWarn, nil
case "error":
return slog.LevelError, nil
default:
return slog.LevelInfo, fmt.Errorf("unknown log level: %q (use debug, info, warn, or error)", s)
}
}
func (c *Config) Validate() error {
var errs []error
@@ -107,5 +133,17 @@ func (c *Config) Validate() error {
errs = append(errs, errors.New("jwt.refresh_ttl: must be positive"))
}
if c.Log.Level != "" {
if _, err := ParseLogLevel(c.Log.Level); err != nil {
errs = append(errs, fmt.Errorf("log.level: %w", err))
}
}
if c.Log.FileLevel != "" {
if _, err := ParseLogLevel(c.Log.FileLevel); err != nil {
errs = append(errs, fmt.Errorf("log.file_level: %w", err))
}
}
return errors.Join(errs...)
}
+4
View File
@@ -27,6 +27,10 @@ func defaults(v *viper.Viper) {
v.SetDefault("jwt.secret", "dev-secret-do-not-use-in-production")
v.SetDefault("jwt.access_ttl", "15m")
v.SetDefault("jwt.refresh_ttl", "168h")
v.SetDefault("log.level", "info")
v.SetDefault("log.file_path", "")
v.SetDefault("log.file_level", "debug")
}
func New() *viper.Viper {