- Add new `app` package to manage application initialization and lifecycle - Refactor `main.go` to use new application management approach - Implement graceful shutdown with context timeout and signal handling - Add dependency injection container initialization - Enhance logging with configurable log levels and structured logging - Update configuration loading and server initialization process - Modify Jitsi configuration in `.env` for custom deployment - Improve error handling and logging throughout application startup - Centralize application startup and shutdown logic in single package Introduces a more robust and flexible application management system with improved initialization, logging, and shutdown capabilities.
164 lines
4.6 KiB
Go
164 lines
4.6 KiB
Go
package middleware
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"runtime/debug"
|
|
|
|
"attune-heart-therapy/internal/errors"
|
|
"attune-heart-therapy/internal/logger"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ErrorHandlerMiddleware handles errors and panics in a structured way
|
|
func ErrorHandlerMiddleware() gin.HandlerFunc {
|
|
return gin.CustomRecovery(func(c *gin.Context, recovered interface{}) {
|
|
log := logger.New("error_handler")
|
|
|
|
if recovered != nil {
|
|
// Handle panic
|
|
err := fmt.Errorf("panic recovered: %v", recovered)
|
|
log.Error("Panic recovered", err, map[string]interface{}{
|
|
"method": c.Request.Method,
|
|
"path": c.Request.URL.Path,
|
|
"client_ip": c.ClientIP(),
|
|
"user_agent": c.Request.UserAgent(),
|
|
"stack": string(debug.Stack()),
|
|
})
|
|
|
|
// Return internal server error for panics
|
|
appErr := errors.ErrInternalServer.WithDetails("An unexpected error occurred")
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
c.Abort()
|
|
return
|
|
}
|
|
|
|
// Handle regular errors
|
|
if len(c.Errors) > 0 {
|
|
err := c.Errors.Last()
|
|
handleError(c, err.Err, log)
|
|
}
|
|
})
|
|
}
|
|
|
|
// handleError processes different types of errors and returns appropriate responses
|
|
func handleError(c *gin.Context, err error, log *logger.Logger) {
|
|
// Check if it's already an AppError
|
|
if appErr := errors.GetAppError(err); appErr != nil {
|
|
logError(log, c, appErr, appErr.Cause)
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
return
|
|
}
|
|
|
|
// Handle validation errors
|
|
if validationErrs, ok := err.(errors.ValidationErrors); ok {
|
|
appErr := validationErrs.ToAppError()
|
|
logError(log, c, appErr, err)
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
return
|
|
}
|
|
|
|
// Handle other known error types
|
|
appErr := classifyError(err)
|
|
logError(log, c, appErr, err)
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
}
|
|
|
|
// classifyError converts generic errors to AppErrors based on error content
|
|
func classifyError(err error) *errors.AppError {
|
|
errMsg := err.Error()
|
|
|
|
// Database errors
|
|
if containsAny(errMsg, []string{"database", "sql", "connection", "timeout"}) {
|
|
return errors.ErrDatabaseError.WithCause(err)
|
|
}
|
|
|
|
// Network/external API errors
|
|
if containsAny(errMsg, []string{"network", "connection refused", "timeout", "dns"}) {
|
|
return errors.ErrExternalAPI.WithCause(err)
|
|
}
|
|
|
|
// Validation errors
|
|
if containsAny(errMsg, []string{"invalid", "validation", "required", "format"}) {
|
|
return errors.ErrValidationFailed.WithCause(err)
|
|
}
|
|
|
|
// Not found errors
|
|
if containsAny(errMsg, []string{"not found", "does not exist"}) {
|
|
return errors.ErrNotFound.WithCause(err)
|
|
}
|
|
|
|
// Default to internal server error
|
|
return errors.ErrInternalServer.WithCause(err)
|
|
}
|
|
|
|
// logError logs error information with context
|
|
func logError(log *logger.Logger, c *gin.Context, appErr *errors.AppError, cause error) {
|
|
fields := map[string]interface{}{
|
|
"error_code": appErr.Code,
|
|
"method": c.Request.Method,
|
|
"path": c.Request.URL.Path,
|
|
"client_ip": c.ClientIP(),
|
|
"user_agent": c.Request.UserAgent(),
|
|
"status": appErr.HTTPStatus,
|
|
}
|
|
|
|
// Add user ID if available
|
|
if userID, exists := c.Get("user_id"); exists {
|
|
fields["user_id"] = userID
|
|
}
|
|
|
|
// Add trace ID if available
|
|
if traceID, exists := c.Get("trace_id"); exists {
|
|
fields["trace_id"] = traceID
|
|
}
|
|
|
|
// Add error fields if available
|
|
if appErr.Fields != nil {
|
|
for k, v := range appErr.Fields {
|
|
fields["error_"+k] = v
|
|
}
|
|
}
|
|
|
|
// Log based on severity
|
|
if appErr.HTTPStatus >= 500 {
|
|
log.Error("Server error occurred", cause, fields)
|
|
} else if appErr.HTTPStatus >= 400 {
|
|
log.Warn("Client error occurred", fields)
|
|
} else {
|
|
log.Info("Request completed with error", fields)
|
|
}
|
|
}
|
|
|
|
// containsAny checks if a string contains any of the given substrings
|
|
func containsAny(s string, substrings []string) bool {
|
|
for _, substr := range substrings {
|
|
if len(s) >= len(substr) {
|
|
for i := 0; i <= len(s)-len(substr); i++ {
|
|
if s[i:i+len(substr)] == substr {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// NotFoundHandler handles 404 errors
|
|
func NotFoundHandler() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
appErr := errors.ErrNotFound.WithDetails(fmt.Sprintf("Route %s %s not found", c.Request.Method, c.Request.URL.Path))
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
}
|
|
}
|
|
|
|
// MethodNotAllowedHandler handles 405 errors
|
|
func MethodNotAllowedHandler() gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
appErr := errors.New(errors.ErrCodeValidationFailed, "Method not allowed", http.StatusMethodNotAllowed).
|
|
WithDetails(fmt.Sprintf("Method %s not allowed for route %s", c.Request.Method, c.Request.URL.Path))
|
|
c.JSON(appErr.HTTPStatus, appErr.ToErrorResponse())
|
|
}
|
|
}
|