From dd43845138c0b13da87b784aba2bc5084bce4675 Mon Sep 17 00:00:00 2001 From: Hayden Hargreaves Date: Mon, 26 Jan 2026 22:41:40 -0700 Subject: [PATCH] (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...)