Add email logging and SMTP health check

- Add structured logging for email sending (success/failure)
- Add HealthCheck method to EmailService interface
- Extend /health endpoint to include SMTP status
- Update all email service mocks to implement HealthCheck
- SMTP status shows: ok, error, or disabled
This commit is contained in:
2025-12-03 20:41:40 +01:00
parent 935f742ac9
commit 568ba3b016
7 changed files with 60 additions and 5 deletions
+1 -1
View File
@@ -116,7 +116,7 @@ func main() {
authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator) authHandlers := httpInfra.NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator) habitHandlers := httpInfra.NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler, translator) statsHandlers := httpInfra.NewStatsHandlers(getHabitStatsHandler, translator)
healthHandlers := httpInfra.NewHealthHandlers(db.Conn()) healthHandlers := httpInfra.NewHealthHandlers(db.Conn(), emailService)
userHandlers := httpInfra.NewUserHandlers(deleteUserHandler, translator) userHandlers := httpInfra.NewUserHandlers(deleteUserHandler, translator)
router := httpInfra.NewRouter(cfg.AppURL, habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator) router := httpInfra.NewRouter(cfg.AppURL, habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)
@@ -85,6 +85,10 @@ func (m *mockRequestResetEmailService) Send(message services.EmailMessage) error
return nil return nil
} }
func (m *mockRequestResetEmailService) HealthCheck() error {
return nil
}
func TestRequestPasswordResetHandler_Success(t *testing.T) { func TestRequestPasswordResetHandler_Success(t *testing.T) {
user := entities.NewUser("test@example.com", "hash") user := entities.NewUser("test@example.com", "hash")
user.ID = "user-123" user.ID = "user-123"
@@ -65,6 +65,10 @@ func (m *mockEmailService) Send(message services.EmailMessage) error {
return nil return nil
} }
func (m *mockEmailService) HealthCheck() error {
return nil
}
func TestVerifyEmailHandler_Success(t *testing.T) { func TestVerifyEmailHandler_Success(t *testing.T) {
token := "valid-token" token := "valid-token"
expiry := time.Now().Add(24 * time.Hour) expiry := time.Now().Add(24 * time.Hour)
@@ -9,4 +9,5 @@ type EmailMessage struct {
type EmailService interface { type EmailService interface {
Send(message EmailMessage) error Send(message EmailMessage) error
HealthCheck() error
} }
@@ -3,6 +3,7 @@ package email
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"log"
"strings" "strings"
"time" "time"
@@ -53,9 +54,11 @@ func (s *SMTPService) Send(message services.EmailMessage) error {
} }
if err := s.sendWithRetry(dialer, m); err != nil { if err := s.sendWithRetry(dialer, m); err != nil {
log.Printf("[EMAIL] status=failed to=%s subject=%q error=%q", message.To, message.Subject, err.Error())
return fmt.Errorf("failed to send email: %w", err) return fmt.Errorf("failed to send email: %w", err)
} }
log.Printf("[EMAIL] status=sent to=%s subject=%q", message.To, message.Subject)
return nil return nil
} }
@@ -103,3 +106,28 @@ func isConfigError(err error) bool {
func (s *SMTPService) GetConfig() SMTPConfig { func (s *SMTPService) GetConfig() SMTPConfig {
return s.config return s.config
} }
func (s *SMTPService) HealthCheck() error {
dialer := mail.NewDialer(s.config.Host, s.config.Port, s.config.Username, s.config.Password)
dialer.TLSConfig = &tls.Config{
ServerName: s.config.Host,
}
if s.config.Port == 465 {
dialer.SSL = true
}
smtpCloser, err := dialer.Dial()
if err != nil {
if isAuthError(err) {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
if isConfigError(err) {
return fmt.Errorf("SMTP connection failed: %w", err)
}
return fmt.Errorf("SMTP error: %w", err)
}
defer smtpCloser.Close()
return nil
}
@@ -1,6 +1,7 @@
package http package http
import ( import (
"apocapoc-api/internal/domain/services"
"database/sql" "database/sql"
"net/http" "net/http"
"time" "time"
@@ -9,18 +10,21 @@ import (
var startTime = time.Now() var startTime = time.Now()
type HealthHandlers struct { type HealthHandlers struct {
db *sql.DB db *sql.DB
emailService services.EmailService
} }
func NewHealthHandlers(db *sql.DB) *HealthHandlers { func NewHealthHandlers(db *sql.DB, emailService services.EmailService) *HealthHandlers {
return &HealthHandlers{ return &HealthHandlers{
db: db, db: db,
emailService: emailService,
} }
} }
type HealthResponse struct { type HealthResponse struct {
Status string `json:"status"` Status string `json:"status"`
Database string `json:"database"` Database string `json:"database"`
SMTP string `json:"smtp"`
Uptime string `json:"uptime"` Uptime string `json:"uptime"`
} }
@@ -34,6 +38,7 @@ type HealthResponse struct {
// @Router /health [get] // @Router /health [get]
func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) { func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) {
dbStatus := "ok" dbStatus := "ok"
smtpStatus := "ok"
overallStatus := "ok" overallStatus := "ok"
statusCode := http.StatusOK statusCode := http.StatusOK
@@ -43,12 +48,25 @@ func (h *HealthHandlers) Health(w http.ResponseWriter, r *http.Request) {
statusCode = http.StatusServiceUnavailable statusCode = http.StatusServiceUnavailable
} }
if h.emailService != nil {
if err := h.emailService.HealthCheck(); err != nil {
smtpStatus = "error"
if overallStatus != "degraded" {
overallStatus = "degraded"
statusCode = http.StatusServiceUnavailable
}
}
} else {
smtpStatus = "disabled"
}
uptime := time.Since(startTime) uptime := time.Since(startTime)
uptimeStr := formatDuration(uptime) uptimeStr := formatDuration(uptime)
response := HealthResponse{ response := HealthResponse{
Status: overallStatus, Status: overallStatus,
Database: dbStatus, Database: dbStatus,
SMTP: smtpStatus,
Uptime: uptimeStr, Uptime: uptimeStr,
} }
@@ -72,7 +72,7 @@ func setupTestServer(t *testing.T) *TestServer {
authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator) authHandlers := NewAuthHandlers(registerHandler, loginHandler, refreshTokenHandler, revokeTokenHandler, revokeAllTokensHandler, verifyEmailHandler, resendVerificationEmailHandler, requestPasswordResetHandler, resetPasswordHandler, jwtService, refreshTokenRepo, refreshTokenExpiry, translator)
habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator) habitHandlers := NewHabitHandlers(createHandler, getTodaysHandler, getUserHabitsHandler, getHabitByIDHandler, getHabitEntriesHandler, updateHandler, archiveHandler, markHandler, unmarkHandler, translator)
statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator) statsHandlers := NewStatsHandlers(getHabitStatsHandler, translator)
healthHandlers := NewHealthHandlers(db) healthHandlers := NewHealthHandlers(db, nil)
userHandlers := NewUserHandlers(deleteUserHandler, translator) userHandlers := NewUserHandlers(deleteUserHandler, translator)
router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator) router := NewRouter("http://localhost:3000", habitHandlers, authHandlers, statsHandlers, healthHandlers, userHandlers, jwtService, translator)