(FEAT): Logging is so much better now :)

This commit is contained in:
Hayden Hargreaves 2026-01-23 10:25:51 -07:00
parent 745a59ecaa
commit 72c9cb0f96
10 changed files with 240 additions and 38 deletions

View File

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

1
go.mod
View File

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

2
go.sum
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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