Files
mygo/internal/config/config.go
T
ld 1dfccf513a 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`
2026-07-04 16:24:22 +08:00

150 lines
4.1 KiB
Go

package config
import (
"errors"
"fmt"
"log/slog"
"net"
"time"
)
type Config struct {
Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"`
Storage StorageConfig `mapstructure:"storage"`
JWT JWTConfig `mapstructure:"jwt"`
Log LogConfig `mapstructure:"log"`
}
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type DatabaseConfig struct {
Driver string `mapstructure:"driver"`
SQLite SQLiteConfig `mapstructure:"sqlite"`
Postgres PostgresConfig `mapstructure:"postgres"`
}
type SQLiteConfig struct {
Path string `mapstructure:"path"`
}
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
}
type StorageConfig struct {
Driver string `mapstructure:"driver"`
Local LocalStorageConfig `mapstructure:"local"`
MaxUploadSize int64 `mapstructure:"max_upload_size"`
}
type LocalStorageConfig struct {
Path string `mapstructure:"path"`
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
AccessTTL time.Duration `mapstructure:"access_ttl"`
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
if c.Server.Port < 1 || c.Server.Port > 65535 {
errs = append(errs, fmt.Errorf("server.port: %d out of range [1, 65535]", c.Server.Port))
}
if addr := net.ParseIP(c.Server.Host); c.Server.Host != "" && addr == nil {
errs = append(errs, fmt.Errorf("server.host: %q is not a valid IP address", c.Server.Host))
}
switch c.Database.Driver {
case "sqlite3":
if c.Database.SQLite.Path == "" {
errs = append(errs, errors.New("database.sqlite.path: must not be empty"))
}
case "postgres":
if c.Database.Postgres.Host == "" {
errs = append(errs, errors.New("database.postgres.host: must not be empty"))
}
if c.Database.Postgres.Port < 1 || c.Database.Postgres.Port > 65535 {
errs = append(errs, fmt.Errorf("database.postgres.port: %d out of range [1, 65535]", c.Database.Postgres.Port))
}
if c.Database.Postgres.User == "" {
errs = append(errs, errors.New("database.postgres.user: must not be empty"))
}
if c.Database.Postgres.DBName == "" {
errs = append(errs, errors.New("database.postgres.dbname: must not be empty"))
}
default:
errs = append(errs, fmt.Errorf("database.driver: %q is not supported (use sqlite3 or postgres)", c.Database.Driver))
}
if c.Storage.Local.Path == "" {
errs = append(errs, errors.New("storage.local.path: must not be empty"))
}
if c.Storage.MaxUploadSize < 0 {
errs = append(errs, errors.New("storage.max_upload_size: must not be negative"))
}
if c.JWT.Secret == "" {
errs = append(errs, errors.New("jwt.secret: must not be empty"))
}
if c.JWT.AccessTTL <= 0 {
errs = append(errs, errors.New("jwt.access_ttl: must be positive"))
}
if c.JWT.RefreshTTL <= 0 {
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...)
}