diff --git a/config/config.go b/config/config.go index c24a152..5e89071 100644 --- a/config/config.go +++ b/config/config.go @@ -34,25 +34,19 @@ type Config struct { AllowedOrigins []string // Field for the allowed origins CronCleanupFrequency time.Duration // Field for configuring key cleanup cron } + Logging struct { + Level string + } } -// LoadConfig loads the application configuration from environment variables or defaults -func LoadConfig() *Config { +var AppConfig *Config + +func init() { err := godotenv.Load() if err != nil { slog.Debug("Warning: .env file not found, falling back to system environment variables.") } - - return &Config{ - DiceDBAdmin: struct { - Addr string - Username string - Password string - }{ - Addr: getEnv("DICEDB_ADMIN_ADDR", "localhost:7379"), // Default DiceDB Admin address - Username: getEnv("DICEDB_ADMIN_USERNAME", "diceadmin"), // Default DiceDB Admin username - Password: getEnv("DICEDB_ADMIN_PASSWORD", ""), // Default DiceDB Admin password - }, + AppConfig = &Config{ DiceDB: struct { Addr string Username string @@ -77,6 +71,11 @@ func LoadConfig() *Config { AllowedOrigins: getEnvArray("ALLOWED_ORIGINS", []string{"http://localhost:3000"}), // Default allowed origins CronCleanupFrequency: time.Duration(getEnvInt("CRON_CLEANUP_FREQUENCY_MINS", 15)) * time.Minute, // Default cron cleanup frequency }, + Logging: struct { + Level string + }{ + Level: getEnv("LOGGING_LEVEL", "info"), + }, } } diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..5a48b53 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,88 @@ +package logger + +import ( + "context" + "fmt" + "log" + "log/slog" + "server/config" + "strings" + "time" +) + +type CustomLogHandler struct { + level slog.Level + attrs map[string]interface{} + group string +} + +func NewCustomLogHandler(level slog.Level) *CustomLogHandler { + return &CustomLogHandler{ + level: level, + attrs: make(map[string]interface{}), + } +} + +func (h *CustomLogHandler) Enabled(_ context.Context, level slog.Level) bool { + return level >= h.level +} + +//nolint:gocritic // The slog.Record struct triggers hugeParam, but we don't control the interface (it's a standard library one) +func (h *CustomLogHandler) Handle(_ context.Context, record slog.Record) error { + if !h.Enabled(context.TODO(), record.Level) { + return nil + } + + message := fmt.Sprintf("[%s] [%s] %s", time.Now().Format(time.RFC3339), record.Level, record.Message) + + // Append attributes + for k, v := range h.attrs { + message += fmt.Sprintf(" | %s=%v", k, v) + } + + // Log to standard output + log.Println(message) + return nil +} + +func (h *CustomLogHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + newHandler := *h + newHandler.attrs = make(map[string]interface{}) + + for k, v := range h.attrs { + newHandler.attrs[k] = v + } + + for _, attr := range attrs { + newHandler.attrs[attr.Key] = attr.Value.Any() + } + return &newHandler +} + +func (h *CustomLogHandler) WithGroup(name string) slog.Handler { + newHandler := *h + newHandler.group = name + return &newHandler +} + +func ParseLogLevel(levelStr string) slog.Level { + switch strings.ToLower(levelStr) { + case "debug": + return slog.LevelDebug + case "info": + return slog.LevelInfo + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + +func New() *slog.Logger { + levelStr := config.AppConfig.Logging.Level + level := ParseLogLevel(levelStr) + handler := NewCustomLogHandler(level) + return slog.New(handler) +} diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go index e05c842..709489e 100644 --- a/internal/middleware/cors.go +++ b/internal/middleware/cors.go @@ -7,8 +7,7 @@ import ( // Updated enableCors function to return a boolean indicating if OPTIONS was handled func handleCors(w http.ResponseWriter, r *http.Request) bool { - configValue := config.LoadConfig() - allAllowedOrigins := configValue.Server.AllowedOrigins + allAllowedOrigins := config.AppConfig.Server.AllowedOrigins origin := r.Header.Get("Origin") allowed := false diff --git a/internal/tests/integration/commands/setup.go b/internal/tests/integration/commands/setup.go index 2b2420b..7ed75c0 100644 --- a/internal/tests/integration/commands/setup.go +++ b/internal/tests/integration/commands/setup.go @@ -40,8 +40,7 @@ type TestCase struct { } func NewHTTPCommandExecutor() (*HTTPCommandExecutor, error) { - configValue := config.LoadConfig() - diceClient, err := db.InitDiceClient(configValue, false) + diceClient, err := db.InitDiceClient(config.AppConfig, false) if err != nil { return nil, fmt.Errorf("failed to initialize DiceDB client: %v", err) } diff --git a/internal/tests/integration/ratelimiter_integration_test.go b/internal/tests/integration/ratelimiter_integration_test.go index 017ba95..a1e2c9d 100644 --- a/internal/tests/integration/ratelimiter_integration_test.go +++ b/internal/tests/integration/ratelimiter_integration_test.go @@ -11,9 +11,8 @@ import ( ) func TestRateLimiterWithinLimit(t *testing.T) { - configValue := config.LoadConfig() - limit := configValue.Server.RequestLimitPerMin - window := configValue.Server.RequestWindowSec + limit := config.AppConfig.Server.RequestLimitPerMin + window := config.AppConfig.Server.RequestWindowSec w, r, rateLimiter := util.SetupRateLimiter(limit, window) @@ -24,9 +23,8 @@ func TestRateLimiterWithinLimit(t *testing.T) { } func TestRateLimiterExceedsLimit(t *testing.T) { - configValue := config.LoadConfig() - limit := configValue.Server.RequestLimitPerMin - window := configValue.Server.RequestWindowSec + limit := config.AppConfig.Server.RequestLimitPerMin + window := config.AppConfig.Server.RequestWindowSec w, r, rateLimiter := util.SetupRateLimiter(limit, window) @@ -42,9 +40,8 @@ func TestRateLimiterExceedsLimit(t *testing.T) { } func TestRateLimitHeadersSet(t *testing.T) { - configValue := config.LoadConfig() - limit := configValue.Server.RequestLimitPerMin - window := configValue.Server.RequestWindowSec + limit := config.AppConfig.Server.RequestLimitPerMin + window := config.AppConfig.Server.RequestWindowSec w, r, rateLimiter := util.SetupRateLimiter(limit, window) diff --git a/internal/tests/stress/ratelimiter_stress_test.go b/internal/tests/stress/ratelimiter_stress_test.go index 79872e6..f155809 100644 --- a/internal/tests/stress/ratelimiter_stress_test.go +++ b/internal/tests/stress/ratelimiter_stress_test.go @@ -13,9 +13,8 @@ import ( ) func TestRateLimiterUnderStress(t *testing.T) { - configValue := config.LoadConfig() - limit := configValue.Server.RequestLimitPerMin - window := configValue.Server.RequestWindowSec + limit := config.AppConfig.Server.RequestLimitPerMin + window := config.AppConfig.Server.RequestWindowSec _, r, rateLimiter := util.SetupRateLimiter(limit, window) diff --git a/main.go b/main.go index 6539f8d..66985b6 100644 --- a/main.go +++ b/main.go @@ -7,6 +7,7 @@ import ( "os" "server/config" "server/internal/db" + "server/internal/logger" "server/internal/server" "sync" @@ -14,14 +15,14 @@ import ( ) func main() { - configValue := config.LoadConfig() - diceDBAdminClient, err := db.InitDiceClient(configValue, true) + slog.SetDefault(logger.New()) + diceDBAdminClient, err := db.InitDiceClient(config.AppConfig, true) if err != nil { slog.Error("Failed to initialize DiceDB Admin client: %v", slog.Any("err", err)) os.Exit(1) } - diceDBClient, err := db.InitDiceClient(configValue, false) + diceDBClient, err := db.InitDiceClient(config.AppConfig, false) if err != nil { slog.Error("Failed to initialize DiceDB client: %v", slog.Any("err", err)) os.Exit(1) @@ -31,14 +32,14 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) wg := sync.WaitGroup{} // Register a cleanup manager, this runs user DiceDB instance cleanup job at configured frequency - cleanupManager := server.NewCleanupManager(diceDBAdminClient, diceDBClient, configValue.Server.CronCleanupFrequency) + cleanupManager := server.NewCleanupManager(diceDBAdminClient, diceDBClient, config.AppConfig.Server.CronCleanupFrequency) wg.Add(1) go cleanupManager.Run(ctx, &wg) // Create mux and register routes mux := http.NewServeMux() - httpServer := server.NewHTTPServer(":8080", mux, diceDBAdminClient, diceDBClient, configValue.Server.RequestLimitPerMin, - configValue.Server.RequestWindowSec) + httpServer := server.NewHTTPServer(":8080", mux, diceDBAdminClient, diceDBClient, config.AppConfig.Server.RequestLimitPerMin, + config.AppConfig.Server.RequestWindowSec) mux.HandleFunc("/health", httpServer.HealthCheck) mux.HandleFunc("/shell/exec/{cmd}", httpServer.CliHandler) mux.HandleFunc("/search", httpServer.SearchHandler) diff --git a/util/helpers.go b/util/helpers.go index 0e9854f..9b6c9bd 100644 --- a/util/helpers.go +++ b/util/helpers.go @@ -53,9 +53,8 @@ func ParseHTTPRequest(r *http.Request) (*cmds.CommandRequest, error) { return nil, errors.New("invalid command") } - configValue := config.LoadConfig() // Check if the command is blocklisted - if err := BlockListedCommand(command); err != nil && !configValue.Server.IsTestEnv { + if err := BlockListedCommand(command); err != nil && !config.AppConfig.Server.IsTestEnv { return nil, err }