Merge pull request 'MERGING LOGGER INTO DEV' (#78) from feature/logger into dev
Reviewed-on: #78
This commit is contained in:
commit
bd961beb3a
1
go.mod
1
go.mod
@ -24,6 +24,7 @@ require (
|
|||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // 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/json-iterator/go v1.1.12 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
|||||||
2
go.sum
2
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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
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/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 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
|||||||
@ -2,13 +2,13 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
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
|
// 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) {
|
return func(ctx *gin.Context) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
// Log the panic with stack trace
|
// Log the panic with stack trace
|
||||||
err := fmt.Errorf("panic recovered: %v\n%s", r, debug.Stack())
|
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{
|
ctx.JSON(http.StatusOK, gin.H{
|
||||||
"status": http.StatusOK,
|
"status": http.StatusOK,
|
||||||
|
|||||||
@ -3,10 +3,12 @@ package server
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/golang-jwt/jwt/v5"
|
"github.com/golang-jwt/jwt/v5"
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
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
|
// 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
|
// JwtOptionalAuthMiddlewareV2 is responsible for collecting user data for routes where
|
||||||
// authentication is optional. Meaning: if the use is not logged in, this function does
|
// 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
|
// not fail or return, it simply does nothing. But if the user is logged in, then the
|
||||||
// 'userId' and 'userEmail' context values are set.
|
// 'userId' and 'userEmail' context values are set.
|
||||||
//
|
//
|
||||||
// e.g., `userIdAny, exists := ctx.Get("userId")`
|
// e.g., `userIdAny, exists := ctx.Get("userId")`
|
||||||
@ -96,3 +98,34 @@ func JwtOptionalAuthMiddlewareV2(jwtSecretKey []byte) gin.HandlerFunc {
|
|||||||
ctx.Next()
|
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.LogLevelInfo,
|
||||||
|
format,
|
||||||
|
status,
|
||||||
|
latency,
|
||||||
|
client,
|
||||||
|
method,
|
||||||
|
path,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import (
|
|||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/database/repository"
|
"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"
|
_ "github.com/lib/pq"
|
||||||
)
|
)
|
||||||
@ -23,18 +25,24 @@ type Server struct {
|
|||||||
config cors.Config
|
config cors.Config
|
||||||
DB *sql.DB
|
DB *sql.DB
|
||||||
deps domain.InjectedDependencies
|
deps domain.InjectedDependencies
|
||||||
|
logs []logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the server with the provided port. CORS settings are defined here.
|
// 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.
|
// A pointer to a server object is returned which allows for method chaining.
|
||||||
func Init(port int) *Server {
|
func Init(port int) *Server {
|
||||||
server := &Server{
|
server := &Server{
|
||||||
Router: gin.Default(),
|
Router: gin.New(), // Not default anymore, to allow for custom logger
|
||||||
port: port,
|
port: port,
|
||||||
config: cors.DefaultConfig(),
|
config: cors.DefaultConfig(),
|
||||||
|
logs: []logging.Logger{},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default logger which logs everything
|
||||||
|
server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace))
|
||||||
|
|
||||||
// Some stuff for templ rendering
|
// Some stuff for templ rendering
|
||||||
|
// TODO: Remove this
|
||||||
htmlRenderer := server.Router.HTMLRender
|
htmlRenderer := server.Router.HTMLRender
|
||||||
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
|
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
|
||||||
|
|
||||||
@ -54,22 +62,27 @@ func Init(port int) *Server {
|
|||||||
|
|
||||||
// Start starts the server on the port provided when the server was initialized
|
// Start starts the server on the port provided when the server was initialized
|
||||||
func (s *Server) Start() {
|
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))
|
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 {
|
func (s *Server) Setup() *Server {
|
||||||
// SETUP THE ENVIRONMENT CONFIGURATION
|
// SETUP THE ENVIRONMENT CONFIGURATION
|
||||||
cfg, err := domain.LoadEnvironment()
|
cfg, err := domain.LoadEnvironment(s.logs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
logging.LogAll(s.logs, logging.LogLevelFatal, err.Error())
|
||||||
panic(err.Error())
|
panic(err.Error())
|
||||||
}
|
}
|
||||||
if cfg == nil {
|
if cfg == nil {
|
||||||
|
logging.LogAll(s.logs, logging.LogLevelFatal, "Environment configuration is nil, crashing.")
|
||||||
panic("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" {
|
if cfg.Environment == "dev" {
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
} else if cfg.Environment == "prod" {
|
} else if cfg.Environment == "prod" {
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
} else {
|
} else {
|
||||||
@ -103,6 +116,24 @@ func (s *Server) Setup() *Server {
|
|||||||
|
|
||||||
s.DB = db
|
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
|
// SETUP JWT
|
||||||
jwtSecret := []byte(cfg.JwtSecret)
|
jwtSecret := []byte(cfg.JwtSecret)
|
||||||
|
|
||||||
@ -111,7 +142,7 @@ func (s *Server) Setup() *Server {
|
|||||||
recipeRepo := repository.NewRecipeRepository(s.DB)
|
recipeRepo := repository.NewRecipeRepository(s.DB)
|
||||||
engagementRepo := repository.NewEngagementRepository(s.DB)
|
engagementRepo := repository.NewEngagementRepository(s.DB)
|
||||||
userService := service.NewUserService(userRepo)
|
userService := service.NewUserService(userRepo)
|
||||||
authService := service.NewAuthService(userRepo, jwtSecret)
|
authService := service.NewAuthService(userRepo, jwtSecret, s.logs)
|
||||||
recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
|
recipeService := service.NewRecipeService(recipeRepo, engagementRepo)
|
||||||
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
|
engagementService := service.NewEngagementService(engagementRepo, recipeRepo)
|
||||||
|
|
||||||
@ -124,9 +155,8 @@ func (s *Server) Setup() *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Apply middleware
|
// Apply middleware
|
||||||
s.Router.Use(RecoveryMiddleware())
|
// TODO: Review the recovery middleware
|
||||||
// NOTE: No longer running on every connection!
|
s.Router.Use(gin.Recovery(), RecoveryMiddleware(s.logs), LoggingMiddleware(s.logs))
|
||||||
// s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
|
||||||
|
|
||||||
// Redirect index to home page: Update this as needed
|
// Redirect index to home page: Update this as needed
|
||||||
s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
|
s.Router.GET("/", func(ctx *gin.Context) { ctx.Redirect(http.StatusSeeOther, domain.WEB_HOME) })
|
||||||
@ -219,14 +249,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/made", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserMadeRecipesV2)
|
||||||
router_api_v2.GET("/user/recipes/viewed", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.GetAuthenicatedUserViewedRecipesV2)
|
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/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/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/favorite/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementFavoriteRecipeHandlerV2)
|
||||||
router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2)
|
router_api_v2.POST("/engagement/make/:id", JwtAuthMiddlewareV2([]byte(cfg.JwtSecret)), s.EngagementMakeRecipeHandlerV2)
|
||||||
|
|
||||||
|
if cfg.Environment == "dev" {
|
||||||
|
s.debugDisplayRoutes()
|
||||||
|
}
|
||||||
|
|
||||||
return s
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
domainServer "github.com/haydenhargreaves/Potion/internal/domain/server"
|
||||||
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
domain "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/auth"
|
||||||
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,6 +32,7 @@ import (
|
|||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
userRepository domain.UserRepository
|
userRepository domain.UserRepository
|
||||||
jwtSecret []byte
|
jwtSecret []byte
|
||||||
|
logs []logging.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compile-time check to ensure the AuthService implements domain.AuthService
|
// 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
|
// 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.
|
// 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{
|
return &AuthService{
|
||||||
userRepository: userRepository,
|
userRepository: userRepository,
|
||||||
jwtSecret: jwtSecret,
|
jwtSecret: jwtSecret,
|
||||||
|
logs: logs,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -124,3 +127,53 @@ func generateJwt(userId int, email string, jwtSecret []byte) (string, error) {
|
|||||||
|
|
||||||
return tokenString, nil
|
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
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
domainEngagement "github.com/haydenhargreaves/Potion/internal/domain/engagement"
|
||||||
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||||
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
"github.com/joho/godotenv"
|
"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 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 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.
|
// 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")
|
err := godotenv.Load(".env")
|
||||||
if err != nil {
|
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")
|
env := os.Getenv("ENVIRONMENT")
|
||||||
@ -130,7 +131,7 @@ func LoadEnvironment() (*EnvironmentConfig, error) {
|
|||||||
FrontendDomain: frontendDomain,
|
FrontendDomain: frontendDomain,
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("Environment Config: %+v\n", cfg)
|
logging.LogAll(logs, logging.LogLevelDebug, "Environment Config: %+v\n", cfg)
|
||||||
|
|
||||||
return cfg, nil
|
return cfg, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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/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/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
|
||||||
|
|
||||||
|
|||||||
63
internal/infrastructure/logging/logger.go
Normal file
63
internal/infrastructure/logging/logger.go
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
package logging
|
||||||
|
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
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"
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
internal/infrastructure/logging/loggers/console_logger.go
Normal file
59
internal/infrastructure/logging/loggers/console_logger.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package loggers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConsoleLogger struct {
|
||||||
|
writer io.Writer
|
||||||
|
filter logging.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ logging.Logger = (*ConsoleLogger)(nil)
|
||||||
|
|
||||||
|
// 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.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)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
bytes := fmt.Appendf(nil, fullFormat, v...)
|
||||||
|
l.writer.Write(bytes)
|
||||||
|
}
|
||||||
77
internal/infrastructure/logging/loggers/database_logger.go
Normal file
77
internal/infrastructure/logging/loggers/database_logger.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/infrastructure/logging/loggers/file_logger.go
Normal file
58
internal/infrastructure/logging/loggers/file_logger.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package loggers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/haydenhargreaves/Potion/internal/infrastructure/logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type FileLogger struct {
|
||||||
|
writer io.Writer
|
||||||
|
filter logging.LogLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
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, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if f == nil {
|
||||||
|
return &FileLogger{}, nil, fmt.Errorf("File could not be opened. File is nil.")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := &FileLogger{
|
||||||
|
writer: f,
|
||||||
|
filter: filter,
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup := func() error {
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
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...)
|
||||||
|
l.writer.Write(bytes)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user