Merge pull request 'Merging dev into master' (#80) from dev into master
All checks were successful
Deploy application with Docker / build_and_deploy (push) Successful in 1m7s
All checks were successful
Deploy application with Docker / build_and_deploy (push) Successful in 1m7s
Reviewed-on: #80
This commit is contained in:
commit
a78a79bfab
2
go.mod
2
go.mod
@ -24,7 +24,9 @@ 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/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
|
||||
|
||||
4
go.sum
4
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=
|
||||
@ -67,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=
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
@ -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.LogLevelInfo,
|
||||
format,
|
||||
status,
|
||||
latency,
|
||||
client,
|
||||
method,
|
||||
path,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 logger which logs everything
|
||||
server.logs = append(server.logs, loggers.NewConsoleLogger(logging.LogLevelTrace))
|
||||
|
||||
// Some stuff for templ rendering
|
||||
// TODO: Remove this
|
||||
htmlRenderer := server.Router.HTMLRender
|
||||
server.Router.HTMLRender = &gintemplrenderer.HTMLTemplRenderer{FallbackHtmlRenderer: htmlRenderer}
|
||||
|
||||
@ -49,32 +57,33 @@ 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
|
||||
}
|
||||
|
||||
// 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...
|
||||
// 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()
|
||||
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.")
|
||||
}
|
||||
|
||||
if cfg.Environment == "dev" {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} 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 (
|
||||
@ -103,6 +112,26 @@ func (s *Server) Setup() *Server {
|
||||
|
||||
s.DB = db
|
||||
|
||||
// TODO: Implement environment here for logging file
|
||||
|
||||
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)
|
||||
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)
|
||||
|
||||
@ -111,7 +140,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 +153,7 @@ func (s *Server) Setup() *Server {
|
||||
}
|
||||
|
||||
// Apply middleware
|
||||
s.Router.Use(RecoveryMiddleware())
|
||||
// NOTE: No longer running on every connection!
|
||||
// s.Router.Use(JwtAuthMiddleWare(jwtSecret))
|
||||
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) })
|
||||
@ -219,14 +246,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,3 +127,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
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
|
||||
@ -2,7 +2,6 @@ package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@ -11,19 +10,21 @@ import (
|
||||
domainRecipe "github.com/haydenhargreaves/Potion/internal/domain/recipe"
|
||||
domainUser "github.com/haydenhargreaves/Potion/internal/domain/user"
|
||||
"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
|
||||
@ -51,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.
|
||||
// LoadEnvironment loads the environment values from either an .env file or docker environment.
|
||||
func LoadEnvironment() (*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)
|
||||
}
|
||||
// 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,
|
||||
}
|
||||
|
||||
fmt.Printf("Environment Config: %+v\n", cfg)
|
||||
|
||||
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/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