From 745a59ecaaef7730191cf6b7973bd2f41b65d5c8 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Thu, 15 Jan 2026 13:10:53 -0700 Subject: [PATCH 1/5] (WIP): Working on the loggers --- cmd/web/main.go | 9 +++++- internal/infrastructure/logging/logger.go | 16 ++++++++++ .../logging/loggers/console_logger.go | 32 +++++++++++++++++++ .../logging/loggers/file_logger.go | 3 ++ 4 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 internal/infrastructure/logging/logger.go create mode 100644 internal/infrastructure/logging/loggers/console_logger.go create mode 100644 internal/infrastructure/logging/loggers/file_logger.go diff --git a/cmd/web/main.go b/cmd/web/main.go index 56c437e..20cb9f6 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -1,6 +1,10 @@ package main -import "github.com/haydenhargreaves/Potion/internal/app/server" +import ( + "github.com/haydenhargreaves/Potion/internal/app/server" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers" +) const PORT = 3000 @@ -8,5 +12,8 @@ func main() { s := server.Init(PORT).Setup() defer s.DB.Close() + logger := loggers.NewConsoleLogger() + logger.Log(logging.LogLevelDebug, "%s", "Hello world") + s.Start() } diff --git a/internal/infrastructure/logging/logger.go b/internal/infrastructure/logging/logger.go new file mode 100644 index 0000000..a182964 --- /dev/null +++ b/internal/infrastructure/logging/logger.go @@ -0,0 +1,16 @@ +package logging + +type LogLevel string + +const ( + LogLevelTrace LogLevel = "TRACE" + LogLevelDebug LogLevel = "DEBUG" + LogLevelInformation LogLevel = "INFORMATION" + LogLevelWarning LogLevel = "WARNING" + LogLevelError LogLevel = "ERROR" + LogLevelFatal LogLevel = "FATAL" +) + +type Logger interface { + Log(level LogLevel, format string, v ...any) +} diff --git a/internal/infrastructure/logging/loggers/console_logger.go b/internal/infrastructure/logging/loggers/console_logger.go new file mode 100644 index 0000000..334a7d4 --- /dev/null +++ b/internal/infrastructure/logging/loggers/console_logger.go @@ -0,0 +1,32 @@ +package loggers + +import ( + "fmt" + "io" + "os" + + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" +) + +// TODO: Implement the Logger interface + +type ConsoleLogger struct { + writer io.Writer +} + +var _ logging.Logger = (*ConsoleLogger)(nil) + +func NewConsoleLogger() ConsoleLogger { + + return ConsoleLogger{ + writer: os.Stdout, + } +} + +func (l *ConsoleLogger) Log(level logging.LogLevel, format string, v ...any) { + prefix := fmt.Appendf(nil, "[%s] ", level) + bytes := fmt.Appendf(prefix, format, v...) + + // WARN: Do we need to worry about errors? + _, _ = l.writer.Write(bytes) +} diff --git a/internal/infrastructure/logging/loggers/file_logger.go b/internal/infrastructure/logging/loggers/file_logger.go new file mode 100644 index 0000000..721655d --- /dev/null +++ b/internal/infrastructure/logging/loggers/file_logger.go @@ -0,0 +1,3 @@ +package loggers + +// TODO: Implement the Logger interface -- 2.47.2 From 72c9cb0f96ffc8b67a3a17e3fc1ce43ed2c46a2e Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 23 Jan 2026 10:25:51 -0700 Subject: [PATCH 2/5] (FEAT): Logging is so much better now :) --- cmd/web/main.go | 9 +-- go.mod | 1 + go.sum | 2 + internal/app/server/middleware_v2.go | 39 ++++++++++++- internal/app/server/server.go | 51 +++++++++++++---- internal/app/service/auth_service.go | 57 ++++++++++++++++++- internal/domain/server/server.go | 7 ++- internal/infrastructure/logging/logger.go | 25 +++++++- .../logging/loggers/console_logger.go | 38 +++++++++---- .../logging/loggers/file_logger.go | 49 ++++++++++++++++ 10 files changed, 240 insertions(+), 38 deletions(-) diff --git a/cmd/web/main.go b/cmd/web/main.go index 20cb9f6..56c437e 100644 --- a/cmd/web/main.go +++ b/cmd/web/main.go @@ -1,10 +1,6 @@ package main -import ( - "github.com/haydenhargreaves/Potion/internal/app/server" - "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" - "github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers" -) +import "github.com/haydenhargreaves/Potion/internal/app/server" const PORT = 3000 @@ -12,8 +8,5 @@ func main() { s := server.Init(PORT).Setup() defer s.DB.Close() - logger := loggers.NewConsoleLogger() - logger.Log(logging.LogLevelDebug, "%s", "Hello world") - s.Start() } diff --git a/go.mod b/go.mod index d938e8a..dc94d15 100644 --- a/go.mod +++ b/go.mod @@ -24,6 +24,7 @@ require ( github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/goccy/go-json v0.10.5 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 9ff89cb..dee93ff 100644 --- a/go.sum +++ b/go.sum @@ -57,6 +57,8 @@ github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= diff --git a/internal/app/server/middleware_v2.go b/internal/app/server/middleware_v2.go index fbdaa5f..fbf6a9d 100644 --- a/internal/app/server/middleware_v2.go +++ b/internal/app/server/middleware_v2.go @@ -3,10 +3,12 @@ package server import ( "fmt" "net/http" + "time" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" ) // JwtAuthMiddlewareV2 is responsible for protecting routes. Anything that may go wrong @@ -64,9 +66,9 @@ func JwtAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { } } -// JwtOptionalAuthMiddlewareV2 is responsible for collecting user data for routes where -// authentication is optional. Meaning: if the use is not logged in, this function does -// not fail or return, it simply does nothing. But if the user is logged in, then the +// JwtOptionalAuthMiddlewareV2 is responsible for collecting user data for routes where +// authentication is optional. Meaning: if the use is not logged in, this function does +// not fail or return, it simply does nothing. But if the user is logged in, then the // 'userId' and 'userEmail' context values are set. // // e.g., `userIdAny, exists := ctx.Get("userId")` @@ -96,3 +98,34 @@ func JwtOptionalAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc { ctx.Next() } } + +func LoggingMiddleware(logs []logging.Logger) gin.HandlerFunc { + // TODO: Need traces using IDs? + return func(ctx *gin.Context) { + start := time.Now() + + ctx.Next() + + var ( + status int = ctx.Writer.Status() + latency string = time.Since(start).String() + client string = ctx.ClientIP() + method string = ctx.Request.Method + path string = ctx.Request.URL.Path + ) + + // TODO: Add color to status + + format := "%d | %-14s | %15s | %-9s \"%s\"" + logging.LogAll( + logs, + logging.LogLevelInformation, + format, + status, + latency, + client, + method, + path, + ) + } +} diff --git a/internal/app/server/server.go b/internal/app/server/server.go index a2a7b52..bdc5f72 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -13,6 +13,8 @@ import ( domain "github.com/haydenhargreaves/Potion/internal/domain/server" "github.com/haydenhargreaves/Potion/internal/infrastructure/auth" "github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging/loggers" _ "github.com/lib/pq" ) @@ -23,18 +25,24 @@ type Server struct { config cors.Config DB *sql.DB deps domain.InjectedDependencies + logs []logging.Logger } // Init initializes the server with the provided port. CORS settings are defined here. // A pointer to a server object is returned which allows for method chaining. func Init(port int) *Server { server := &Server{ - Router: gin.Default(), + Router: gin.New(), // Not default anymore, to allow for custom logger port: port, config: cors.DefaultConfig(), + logs: []logging.Logger{}, } + // Default loggers + server.logs = append(server.logs, loggers.NewConsoleLogger()) + // Some stuff for templ rendering + // TODO: Remove this htmlRenderer := server.Router.HTMLRender server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer} @@ -54,28 +62,43 @@ func Init(port int) *Server { // Start starts the server on the port provided when the server was initialized func (s *Server) Start() { + logging.LogAll(s.logs, logging.LogLevelDebug, "Server started on :%d\n", s.port) s.Router.Run(fmt.Sprintf(":%d", s.port)) } // TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION - cfg, err := domain.LoadEnvironment() + cfg, err := domain.LoadEnvironment(s.logs) if err != nil { + logging.LogAll(s.logs, logging.LogLevelFatal, err.Error()) panic(err.Error()) } if cfg == nil { + logging.LogAll(s.logs, logging.LogLevelFatal, "Environment configuration is nil, crashing.") panic("Environment configuration is nil, crashing.") } + // TODO: Using release on them all? Def need to clean this shitty environment up if cfg.Environment == "dev" { - gin.SetMode(gin.DebugMode) + gin.SetMode(gin.ReleaseMode) } else if cfg.Environment == "prod" { gin.SetMode(gin.ReleaseMode) } else { gin.SetMode(gin.TestMode) } + // TODO: Implement environment here for logging file + path := "./logs.log" + + fileLogger, cleanup, err := loggers.NewFileLogger(path) + if err != nil { + logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) + } else { + s.logs = append(s.logs, fileLogger) + defer cleanup() + } + // SETUP GOOGLE AUTH var ( // NOTE: USING V2 NOW @@ -111,7 +134,7 @@ func (s *Server) Setup() *Server { recipeRepo := repository.NewRecipeRepository(s.DB) engagementRepo := repository.NewEngagementRepository(s.DB) userService := service.NewUserService(userRepo) - authService := service.NewAuthService(userRepo, jwtSecret) + authService := service.NewAuthService(userRepo, jwtSecret, s.logs) recipeService := service.NewRecipeService(recipeRepo, engagementRepo) engagementService := service.NewEngagementService(engagementRepo, recipeRepo) @@ -124,9 +147,8 @@ func (s *Server) Setup() *Server { } // Apply middleware - s.Router.Use(RecoveryMiddleware()) - // NOTE: No longer running on every connection! - // s.Router.Use(JwtAuthMiddleWare(jwtSecret)) + // TODO: Review the recovery middleware + s.Router.Use(gin.Recovery(), RecoveryMiddleware(), LoggingMiddleware(s.logs)) // Redirect index to home page: Update this as needed s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) @@ -219,14 +241,21 @@ func (s *Server) Setup() *Server { router_api_v2.GET("/user/recipes/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2) router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2) - router_api_v2.GET("/protected", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), func(ctx *gin.Context) { - ctx.JSON(http.StatusOK, gin.H{"msg": "YAY"}) - }) - router_api_v2.POST("/engagement/view/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementViewRecipeHandlerV2) router_api_v2.POST("/engagement/share/:id", JwtOptionalAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementShareRecipeHandlerV2) router_api_v2.POST("/engagement/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2) router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2) + if cfg.Environment == "dev" { + s.debugDisplayRoutes() + } + return s } + +func (s *Server) debugDisplayRoutes() { + for _, route := range s.Router.Routes() { + format := "%-8s %s" + logging.LogAll(s.logs, logging.LogLevelDebug, format, route.Method, route.Path) + } +} diff --git a/internal/app/service/auth_service.go b/internal/app/service/auth_service.go index 41d77a8..a6f04b7 100644 --- a/internal/app/service/auth_service.go +++ b/internal/app/service/auth_service.go @@ -10,6 +10,7 @@ import ( domainServer "github.com/haydenhargreaves/Potion/internal/domain/server" domain "github.com/haydenhargreaves/Potion/internal/domain/user" "github.com/haydenhargreaves/Potion/internal/infrastructure/auth" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" "golang.org/x/oauth2" ) @@ -31,6 +32,7 @@ import ( type AuthService struct { userRepository domain.UserRepository jwtSecret []byte + logs []logging.Logger } // Compile-time check to ensure the AuthService implements domain.AuthService @@ -38,10 +40,11 @@ var _ domainAuth.AuthService = (*AuthService)(nil) // NewAuthService creates a user service object which can be passed into the context. The service // requires a user repository which it will use to hit the database when needed. -func NewAuthService(userRepository domain.UserRepository, jwtSecret []byte) domainAuth.AuthService { +func NewAuthService(userRepository domain.UserRepository, jwtSecret []byte, logs []logging.Logger) domainAuth.AuthService { return &AuthService{ userRepository: userRepository, jwtSecret: jwtSecret, + logs: logs, } } @@ -55,6 +58,8 @@ func (s *AuthService) GetGoogleAuthUrl() string { oauth2.ApprovalForce, ) + logging.LogAll(s.logs, logging.LogLevelDebug, "Generated Google authentication URL: %s", url) + return url } @@ -124,3 +129,53 @@ func generateJwt(userId int, email string, jwtSecret []byte) (string, error) { return tokenString, nil } + +// import ( +// "bytes" +// "time" +// +// "github.com/gin-gonic/gin" +// "github.com/google/uuid" +// "golang.org/x/exp/slog" // or your logging.Logger +// ) +// +// // LoggerMiddleware logs HTTP requests with structured fields +// func LoggerMiddleware(logger *slog.Logger) gin.HandlerFunc { +// return func(c *gin.Context) { +// // Generate request ID for tracing +// reqID := uuid.New().String() +// start := time.Now() +// +// // Capture request body (if enabled) +// var reqBody []byte +// if c.Request.ContentLength > 0 && c.Request.Body != nil { +// reqBody, _ = io.ReadAll(c.Request.Body) +// c.Request.Body.Close() +// c.Request.Body = io.NopCloser(bytes.NewBuffer(reqBody)) +// } +// +// // Log request start +// logger.Info("request started", +// slog.String("req_id", reqID), +// slog.String("method", c.Request.Method), +// slog.String("path", c.Request.URL.Path), +// slog.Int("content_length", int(c.Request.ContentLength)), +// slog.String("user_agent", c.Request.UserAgent()), +// ) +// +// // Process request +// c.Next() +// +// // Log request completion +// duration := time.Since(start) +// logger.Info("request completed", +// slog.String("req_id", reqID), +// slog.Int("status", c.Writer.Status()), +// slog.String("method", c.Request.Method), +// slog.String("path", c.Request.URL.Path), +// slog.Duration("duration", duration), +// slog.Int("size", c.Writer.Size()), +// slog.Any("req_body", string(reqBody)), // truncate if too big +// ) +// } +// } diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go index 7afc797..1f7a308 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -10,6 +10,7 @@ import ( domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainUser "github.com/haydenhargreaves/Potion/internal/domain/user" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" "github.com/joho/godotenv" ) @@ -55,10 +56,10 @@ func IsLoggedIn(ctx *gin.Context) bool { // the event that required fields are not provided, an error will return and the caller should handle // the missing value or panic. Toggles between 'dev', 'prod', etc are also handled by this method, // the values can be access assuming they are the proper values based on the provided environment. -func LoadEnvironment() (*EnvironmentConfig, error) { +func LoadEnvironment(logs []logging.Logger) (*EnvironmentConfig, error) { err := godotenv.Load(".env") if err != nil { - fmt.Printf("No .env file found or error loading .env: %v. Relying on system environment variables.", err) + logging.LogAll(logs, logging.LogLevelWarning, "No .env file found or error loading .env: %v. Relying on system environment variables.", err) } env := os.Getenv("ENVIRONMENT") @@ -130,7 +131,7 @@ func LoadEnvironment() (*EnvironmentConfig, error) { FrontendDomain: frontendDomain, } - fmt.Printf("Environment Config: %+v\n", cfg) + logging.LogAll(logs, logging.LogLevelDebug, "Environment Config: %+v\n", cfg) return cfg, nil } diff --git a/internal/infrastructure/logging/logger.go b/internal/infrastructure/logging/logger.go index a182964..5966a6e 100644 --- a/internal/infrastructure/logging/logger.go +++ b/internal/infrastructure/logging/logger.go @@ -5,12 +5,35 @@ type LogLevel string const ( LogLevelTrace LogLevel = "TRACE" LogLevelDebug LogLevel = "DEBUG" - LogLevelInformation LogLevel = "INFORMATION" + LogLevelInformation LogLevel = "INFO" LogLevelWarning LogLevel = "WARNING" LogLevelError LogLevel = "ERROR" LogLevelFatal LogLevel = "FATAL" ) +const ( + // Background colors + BgBlack = "\033[40m" + BgRed = "\033[41m" + BgGreen = "\033[42m" + BgYellow = "\033[43m" + BgBlue = "\033[44m" + BgMagenta = "\033[45m" + BgCyan = "\033[46m" + BgWhite = "\033[47m" + + // Reset + Reset = "\033[0m" +) + type Logger interface { Log(level LogLevel, format string, v ...any) } + +// LogAll takes all of the inputs for a single logger and executes the logging operation +// on each of the loggers (logs) provided. This is just a convince function. +func LogAll(logs []Logger, level LogLevel, format string, v ...any) { + for _, log := range logs { + log.Log(level, format, v...) + } +} diff --git a/internal/infrastructure/logging/loggers/console_logger.go b/internal/infrastructure/logging/loggers/console_logger.go index 334a7d4..e336b93 100644 --- a/internal/infrastructure/logging/loggers/console_logger.go +++ b/internal/infrastructure/logging/loggers/console_logger.go @@ -4,29 +4,45 @@ import ( "fmt" "io" "os" + "time" "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" ) -// TODO: Implement the Logger interface - type ConsoleLogger struct { writer io.Writer } var _ logging.Logger = (*ConsoleLogger)(nil) -func NewConsoleLogger() ConsoleLogger { - - return ConsoleLogger{ +func NewConsoleLogger() logging.Logger { + return &ConsoleLogger{ writer: os.Stdout, } } -func (l *ConsoleLogger) Log(level logging.LogLevel, format string, v ...any) { - prefix := fmt.Appendf(nil, "[%s] ", level) - bytes := fmt.Appendf(prefix, format, v...) - - // WARN: Do we need to worry about errors? - _, _ = l.writer.Write(bytes) +func formatLevelString(level logging.LogLevel) string { + switch level { + case logging.LogLevelTrace: + return fmt.Sprintf("%s[%s]%s", logging.BgMagenta, level, logging.Reset) + case logging.LogLevelDebug: + return fmt.Sprintf("%s[%s]%s", logging.BgBlue, level, logging.Reset) + case logging.LogLevelInformation: + return fmt.Sprintf("%s[%s]%s", logging.BgGreen, level, logging.Reset) + case logging.LogLevelWarning: + return fmt.Sprintf("%s[%s]%s", logging.BgYellow, level, logging.Reset) + case logging.LogLevelError: + return fmt.Sprintf("%s[%s]%s", logging.BgRed, level, logging.Reset) + case logging.LogLevelFatal: + return fmt.Sprintf("%s[%s]%s", logging.BgRed, level, logging.Reset) + } + return fmt.Sprintf("[%s]", level) +} + +func (l *ConsoleLogger) Log(level logging.LogLevel, format string, v ...any) { + timestamp := time.Now().UTC().Format("01/02/2006 - 15:04:05") + levelStr := formatLevelString(level) + fullFormat := fmt.Sprintf("%-18s %s | %s\n", levelStr, timestamp, format) + bytes := fmt.Appendf(nil, fullFormat, v...) + l.writer.Write(bytes) } diff --git a/internal/infrastructure/logging/loggers/file_logger.go b/internal/infrastructure/logging/loggers/file_logger.go index 721655d..ee56629 100644 --- a/internal/infrastructure/logging/loggers/file_logger.go +++ b/internal/infrastructure/logging/loggers/file_logger.go @@ -1,3 +1,52 @@ package loggers +import ( + "fmt" + "io" + "os" + "time" + + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" +) + // TODO: Implement the Logger interface + +type FileLogger struct { + writer io.Writer +} + +var _ logging.Logger = (*FileLogger)(nil) + +// NewFileLogger creates a new file logger, opened on the filepath provided. If any errors +// occur, an error will be returned, along with an EMPTY logger. This is not a pointer return +// so it will never be nil, just empty. +// +// This function does not close the file, cleanup function that is returned should be called +// to close the file opened in this function. +func NewFileLogger(filepath string) (logging.Logger, func() error, error) { + f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) + if err != nil { + return &FileLogger{}, nil, err + } + + if f == nil { + return &FileLogger{}, nil, fmt.Errorf("File could not be opened. File is nil.") + } + + logger := &FileLogger{ + writer: f, + } + + cleanup := func() error { + return f.Close() + } + + return logger, cleanup, nil +} + +func (l *FileLogger) Log(level logging.LogLevel, format string, v ...any) { + timestamp := time.Now().UTC().Format("01/02/2006 - 15:04:05") + fullFormat := fmt.Sprintf("%-13s %s | %s\n", "["+level+"]", timestamp, format) + bytes := fmt.Appendf(nil, fullFormat, v...) + l.writer.Write(bytes) +} -- 2.47.2 From aca3c8b4ee7d2b0b17bc85f9e186d9342f045b09 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Fri, 23 Jan 2026 10:29:08 -0700 Subject: [PATCH 3/5] (FIX): Logging will be fully implemented later For now, I want to implement a DB logger. --- internal/app/service/auth_service.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/app/service/auth_service.go b/internal/app/service/auth_service.go index a6f04b7..8ef4455 100644 --- a/internal/app/service/auth_service.go +++ b/internal/app/service/auth_service.go @@ -58,8 +58,6 @@ func (s *AuthService) GetGoogleAuthUrl() string { oauth2.ApprovalForce, ) - logging.LogAll(s.logs, logging.LogLevelDebug, "Generated Google authentication URL: %s", url) - return url } -- 2.47.2 From dd43845138c0b13da87b784aba2bc5084bce4675 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 26 Jan 2026 22:41:40 -0700 Subject: [PATCH 4/5] (FEAT): Logger is implemented! However, its not used everywhere and the ENV needs work. Lots of work... --- internal/app/server/middleware.go | 7 +- internal/app/server/middleware_v2.go | 4 +- internal/app/server/server.go | 38 +++++---- .../migrations/014_create_logs_table.sql | 14 ++++ .../database/migrations/100_init_database.sh | 1 + internal/infrastructure/logging/logger.go | 36 +++++++-- .../logging/loggers/console_logger.go | 15 +++- .../logging/loggers/database_logger.go | 77 +++++++++++++++++++ .../logging/loggers/file_logger.go | 12 ++- 9 files changed, 172 insertions(+), 32 deletions(-) create mode 100644 internal/infrastructure/database/migrations/014_create_logs_table.sql create mode 100644 internal/infrastructure/logging/loggers/database_logger.go diff --git a/internal/app/server/middleware.go b/internal/app/server/middleware.go index 16b0966..cd13063 100644 --- a/internal/app/server/middleware.go +++ b/internal/app/server/middleware.go @@ -2,13 +2,13 @@ package server import ( "fmt" - "log" "net/http" "runtime/debug" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" domain "github.com/haydenhargreaves/Potion/internal/domain/server" + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" ) // DepedencyInjectionMiddleware injects the dependencies into the context set. This is a middleware @@ -77,14 +77,13 @@ func JwtAuthMiddleWare(jwtSecretKey []byte) gin.HandlerFunc { } } -func RecoveryMiddleware() gin.HandlerFunc { - +func RecoveryMiddleware(logs []logging.Logger) gin.HandlerFunc { return func(ctx *gin.Context) { defer func() { if r := recover(); r != nil { // Log the panic with stack trace err := fmt.Errorf("panic recovered: %v\n%s", r, debug.Stack()) - log.Printf("[PANIC RECOVERY] %s", err) + logging.LogAll(logs, logging.LogLevelFatal, "[PANIC RECOVERY] %s\n", err) ctx.JSON(http.StatusOK, gin.H{ "status": http.StatusOK, diff --git a/internal/app/server/middleware_v2.go b/internal/app/server/middleware_v2.go index fbf6a9d..1c73d8e 100644 --- a/internal/app/server/middleware_v2.go +++ b/internal/app/server/middleware_v2.go @@ -114,12 +114,12 @@ func LoggingMiddleware(logs []logging.Logger) gin.HandlerFunc { path string = ctx.Request.URL.Path ) - // TODO: Add color to status + // TODO: Add color to status format := "%d | %-14s | %15s | %-9s \"%s\"" logging.LogAll( logs, - logging.LogLevelInformation, + logging.LogLevelInfo, format, status, latency, diff --git a/internal/app/server/server.go b/internal/app/server/server.go index bdc5f72..958d132 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -38,8 +38,8 @@ func Init(port int) *Server { logs: []logging.Logger{}, } - // Default loggers - server.logs = append(server.logs, loggers.NewConsoleLogger()) + // Default logger which logs everything + server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace)) // Some stuff for templ rendering // TODO: Remove this @@ -66,7 +66,8 @@ func (s *Server) Start() { s.Router.Run(fmt.Sprintf(":%d", s.port)) } -// TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... +// TODO: (9/4/2025) Abstract these functions and cleanup. This is fucking messy... +// TODO: (1/26/2026) Abstract these functions and cleanup. This is fucking messy... still func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION cfg, err := domain.LoadEnvironment(s.logs) @@ -88,17 +89,6 @@ func (s *Server) Setup() *Server { gin.SetMode(gin.TestMode) } - // TODO: Implement environment here for logging file - path := "./logs.log" - - fileLogger, cleanup, err := loggers.NewFileLogger(path) - if err != nil { - logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) - } else { - s.logs = append(s.logs, fileLogger) - defer cleanup() - } - // SETUP GOOGLE AUTH var ( // NOTE: USING V2 NOW @@ -126,6 +116,24 @@ func (s *Server) Setup() *Server { s.DB = db + // TODO: Implement environment here for logging file + path := "./logs.log" + + fileLogger, cleanup, err := loggers.NewFileLogger(path, logging.LogLevelDebug) + if err != nil { + logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) + } else { + s.logs = append(s.logs, fileLogger) + defer cleanup() + } + + databaseLogger, err := loggers.NewDatabaseLogger(s.DB, "logs", logging.LogLevelInfo) + if err != nil { + logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create database logger. %s\n", err.Error()) + } else { + s.logs = append(s.logs, databaseLogger) + } + // SETUP JWT jwtSecret := []byte(cfg.JwtSecret) @@ -148,7 +156,7 @@ func (s *Server) Setup() *Server { // Apply middleware // TODO: Review the recovery middleware - s.Router.Use(gin.Recovery(), RecoveryMiddleware(), LoggingMiddleware(s.logs)) + s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs)) // Redirect index to home page: Update this as needed s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) }) diff --git a/internal/infrastructure/database/migrations/014_create_logs_table.sql b/internal/infrastructure/database/migrations/014_create_logs_table.sql new file mode 100644 index 0000000..ef775a3 --- /dev/null +++ b/internal/infrastructure/database/migrations/014_create_logs_table.sql @@ -0,0 +1,14 @@ +-- Author: Hayden Hargreaves (hhargreaves2006@gmail.com) +-- Desc: Create the logs table. +-- Date: 01/23/2026 + +BEGIN; + +CREATE TABLE IF NOT EXISTS Logs ( + Id SERIAL PRIMARY KEY NOT NULL, + Level TEXT NOT NULL CHECK (level IN ('TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR', 'FATAL')), + Message TEXT NOT NULL, + Created TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMIT; diff --git a/internal/infrastructure/database/migrations/100_init_database.sh b/internal/infrastructure/database/migrations/100_init_database.sh index e4893c4..73dd550 100755 --- a/internal/infrastructure/database/migrations/100_init_database.sh +++ b/internal/infrastructure/database/migrations/100_init_database.sh @@ -13,4 +13,5 @@ psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infra psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/011_update_engagement_enum.sql psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/013_update_recipes_allow_large_servings.sql +psql -h "$PSQL_HOST" -U "$PSQL_USERNAME" -d "$PSQL_DATABASE" -f ./internal/infrastructure/database/migrations/014_create_logs_table.sql diff --git a/internal/infrastructure/logging/logger.go b/internal/infrastructure/logging/logger.go index 5966a6e..57a4490 100644 --- a/internal/infrastructure/logging/logger.go +++ b/internal/infrastructure/logging/logger.go @@ -3,14 +3,38 @@ package logging type LogLevel string const ( - LogLevelTrace LogLevel = "TRACE" - LogLevelDebug LogLevel = "DEBUG" - LogLevelInformation LogLevel = "INFO" - LogLevelWarning LogLevel = "WARNING" - LogLevelError LogLevel = "ERROR" - LogLevelFatal LogLevel = "FATAL" + LogLevelTrace LogLevel = "TRACE" + LogLevelDebug LogLevel = "DEBUG" + LogLevelInfo LogLevel = "INFO" + LogLevelWarning LogLevel = "WARN" + LogLevelError LogLevel = "ERROR" + LogLevelFatal LogLevel = "FATAL" ) +// MatchFilter is called on a filter (l) with a target level to match on the filter. Match +// means returning true of the target is greater than OR EQUAL TO the filter level. They order +// by scale of magnitude. +func (filter LogLevel) MatchFilter(target LogLevel) bool { + // Define severity levels (higher number = more severe) + severity := map[LogLevel]int{ + LogLevelTrace: 0, + LogLevelDebug: 1, + LogLevelInfo: 2, + LogLevelWarning: 3, + LogLevelError: 4, + LogLevelFatal: 5, + } + + filterSeverity, filterOk := severity[filter] + targetSeverity, targetOk := severity[target] + + if !filterOk || !targetOk { + return false + } + + return targetSeverity >= filterSeverity +} + const ( // Background colors BgBlack = "\033[40m" diff --git a/internal/infrastructure/logging/loggers/console_logger.go b/internal/infrastructure/logging/loggers/console_logger.go index e336b93..671b12a 100644 --- a/internal/infrastructure/logging/loggers/console_logger.go +++ b/internal/infrastructure/logging/loggers/console_logger.go @@ -11,23 +11,28 @@ import ( type ConsoleLogger struct { writer io.Writer + filter logging.LogLevel } var _ logging.Logger = (*ConsoleLogger)(nil) -func NewConsoleLogger() logging.Logger { +// NewConsoleLogger creates a new logger which writes directly to standard out (stdout). +func NewConsoleLogger(filter logging.LogLevel) logging.Logger { return &ConsoleLogger{ writer: os.Stdout, + filter: filter, } } +// formatLevelString converts a log level string (level) into a new, formatted output string. +// This also includes color, if the shell supports it. Otherwise, the rendering may appear odd. func formatLevelString(level logging.LogLevel) string { switch level { case logging.LogLevelTrace: return fmt.Sprintf("%s[%s]%s", logging.BgMagenta, level, logging.Reset) case logging.LogLevelDebug: return fmt.Sprintf("%s[%s]%s", logging.BgBlue, level, logging.Reset) - case logging.LogLevelInformation: + case logging.LogLevelInfo: return fmt.Sprintf("%s[%s]%s", logging.BgGreen, level, logging.Reset) case logging.LogLevelWarning: return fmt.Sprintf("%s[%s]%s", logging.BgYellow, level, logging.Reset) @@ -39,7 +44,13 @@ func formatLevelString(level logging.LogLevel) string { return fmt.Sprintf("[%s]", level) } +// Log implements the interface. func (l *ConsoleLogger) Log(level logging.LogLevel, format string, v ...any) { + // level is too low, do not log + if !l.filter.MatchFilter(level) { + return + } + timestamp := time.Now().UTC().Format("01/02/2006 - 15:04:05") levelStr := formatLevelString(level) fullFormat := fmt.Sprintf("%-18s %s | %s\n", levelStr, timestamp, format) diff --git a/internal/infrastructure/logging/loggers/database_logger.go b/internal/infrastructure/logging/loggers/database_logger.go new file mode 100644 index 0000000..df4160a --- /dev/null +++ b/internal/infrastructure/logging/loggers/database_logger.go @@ -0,0 +1,77 @@ +package loggers + +import ( + "database/sql" + "fmt" + + "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" +) + +type DatabaseLogger struct { + db *sql.DB + table string + filter logging.LogLevel +} + +var _ logging.Logger = (*DatabaseLogger)(nil) + +func NewDatabaseLogger(conn *sql.DB, table string, filter logging.LogLevel) (logging.Logger, error) { + if conn == nil { + return &DatabaseLogger{}, fmt.Errorf("Connection is nil, something is very wrong.") + } + // Ensure the DB is open + if err := conn.Ping(); err != nil { + return &DatabaseLogger{}, err + } + + // Ensure the table exists + exists, err := tableExists(conn, table) + if err != nil { + return &DatabaseLogger{}, err + } + + if !exists { + return &DatabaseLogger{}, fmt.Errorf("Database table '%s' does not exist on provided connection.", table) + } + + logger := &DatabaseLogger{ + db: conn, + table: table, + filter: filter, + } + + return logger, nil +} + +// tableExists queries a database connection and returns whether the table name provided +// exists on the table. +func tableExists(conn *sql.DB, tableName string) (bool, error) { + var exists bool + err := conn.QueryRow(` + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_schema = 'public' + AND table_name = $1 + )`, + tableName).Scan(&exists) + return exists, err +} + +// Log implements the interface. +func (l *DatabaseLogger) Log(level logging.LogLevel, format string, v ...any) { + // level is too low, do not log + if !l.filter.MatchFilter(level) { + return + } + + message := fmt.Sprintf(format, v...) + query := "INSERT INTO logs (level, message) VALUES ($1, $2);" + + // Ignoring result and error, cuz what the hell would we do with them lol + _, err := l.db.Exec(query, level, message) + + // TODO: Remove + if err != nil { + println(err) + } +} diff --git a/internal/infrastructure/logging/loggers/file_logger.go b/internal/infrastructure/logging/loggers/file_logger.go index ee56629..c125d96 100644 --- a/internal/infrastructure/logging/loggers/file_logger.go +++ b/internal/infrastructure/logging/loggers/file_logger.go @@ -9,10 +9,9 @@ import ( "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" ) -// TODO: Implement the Logger interface - type FileLogger struct { writer io.Writer + filter logging.LogLevel } var _ logging.Logger = (*FileLogger)(nil) @@ -23,7 +22,7 @@ var _ logging.Logger = (*FileLogger)(nil) // // This function does not close the file, cleanup function that is returned should be called // to close the file opened in this function. -func NewFileLogger(filepath string) (logging.Logger, func() error, error) { +func NewFileLogger(filepath string, filter logging.LogLevel) (logging.Logger, func() error, error) { f, err := os.OpenFile(filepath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return &FileLogger{}, nil, err @@ -35,6 +34,7 @@ func NewFileLogger(filepath string) (logging.Logger, func() error, error) { logger := &FileLogger{ writer: f, + filter: filter, } cleanup := func() error { @@ -44,7 +44,13 @@ func NewFileLogger(filepath string) (logging.Logger, func() error, error) { return logger, cleanup, nil } +// Log implements the interface. func (l *FileLogger) Log(level logging.LogLevel, format string, v ...any) { + // level is too low, do not log + if !l.filter.MatchFilter(level) { + return + } + timestamp := time.Now().UTC().Format("01/02/2006 - 15:04:05") fullFormat := fmt.Sprintf("%-13s %s | %s\n", "["+level+"]", timestamp, format) bytes := fmt.Appendf(nil, fullFormat, v...) -- 2.47.2 From a54575b003ec0134c04cce1e403be7370342a6d7 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 26 Jan 2026 23:26:07 -0700 Subject: [PATCH 5/5] (FIX): Fixed up the environent, much needed. Super cool package that uses struct tags to load the environment. Still need to clean up the function in the server/server.go file. --- go.mod | 1 + go.sum | 2 + internal/app/server/server.go | 31 +++++----- internal/domain/server/server.go | 103 +++++-------------------------- 4 files changed, 33 insertions(+), 104 deletions(-) diff --git a/go.mod b/go.mod index dc94d15..2b8eca0 100644 --- a/go.mod +++ b/go.mod @@ -26,6 +26,7 @@ require ( github.com/goccy/go-json v0.10.5 // indirect github.com/google/uuid v1.6.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/kelseyhightower/envconfig v1.4.0 // indirect github.com/klauspost/cpuid/v2 v2.2.10 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect diff --git a/go.sum b/go.sum index dee93ff..a3a4fbc 100644 --- a/go.sum +++ b/go.sum @@ -69,6 +69,8 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= +github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= diff --git a/internal/app/server/server.go b/internal/app/server/server.go index 958d132..f78c781 100644 --- a/internal/app/server/server.go +++ b/internal/app/server/server.go @@ -57,6 +57,9 @@ func Init(port int) *Server { server.config.AllowCredentials = true server.Router.Use(cors.New(server.config)) + // We can use release mode since we don't need Gin's logging + gin.SetMode(gin.ReleaseMode) + return server } @@ -70,7 +73,7 @@ func (s *Server) Start() { // TODO: (1/26/2026) Abstract these functions and cleanup. This is fucking messy... still func (s *Server) Setup() *Server { // SETUP THE ENVIRONMENT CONFIGURATION - cfg, err := domain.LoadEnvironment(s.logs) + cfg, err := domain.LoadEnvironment() if err != nil { logging.LogAll(s.logs, logging.LogLevelFatal, err.Error()) panic(err.Error()) @@ -80,14 +83,7 @@ func (s *Server) Setup() *Server { panic("Environment configuration is nil, crashing.") } - // TODO: Using release on them all? Def need to clean this shitty environment up - if cfg.Environment == "dev" { - gin.SetMode(gin.ReleaseMode) - } else if cfg.Environment == "prod" { - gin.SetMode(gin.ReleaseMode) - } else { - gin.SetMode(gin.TestMode) - } + logging.LogAll(s.logs, logging.LogLevelDebug, "env: %+v\n", cfg) // SETUP GOOGLE AUTH var ( @@ -117,14 +113,16 @@ func (s *Server) Setup() *Server { s.DB = db // TODO: Implement environment here for logging file - path := "./logs.log" - fileLogger, cleanup, err := loggers.NewFileLogger(path, logging.LogLevelDebug) - if err != nil { - logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) - } else { - s.logs = append(s.logs, fileLogger) - defer cleanup() + if cfg.LogFilePath != "" { + fileLogger, cleanup, err := loggers.NewFileLogger(cfg.LogFilePath, logging.LogLevelDebug) + if err != nil { + logging.LogAll(s.logs, logging.LogLevelWarning, "Failed to create file logger. %s\n", err.Error()) + } else { + logging.LogAll(s.logs, logging.LogLevelDebug, "Initialized file logger on file '%s'\n", cfg.LogFilePath) + s.logs = append(s.logs, fileLogger) + defer cleanup() + } } databaseLogger, err := loggers.NewDatabaseLogger(s.DB, "logs", logging.LogLevelInfo) @@ -155,7 +153,6 @@ func (s *Server) Setup() *Server { } // Apply middleware - // TODO: Review the recovery middleware s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs)) // Redirect index to home page: Update this as needed diff --git a/internal/domain/server/server.go b/internal/domain/server/server.go index 1f7a308..58c8c29 100644 --- a/internal/domain/server/server.go +++ b/internal/domain/server/server.go @@ -2,7 +2,6 @@ package domain import ( "fmt" - "os" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt/v5" @@ -10,21 +9,22 @@ import ( domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement" domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe" domainUser "github.com/haydenhargreaves/Potion/internal/domain/user" - "github.com/haydenhargreaves/Potion/internal/infrastructure/logging" "github.com/joho/godotenv" + "github.com/kelseyhightower/envconfig" ) // EnvironmentConfig stores the configuration of the environment. Anything loaded from the .env // or docker environment will be stored here and can be accessed from the InjectedDependencies // struct, which this is attached to. type EnvironmentConfig struct { - GoogleClientId string - GoogleClientSecret string - JwtSecret string - DatabaseUrl string - Environment string - Domain string - FrontendDomain string + GoogleClientId string `envconfig:"GOOGLE_CLIENT_ID" required:"true"` + GoogleClientSecret string `envconfig:"GOOGLE_CLIENT_SECRET" required:"true"` + JwtSecret string `envconfig:"JWT_SECRET" required:"true"` + DatabaseUrl string `envconfig:"DATABASE_URL" required:"true"` + Domain string `envconfig:"DOMAIN" required:"true"` + FrontendDomain string `envconfig:"FRONTEND_DOMAIN" required:"true"` + Environment string `envconfig:"ENVIRONMENT" required:"true"` + LogFilePath string `envconfig:"LOG_FILE_PATH" required:"false"` } // InjectedDependencies is a collection of dependencies that are injected into the application. They @@ -52,87 +52,16 @@ func IsLoggedIn(ctx *gin.Context) bool { return id && email } -// LoadEnvironment loads the environment values from either an .env file or docker environment. In -// the event that required fields are not provided, an error will return and the caller should handle -// the missing value or panic. Toggles between 'dev', 'prod', etc are also handled by this method, -// the values can be access assuming they are the proper values based on the provided environment. -func LoadEnvironment(logs []logging.Logger) (*EnvironmentConfig, error) { - err := godotenv.Load(".env") - if err != nil { - logging.LogAll(logs, logging.LogLevelWarning, "No .env file found or error loading .env: %v. Relying on system environment variables.", err) - } +// LoadEnvironment loads the environment values from either an .env file or docker environment. +func LoadEnvironment() (*EnvironmentConfig, error) { + // NOTE: Does the error return matter? + godotenv.Load(".env") - env := os.Getenv("ENVIRONMENT") - if env == "" { - return nil, fmt.Errorf("ENVIRONMENT environment variable is required.") + cfg := &EnvironmentConfig{} + if err := envconfig.Process("", cfg); err != nil { + return nil, fmt.Errorf("Failed to load environment: %w", err) } - googleClientId := os.Getenv("GOOGLE_CLIENT_ID") - if googleClientId == "" { - return nil, fmt.Errorf("GOOGLE_CLIENT_ID environment variable is required.") - } - - googleClientSecret := os.Getenv("GOOGLE_CLIENT_SECRET") - if googleClientSecret == "" { - return nil, fmt.Errorf("GOOGLE_CLIENT_SECRET environment variable is required.") - } - - jwtSecret := os.Getenv("JWT_SECRET") - if jwtSecret == "" { - return nil, fmt.Errorf("JWT_SECRET environment variable is required.") - } - - var domain string - var frontendDomain string - if env == "dev" { - domain = os.Getenv("DOMAIN_DEV") - if domain == "" { - return nil, fmt.Errorf("DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") - } - frontendDomain = os.Getenv("FRONTEND_DOMAIN_DEV") - if frontendDomain == "" { - return nil, fmt.Errorf("FRONTEND_DOMAIN_DEV environment variable is required when ENVIRONMENT is 'dev'.") - } - } else if env == "prod" { - domain = os.Getenv("DOMAIN_PROD") - if domain == "" { - return nil, fmt.Errorf("DOMAIN_PROD environment variable is required when ENVIRONMENT is 'prod'.") - } - frontendDomain = os.Getenv("FRONTEND_DOMAIN_PROD") - if frontendDomain == "" { - return nil, fmt.Errorf("FRONTEND_DOMAIN_PROD environment variable is required when ENVIRONMENT is 'dev'.") - } - } else { - return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") - } - - var dbUrl string - if env == "dev" { - dbUrl = os.Getenv("DATABASE_URL_DEV") - if dbUrl == "" { - return nil, fmt.Errorf("DATABASE_URL_DEV environment variable is required when ENVIRONMENT is 'dev'.") - } - } else if env == "prod" { - dbUrl = os.Getenv("DATABASE_URL_PROD") - if dbUrl == "" { - return nil, fmt.Errorf("DATABASE_URL_PROD environment variable is required when ENVIRONMENT is 'prod'.") - } - } else { - return nil, fmt.Errorf("ENVIRONMENT environment variable is required and must be 'dev' or 'prod'.") - } - - cfg := &EnvironmentConfig{ - GoogleClientId: googleClientId, - GoogleClientSecret: googleClientSecret, - JwtSecret: jwtSecret, - DatabaseUrl: dbUrl, - Environment: env, - Domain: domain, - FrontendDomain: frontendDomain, - } - - logging.LogAll(logs, logging.LogLevelDebug, "Environment Config: %+v\n", cfg) - return cfg, nil } -- 2.47.2