Enhance notification APIs with HTML email support, CC/BCC, and structured logging
Add comprehensive improvements across REST and gRPC APIs: - Add structured logging for all notification operations - Implement HTML email support with multipart/alternative MIME - Add CC and BCC recipient support for email notifications - Add GetNotifiers endpoint to query available notifier configurations - Support configurable From name in SMTP configuration - Auto-detect content type (text vs HTML) in notification bodies - Improve error handling and validation across all endpoints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/igodwin/notifier/internal/domain"
|
||||
"github.com/igodwin/notifier/internal/logging"
|
||||
)
|
||||
|
||||
// AccountResolver is an interface for resolving default accounts
|
||||
@@ -24,10 +25,11 @@ type NotificationService struct {
|
||||
workerCount int
|
||||
stopChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
logger *logging.Logger
|
||||
}
|
||||
|
||||
// NewNotificationService creates a new notification service
|
||||
func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue, workerCount int, accountResolver AccountResolver) *NotificationService {
|
||||
func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue, workerCount int, accountResolver AccountResolver, logger *logging.Logger) *NotificationService {
|
||||
if workerCount <= 0 {
|
||||
workerCount = 10
|
||||
}
|
||||
@@ -39,6 +41,7 @@ func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue,
|
||||
notifications: make(map[string]*domain.Notification),
|
||||
workerCount: workerCount,
|
||||
stopChan: make(chan struct{}),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,6 +100,9 @@ func (s *NotificationService) worker(ctx context.Context, id int) {
|
||||
func (s *NotificationService) processNotification(ctx context.Context, msg *domain.QueueMessage) {
|
||||
notification := msg.Notification
|
||||
|
||||
s.logger.Debugf("Processing notification - id=%s, type=%s, recipients=%d",
|
||||
notification.ID, notification.Type, len(notification.Recipients))
|
||||
|
||||
// Resolve account if not specified
|
||||
account := notification.Account
|
||||
if account == "" && s.accountResolver != nil {
|
||||
@@ -106,6 +112,8 @@ func (s *NotificationService) processNotification(ctx context.Context, msg *doma
|
||||
// Get the appropriate notifier
|
||||
notifier, err := s.factory.Create(notification.Type, account)
|
||||
if err != nil {
|
||||
s.logger.Errorf("Failed to create notifier - id=%s, type=%s, account=%s, error=%v",
|
||||
notification.ID, notification.Type, account, err)
|
||||
notification.Status = domain.StatusFailed
|
||||
notification.LastError = fmt.Sprintf("failed to create notifier: %v", err)
|
||||
s.queue.Nack(ctx, msg.ID, false)
|
||||
@@ -125,9 +133,13 @@ func (s *NotificationService) processNotification(ctx context.Context, msg *doma
|
||||
// Check if we should retry
|
||||
if notification.RetryCount < notification.MaxRetries {
|
||||
notification.Status = domain.StatusRetrying
|
||||
s.logger.Warnf("Notification send failed, will retry - id=%s, type=%s, account=%s, attempt=%d/%d, error=%s",
|
||||
notification.ID, notification.Type, account, notification.RetryCount, notification.MaxRetries, notification.LastError)
|
||||
s.queue.Nack(ctx, msg.ID, true) // Requeue
|
||||
} else {
|
||||
notification.Status = domain.StatusFailed
|
||||
s.logger.Errorf("Notification send failed permanently - id=%s, type=%s, account=%s, recipients=%v, attempts=%d, error=%s",
|
||||
notification.ID, notification.Type, account, notification.Recipients, notification.RetryCount, notification.LastError)
|
||||
s.queue.Nack(ctx, msg.ID, false) // Don't requeue
|
||||
}
|
||||
} else {
|
||||
@@ -135,6 +147,8 @@ func (s *NotificationService) processNotification(ctx context.Context, msg *doma
|
||||
now := time.Now()
|
||||
notification.SentAt = &now
|
||||
s.queue.Ack(ctx, msg.ID)
|
||||
s.logger.Infof("Notification sent successfully - id=%s, type=%s, account=%s, recipients=%v",
|
||||
notification.ID, notification.Type, account, notification.Recipients)
|
||||
}
|
||||
|
||||
s.updateNotification(notification)
|
||||
@@ -302,6 +316,30 @@ func (s *NotificationService) GetStats(ctx context.Context) (*domain.Notificatio
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
// GetNotifiers returns information about available notifiers
|
||||
func (s *NotificationService) GetNotifiers(ctx context.Context) (*domain.NotifiersResponse, error) {
|
||||
supportedTypes := s.factory.SupportedTypes()
|
||||
notifiers := make([]domain.NotifierInfo, 0, len(supportedTypes))
|
||||
|
||||
for _, notifType := range supportedTypes {
|
||||
accounts := s.factory.GetAccounts(notifType)
|
||||
defaultAccount := ""
|
||||
if s.accountResolver != nil {
|
||||
defaultAccount = s.accountResolver.GetDefaultAccount(notifType)
|
||||
}
|
||||
|
||||
notifiers = append(notifiers, domain.NotifierInfo{
|
||||
Type: notifType,
|
||||
Accounts: accounts,
|
||||
DefaultAccount: defaultAccount,
|
||||
})
|
||||
}
|
||||
|
||||
return &domain.NotifiersResponse{
|
||||
Notifiers: notifiers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// storeNotification stores a notification in memory
|
||||
func (s *NotificationService) storeNotification(notification *domain.Notification) {
|
||||
s.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user