Merging dev into master #80

Merged
azpect merged 7 commits from dev into master 2026-01-26 23:34:00 -07:00
9 changed files with 172 additions and 32 deletions
Showing only changes of commit dd43845138 - Show all commits

View File

@ -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,

View File

@ -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,

View File

@ -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) })

View File

@ -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;

View File

@ -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

View File

@ -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"

View File

@ -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)

View File

@ -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)
}
}

View File

@ -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...)