diff --git a/src/internal/config/base.go b/src/internal/config/base.go index 891df7e..200d584 100644 --- a/src/internal/config/base.go +++ b/src/internal/config/base.go @@ -15,23 +15,26 @@ import ( func LoadConfig(logger *slog.Logger) (*Config, error) { _ = godotenv.Load() // необязательно фейлиться, если файла нет - cfg := &Config{ - Timezone: GetEnvAs("TIMEZONE", "UTC", ParseString), - ServiceURL: GetEnvAs("SERVICE_URL", "http://localhost:8080", ParseString), - LoggingConfig: LoggingConfig{ - Instance: logger, - Level: GetEnvAs("LOG_LEVEL", "info", ParseString), - }, - DatabaseConfig: FillDatabaseConfig(), - } + cfg := &Config{} - printConfig(cfg) + cfg.Timezone = GetEnvAs("TIMEZONE", "UTC", ParseString) + cfg.ServiceURL = GetEnvAs("SERVICE_URL", "http://localhost:8080", ParseString) + + cfg.LoggingConfig.Instance = logger + cfg.LoggingConfig.Level = GetEnvAs("LOG_LEVEL", "info", ParseString) + cfg.LoggingConfig.ShowCfgDump = GetEnvAs("LOG_SHOW_DUMP", false, ParseBool) + + cfg.DatabaseConfig = FillDatabaseConfig() + + if cfg.LoggingConfig.ShowCfgDump { + cfg.Print() + } return cfg, nil } // PrintConfig выводит конфигурацию (или любой другой struct) в виде таблички "KEY - VALUE". // Функция использует рефлексию для перебора полей структуры. -func printConfig(cfg any) { +func (c *Config) Print() { w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) fmt.Fprintln(w, "Loaded configuration:") @@ -39,7 +42,7 @@ func printConfig(cfg any) { fmt.Fprintln(w, "----\t-----") // Получаем reflect.Value объекта. - v := reflect.ValueOf(cfg) + v := reflect.ValueOf(c) // Если передан указатель, получаем значение, на которое он указывает. if v.Kind() == reflect.Ptr { v = v.Elem() diff --git a/src/internal/config/database.go b/src/internal/config/database.go index 6ea959f..f86dddc 100644 --- a/src/internal/config/database.go +++ b/src/internal/config/database.go @@ -2,10 +2,12 @@ package config import ( "fmt" + "net/url" "time" ) type DatabaseConfig struct { + URL string Kind string Host string Port string @@ -17,27 +19,95 @@ type DatabaseConfig struct { } func FillDatabaseConfig() DatabaseConfig { + databaseURL := GetEnvAs("DATABASE_URL", "", ParseString) + + if databaseURL == "" { + return DatabaseConfig{} + } + + u, err := url.Parse(databaseURL) + if err != nil { + return DatabaseConfig{} + } + + kind := "postgres" + if u.Scheme == "postgresql" { + kind = "postgres" + } else if u.Scheme != "" { + kind = u.Scheme + } + + dbName := "" + if len(u.Path) > 1 { + dbName = u.Path[1:] + } + return DatabaseConfig{ - Kind: GetEnvAs("DATABASE_KIND", "postgres", ParseString), - Host: GetEnvAs("DATABASE_HOST", "localhost", ParseString), - Port: GetEnvAs("DATABASE_PORT", "5432", ParseString), - User: GetEnvAs("DATABASE_USER", "chudovo", ParseString), - Password: GetEnvAs("DATABASE_PASS", "top_secret", ParseString), - Name: GetEnvAs("DATABASE_NAME", "chudovo", ParseString), - UseTLS: GetEnvAs("DATABASE_USETLS", false, ParseBool), + URL: databaseURL, + Kind: kind, + Host: u.Hostname(), + Port: u.Port(), + User: u.User.Username(), + Password: func() string { + password, _ := u.User.Password() + return password + }(), + Name: dbName, + UseTLS: GetEnvAs("DATABASE_USETLS", false, ParseBool), + Timeout: GetEnvAs("DATABASE_TIMEOUT", 30*time.Second, ParseDuration), } } -func GetDatabaseDSN(cfg *DatabaseConfig) string { +// GetDatabaseURLForLogging возвращает URL базы данных для логирования, скрывая пароль. +func GetDatabaseURLForLogging(cfg *DatabaseConfig) (string, error) { + if cfg.URL == "" { + return "", fmt.Errorf("parameter DATABASE_URL is empty") + } + + u, err := url.Parse(cfg.URL) + if err == nil { + if u.User != nil { + username := u.User.Username() + s := u.Scheme + "://" + username + ":***@" + u.Host + if u.Path != "" { + s += u.Path + } + if u.RawQuery != "" { + s += "?" + u.RawQuery + } + return s, err + } + } + return u.String(), err +} + +func GetDatabaseDSN(cfg *DatabaseConfig) (string, error) { + if cfg.URL == "" { + return "", fmt.Errorf("parameter DATABASE_URL is empty") + } + + u, err := url.Parse(cfg.URL) + if err == nil { + query := u.Query() + if !cfg.UseTLS { + query.Set("sslmode", "disable") + } else if query.Get("sslmode") == "" { + query.Set("sslmode", "require") + } + u.RawQuery = query.Encode() + return u.String(), err + } + dsn := fmt.Sprintf( "host=%s port=%s user=%s password=%s dbname=%s", cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Name, ) - // Если TLS отключен, добавляем параметр sslmode=disable if !cfg.UseTLS { dsn += " sslmode=disable" + } else { + dsn += " sslmode=require" } - return dsn + return dsn, nil } diff --git a/src/internal/config/interface.go b/src/internal/config/interface.go index ab6d8e6..64ca0a0 100644 --- a/src/internal/config/interface.go +++ b/src/internal/config/interface.go @@ -5,7 +5,7 @@ import "log/slog" type LoggingConfig struct { Instance *slog.Logger Level string - ShowCanDump bool + ShowCfgDump bool } type Config struct { diff --git a/src/internal/logging/logging.go b/src/internal/logging/logging.go new file mode 100644 index 0000000..a29bd39 --- /dev/null +++ b/src/internal/logging/logging.go @@ -0,0 +1,28 @@ +package logging + +import ( + "log/slog" + "os" + "strings" +) + +// New creates a slog logger with the provided level string (e.g., "debug", "info"). +func New(level string) *slog.Logger { + return slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: ParseLevel(level)})) +} + +// ParseLevel converts a string level to slog.Level, defaults to info on unknown. +func ParseLevel(lvl string) slog.Level { + switch strings.ToLower(lvl) { + case "debug": + return slog.LevelDebug + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + case "info", "": + return slog.LevelInfo + default: + return slog.LevelInfo + } +} diff --git a/src/main.go b/src/main.go index 67dc0ac..57beb2d 100644 --- a/src/main.go +++ b/src/main.go @@ -11,6 +11,7 @@ import ( "backend/ent" "backend/src/internal/config" "backend/src/internal/gateway" + "backend/src/internal/logging" "backend/src/logic" _ "github.com/lib/pq" // побочный импорт драйвера PostgreSQL @@ -24,8 +25,8 @@ const ( func main() { var err error - logger := slog.Default() - // slog.SetLogLoggerLevel(slog.LevelDebug) + logger := logging.New("info") + slog.SetDefault(logger) logger.Info(fmt.Sprintf("Starting %s version %s\n", AppName, AppVersion)) @@ -35,12 +36,32 @@ func main() { return } + // adjust logger according to LOG_LEVEL + level := logging.ParseLevel(cfg.LoggingConfig.Level) + if level != slog.LevelInfo { + logger.Info("Adjusting log level from env", "level", level.String()) + } + logger = logging.New(cfg.LoggingConfig.Level) + slog.SetDefault(logger) + cfg.LoggingConfig.Instance = logger + // создаем контекст с отменой для управления жизненным циклом сервиса. ctx, cancel := context.WithCancel(context.Background()) defer cancel() // подключаемся к базе данных - dsn := config.GetDatabaseDSN(&cfg.DatabaseConfig) + dbURL, err := config.GetDatabaseURLForLogging(&cfg.DatabaseConfig) + if err != nil { + logger.Error("Failed getting database URL for logging", "error", err) + return + } + logger.Info("Connecting to database...", "url", dbURL) + + dsn, err := config.GetDatabaseDSN(&cfg.DatabaseConfig) + if err != nil { + logger.Error("Failed getting database DSN", "error", err) + return + } db, err := ent.Open(cfg.DatabaseConfig.Kind, dsn) if err != nil { logger.Error("Failed opening connection to postgres", "error", err)