Use ISO 8601 format for server logging

This commit is contained in:
2025-10-16 21:28:38 -07:00
parent 9087a710e5
commit 0749c6a6d3
3 changed files with 204 additions and 36 deletions
BIN
View File
Binary file not shown.
+46 -36
View File
@@ -3,7 +3,6 @@ package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
@@ -15,6 +14,7 @@ import (
"github.com/igodwin/notifier/api/rest"
"github.com/igodwin/notifier/internal/config"
"github.com/igodwin/notifier/internal/domain"
"github.com/igodwin/notifier/internal/logging"
"github.com/igodwin/notifier/internal/notifier"
"github.com/igodwin/notifier/internal/queue"
"github.com/igodwin/notifier/internal/service"
@@ -26,11 +26,21 @@ func main() {
// Load configuration
cfg, err := config.Load("")
if err != nil {
log.Printf("Warning: failed to load config, using defaults: %v", err)
// Use basic logger before we have config
logger, _ := logging.NewFromConfig("info", "stdout")
logger.Warnf("Failed to load config, using defaults: %v", err)
cfg = getDefaultConfig()
}
log.Printf("Starting Notifier Service in mode: %s", cfg.Server.Mode)
// Create logger from config
logger, err := logging.NewFromConfig(cfg.Logging.Level, cfg.Logging.OutputPath)
if err != nil {
// Fallback to stdout if log file can't be opened
logger, _ = logging.NewFromConfig(cfg.Logging.Level, "stdout")
logger.Warnf("Failed to open log file, using stdout: %v", err)
}
logger.Infof("Starting Notifier Service in mode: %s", cfg.Server.Mode)
// Create context
ctx, cancel := context.WithCancel(context.Background())
@@ -41,32 +51,32 @@ func main() {
if cfg.Queue.Type == "local" {
q, err = queue.NewLocalQueue(cfg.Queue.Local)
if err != nil {
log.Fatalf("Failed to create queue: %v", err)
logger.Fatalf("Failed to create queue: %v", err)
}
log.Println("Using local queue")
logger.Info("Using local queue")
} else {
log.Fatalf("Queue type %s not implemented yet", cfg.Queue.Type)
logger.Fatalf("Queue type %s not implemented yet", cfg.Queue.Type)
}
// Initialize notifier factory and register notifiers
factory := notifier.NewFactory()
registerNotifiers(cfg, factory)
registerNotifiers(cfg, factory, logger)
// Check if any notifiers are registered
if len(factory.SupportedTypes()) == 0 {
log.Fatal("No notifiers configured. Please enable at least one notifier in config.yaml")
logger.Fatal("No notifiers configured. Please enable at least one notifier in config.yaml")
}
log.Printf("Supported notification types: %v", factory.SupportedTypes())
logger.Infof("Supported notification types: %v", factory.SupportedTypes())
// Create notification service
svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount)
// Start workers
if err := svc.Start(ctx); err != nil {
log.Fatalf("Failed to start service: %v", err)
logger.Fatalf("Failed to start service: %v", err)
}
log.Printf("Started %d worker(s)", cfg.Queue.WorkerCount)
logger.Infof("Started %d worker(s)", cfg.Queue.WorkerCount)
// Wait group for both servers
var wg sync.WaitGroup
@@ -75,14 +85,14 @@ func main() {
var grpcServer *grpc.Server
if cfg.Server.Mode == "both" || cfg.Server.Mode == "grpc" {
wg.Add(1)
grpcServer = startGRPCServer(ctx, &wg, cfg, svc)
grpcServer = startGRPCServer(ctx, &wg, cfg, svc, logger)
}
// Start REST server if enabled
var restServer *http.Server
if cfg.Server.Mode == "both" || cfg.Server.Mode == "rest" {
wg.Add(1)
restServer = startRESTServer(ctx, &wg, cfg, svc)
restServer = startRESTServer(ctx, &wg, cfg, svc, logger)
}
// Wait for interrupt signal
@@ -90,7 +100,7 @@ func main() {
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
<-sigChan
log.Println("Shutting down servers...")
logger.Info("Shutting down servers...")
// Graceful shutdown
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
@@ -99,7 +109,7 @@ func main() {
// Stop REST server
if restServer != nil {
if err := restServer.Shutdown(shutdownCtx); err != nil {
log.Printf("Error during REST server shutdown: %v", err)
logger.Errorf("Error during REST server shutdown: %v", err)
}
}
@@ -113,64 +123,64 @@ func main() {
// Stop service
if err := svc.Stop(); err != nil {
log.Printf("Error stopping service: %v", err)
logger.Errorf("Error stopping service: %v", err)
}
log.Println("Servers stopped")
logger.Info("Servers stopped")
}
func registerNotifiers(cfg *config.Config, factory *notifier.Factory) {
func registerNotifiers(cfg *config.Config, factory *notifier.Factory, logger *logging.Logger) {
if cfg.Notifiers.Stdout {
stdoutNotifier := notifier.NewStdoutNotifier()
if err := factory.RegisterNotifier(domain.TypeStdout, stdoutNotifier); err != nil {
log.Fatalf("Failed to register stdout notifier: %v", err)
logger.Fatalf("Failed to register stdout notifier: %v", err)
}
log.Println("Registered stdout notifier")
logger.Info("Registered stdout notifier")
}
if cfg.Notifiers.SMTP != nil {
smtpNotifier, err := notifier.NewSMTPNotifier(cfg.Notifiers.SMTP)
if err != nil {
log.Printf("Warning: failed to create SMTP notifier: %v", err)
logger.Warnf("Failed to create SMTP notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeEmail, smtpNotifier); err != nil {
log.Fatalf("Failed to register SMTP notifier: %v", err)
logger.Fatalf("Failed to register SMTP notifier: %v", err)
}
log.Println("Registered SMTP notifier")
logger.Info("Registered SMTP notifier")
}
}
if cfg.Notifiers.Slack != nil {
slackNotifier, err := notifier.NewSlackNotifier(cfg.Notifiers.Slack)
if err != nil {
log.Printf("Warning: failed to create Slack notifier: %v", err)
logger.Warnf("Failed to create Slack notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeSlack, slackNotifier); err != nil {
log.Fatalf("Failed to register Slack notifier: %v", err)
logger.Fatalf("Failed to register Slack notifier: %v", err)
}
log.Println("Registered Slack notifier")
logger.Info("Registered Slack notifier")
}
}
if cfg.Notifiers.Ntfy != nil {
ntfyNotifier, err := notifier.NewNtfyNotifier(cfg.Notifiers.Ntfy)
if err != nil {
log.Printf("Warning: failed to create Ntfy notifier: %v", err)
logger.Warnf("Failed to create Ntfy notifier: %v", err)
} else {
if err := factory.RegisterNotifier(domain.TypeNtfy, ntfyNotifier); err != nil {
log.Fatalf("Failed to register Ntfy notifier: %v", err)
logger.Fatalf("Failed to register Ntfy notifier: %v", err)
}
log.Println("Registered Ntfy notifier")
logger.Info("Registered Ntfy notifier")
}
}
}
func startGRPCServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService) *grpc.Server {
func startGRPCServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService, logger *logging.Logger) *grpc.Server {
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort)
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("Failed to listen on %s: %v", addr, err)
logger.Fatalf("Failed to listen on %s: %v", addr, err)
}
grpcServer := grpc.NewServer()
@@ -183,16 +193,16 @@ func startGRPCServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config
go func() {
defer wg.Done()
log.Printf("gRPC server listening on %s", addr)
logger.Infof("gRPC server listening on %s", addr)
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("Failed to serve gRPC: %v", err)
logger.Fatalf("Failed to serve gRPC: %v", err)
}
}()
return grpcServer
}
func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService) *http.Server {
func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService, logger *logging.Logger) *http.Server {
router := rest.NewRouter(svc)
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.RESTPort)
@@ -206,9 +216,9 @@ func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config
go func() {
defer wg.Done()
log.Printf("REST server listening on %s", addr)
logger.Infof("REST server listening on %s", addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start REST server: %v", err)
logger.Fatalf("Failed to start REST server: %v", err)
}
}()
+158
View File
@@ -0,0 +1,158 @@
package logging
import (
"fmt"
"io"
"log"
"os"
"time"
)
// Logger provides structured logging with ISO 8601 timestamps
type Logger struct {
*log.Logger
level LogLevel
}
// LogLevel represents the logging level
type LogLevel int
const (
DebugLevel LogLevel = iota
InfoLevel
WarnLevel
ErrorLevel
)
// New creates a new logger with ISO 8601 timestamp format
func New(level LogLevel, output io.Writer) *Logger {
if output == nil {
output = os.Stdout
}
return &Logger{
Logger: log.New(output, "", 0), // No flags, we'll format ourselves
level: level,
}
}
// NewFromConfig creates a logger from configuration
func NewFromConfig(levelStr string, outputPath string) (*Logger, error) {
level := parseLevel(levelStr)
var output io.Writer
switch outputPath {
case "stdout", "":
output = os.Stdout
case "stderr":
output = os.Stderr
default:
file, err := os.OpenFile(outputPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, err
}
output = file
}
return New(level, output), nil
}
// formatMessage formats a log message with ISO 8601 timestamp
func (l *Logger) formatMessage(level string, msg string) string {
timestamp := time.Now().UTC().Format(time.RFC3339)
return timestamp + " [" + level + "] " + msg
}
// Debug logs a debug message
func (l *Logger) Debug(msg string) {
if l.level <= DebugLevel {
l.Logger.Println(l.formatMessage("DEBUG", msg))
}
}
// Debugf logs a formatted debug message
func (l *Logger) Debugf(format string, args ...interface{}) {
if l.level <= DebugLevel {
msg := sprintf(format, args...)
l.Logger.Println(l.formatMessage("DEBUG", msg))
}
}
// Info logs an info message
func (l *Logger) Info(msg string) {
if l.level <= InfoLevel {
l.Logger.Println(l.formatMessage("INFO", msg))
}
}
// Infof logs a formatted info message
func (l *Logger) Infof(format string, args ...interface{}) {
if l.level <= InfoLevel {
msg := sprintf(format, args...)
l.Logger.Println(l.formatMessage("INFO", msg))
}
}
// Warn logs a warning message
func (l *Logger) Warn(msg string) {
if l.level <= WarnLevel {
l.Logger.Println(l.formatMessage("WARN", msg))
}
}
// Warnf logs a formatted warning message
func (l *Logger) Warnf(format string, args ...interface{}) {
if l.level <= WarnLevel {
msg := sprintf(format, args...)
l.Logger.Println(l.formatMessage("WARN", msg))
}
}
// Error logs an error message
func (l *Logger) Error(msg string) {
if l.level <= ErrorLevel {
l.Logger.Println(l.formatMessage("ERROR", msg))
}
}
// Errorf logs a formatted error message
func (l *Logger) Errorf(format string, args ...interface{}) {
if l.level <= ErrorLevel {
msg := sprintf(format, args...)
l.Logger.Println(l.formatMessage("ERROR", msg))
}
}
// Fatal logs a fatal message and exits
func (l *Logger) Fatal(msg string) {
l.Logger.Println(l.formatMessage("FATAL", msg))
os.Exit(1)
}
// Fatalf logs a formatted fatal message and exits
func (l *Logger) Fatalf(format string, args ...interface{}) {
msg := sprintf(format, args...)
l.Logger.Println(l.formatMessage("FATAL", msg))
os.Exit(1)
}
// parseLevel parses a log level string
func parseLevel(levelStr string) LogLevel {
switch levelStr {
case "debug":
return DebugLevel
case "info":
return InfoLevel
case "warn", "warning":
return WarnLevel
case "error":
return ErrorLevel
default:
return InfoLevel
}
}
// sprintf is a helper using fmt
func sprintf(format string, args ...interface{}) string {
return fmt.Sprintf(format, args...)
}