Simplify logging

This commit is contained in:
2024-12-14 23:59:28 +01:00
parent 1ee8d94789
commit 71df436a93
4 changed files with 46 additions and 224 deletions

View File

@@ -26,9 +26,7 @@ type Config struct {
RateLimitRequests int RateLimitRequests int
RateLimitWindow time.Duration RateLimitWindow time.Duration
IsDevelopment bool IsDevelopment bool
LogDir string
LogLevel logging.LogLevel LogLevel logging.LogLevel
ConsoleOutput bool
} }
// DefaultConfig returns a new Config instance with default values // DefaultConfig returns a new Config instance with default values
@@ -41,9 +39,6 @@ func DefaultConfig() *Config {
RateLimitRequests: 100, RateLimitRequests: 100,
RateLimitWindow: time.Minute * 15, RateLimitWindow: time.Minute * 15,
IsDevelopment: false, IsDevelopment: false,
LogDir: "./logs",
LogLevel: logging.INFO,
ConsoleOutput: false,
} }
} }
@@ -115,25 +110,14 @@ func LoadConfig() (*Config, error) {
} }
} }
// Configure log directory
if logDir := os.Getenv("NOVAMD_LOG_DIR"); logDir != "" {
config.LogDir = logDir
}
// Configure log level, if isDevelopment is set, default to debug // Configure log level, if isDevelopment is set, default to debug
if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" { if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" {
if parsed, err := logging.ParseLogLevel(logLevel); err == nil { parsed := logging.ParseLogLevel(logLevel)
config.LogLevel = parsed config.LogLevel = parsed
}
} else if config.IsDevelopment { } else if config.IsDevelopment {
config.LogLevel = logging.DEBUG config.LogLevel = logging.DEBUG
} } else {
config.LogLevel = logging.INFO
// Configure console output, if isDevelopment is set, default to true
if consoleOutput := os.Getenv("NOVAMD_CONSOLE_OUTPUT"); consoleOutput != "" {
if parsed, err := strconv.ParseBool(consoleOutput); err == nil {
config.ConsoleOutput = parsed
}
} }
// Validate all settings // Validate all settings

View File

@@ -12,7 +12,6 @@ type Options struct {
Config *Config Config *Config
Database db.Database Database db.Database
Storage storage.Manager Storage storage.Manager
Logger logging.Logger
JWTManager auth.JWTManager JWTManager auth.JWTManager
SessionManager auth.SessionManager SessionManager auth.SessionManager
CookieService auth.CookieManager CookieService auth.CookieManager
@@ -36,10 +35,7 @@ func DefaultOptions(cfg *Config) (*Options, error) {
storageManager := storage.NewService(cfg.WorkDir) storageManager := storage.NewService(cfg.WorkDir)
// Initialize logger // Initialize logger
logger, err := logging.New(cfg.LogDir, cfg.LogLevel, cfg.ConsoleOutput) logging.Setup(cfg.LogLevel)
if err != nil {
return nil, err
}
// Initialize auth services // Initialize auth services
jwtManager, sessionService, cookieService, err := initAuth(cfg, database) jwtManager, sessionService, cookieService, err := initAuth(cfg, database)
@@ -56,7 +52,6 @@ func DefaultOptions(cfg *Config) (*Options, error) {
Config: cfg, Config: cfg,
Database: database, Database: database,
Storage: storageManager, Storage: storageManager,
Logger: logger,
JWTManager: jwtManager, JWTManager: jwtManager,
SessionManager: sessionService, SessionManager: sessionService,
CookieService: cookieService, CookieService: cookieService,

View File

@@ -1,8 +1,8 @@
package app package app
import ( import (
"log/slog"
"net/http" "net/http"
"novamd/internal/logging"
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
) )
@@ -11,7 +11,6 @@ import (
type Server struct { type Server struct {
router *chi.Mux router *chi.Mux
options *Options options *Options
logger *slog.Logger
} }
// NewServer creates a new server instance with the given options // NewServer creates a new server instance with the given options
@@ -19,7 +18,6 @@ func NewServer(options *Options) *Server {
return &Server{ return &Server{
router: setupRouter(*options), router: setupRouter(*options),
options: options, options: options,
logger: options.Logger.App(),
} }
} }
@@ -27,7 +25,7 @@ func NewServer(options *Options) *Server {
func (s *Server) Start() error { func (s *Server) Start() error {
// Start server // Start server
addr := ":" + s.options.Config.Port addr := ":" + s.options.Config.Port
s.logger.Info("Starting server", "address", addr) logging.Info("Starting server", "address", addr)
return http.ListenAndServe(addr, s.router) return http.ListenAndServe(addr, s.router)
} }

View File

@@ -1,17 +1,15 @@
// Package logging provides a structured logging interface for the application. // Package logging provides a simple logging interface for the server.
package logging package logging
import ( import (
"context"
"fmt"
"io"
"log/slog" "log/slog"
"os" "os"
"path/filepath"
"time"
) )
// LogLevel represents the logging level // Logger is the global logger instance
var Logger *slog.Logger
// LogLevel represents the log level
type LogLevel slog.Level type LogLevel slog.Level
// Log levels // Log levels
@@ -22,208 +20,55 @@ const (
ERROR LogLevel = LogLevel(slog.LevelError) ERROR LogLevel = LogLevel(slog.LevelError)
) )
// Logger defines the interface for logging operations // Setup initializes the logger with the given minimum log level
type Logger interface { func Setup(minLevel LogLevel) {
App() *slog.Logger // Returns logger for application logs
Audit() *slog.Logger // Returns logger for audit logs
Security() *slog.Logger // Returns logger for security logs
Close() error // Cleanup and close log files
}
// logger implements the Logger interface
type logger struct {
appLogger *slog.Logger
auditLogger *slog.Logger
securityLogger *slog.Logger
files []*os.File // Keep track of open file handles
}
// Output represents a destination for logs
type Output struct {
Type OutputFormat
Writer io.Writer
}
// OutputFormat represents the format of the log output
type OutputFormat int
const (
OutputTypeJSON OutputFormat = iota // OutputTypeJSON JSON format
OutputTypeText // OutputTypeText text format
)
// createLogFile creates a log file with the given name in the log directory
func createLogFile(logDir, name string) (*os.File, error) {
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
filename := filepath.Join(logDir, fmt.Sprintf("%s.log", name))
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
return file, nil
}
// createLogger creates a new slog.Handler for the given outputs and options
func createLogger(opts *slog.HandlerOptions, outputs []Output) slog.Handler {
if len(outputs) == 0 {
return slog.NewTextHandler(io.Discard, opts)
}
if len(outputs) == 1 {
output := outputs[0]
if output.Type == OutputTypeJSON {
return slog.NewJSONHandler(output.Writer, opts)
}
return slog.NewTextHandler(output.Writer, opts)
}
// Multiple outputs - create handlers for each
handlers := make([]slog.Handler, 0, len(outputs))
for _, output := range outputs {
if output.Type == OutputTypeJSON {
handlers = append(handlers, slog.NewJSONHandler(output.Writer, opts))
} else {
handlers = append(handlers, slog.NewTextHandler(output.Writer, opts))
}
}
return multiHandler(handlers)
}
// ParseLogLevel parses a string into a LogLevel
func ParseLogLevel(level string) (LogLevel, error) {
switch level {
case "DEBUG":
return DEBUG, nil
case "INFO":
return INFO, nil
case "WARN":
return WARN, nil
case "ERROR":
return ERROR, nil
default:
return INFO, fmt.Errorf("invalid log level: %s", level)
}
}
// New creates a new Logger instance
func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) {
l := &logger{
files: make([]*os.File, 0, 3),
}
// Define our log types and their filenames
logTypes := []struct {
name string
setLogger func(*slog.Logger)
}{
{"app", func(lg *slog.Logger) { l.appLogger = lg }},
{"audit", func(lg *slog.Logger) { l.auditLogger = lg }},
{"security", func(lg *slog.Logger) { l.securityLogger = lg }},
}
// Setup handlers options
opts := &slog.HandlerOptions{ opts := &slog.HandlerOptions{
Level: slog.Level(minLevel), Level: slog.Level(minLevel),
AddSource: slog.Level(minLevel) == slog.LevelDebug,
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
return slog.Attr{
Key: a.Key,
Value: slog.StringValue(time.Now().UTC().Format(time.RFC3339)),
}
}
return a
},
} }
// Create loggers for each type Logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
for _, lt := range logTypes { }
// Create file output
file, err := createLogFile(logDir, lt.name)
if err != nil {
if err := l.Close(); err != nil {
return nil, fmt.Errorf("failed to close logger: %w", err)
}
return nil, fmt.Errorf("failed to create %s log file: %w", lt.name, err)
}
l.files = append(l.files, file)
// Prepare outputs // ParseLogLevel converts a string to a LogLevel
outputs := []Output{{Type: OutputTypeJSON, Writer: file}} func ParseLogLevel(level string) LogLevel {
if consoleOut { switch level {
outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout}) case "debug":
} return DEBUG
case "warn":
// Create and set logger return WARN
handler := createLogger(opts, outputs) case "error":
lt.setLogger(slog.New(handler)) return ERROR
default:
return INFO
} }
return l, nil
} }
func (l *logger) App() *slog.Logger { // Debug logs a debug message
return l.appLogger func Debug(msg string, args ...any) {
Logger.Debug(msg, args...)
} }
func (l *logger) Audit() *slog.Logger { // Info logs an info message
return l.auditLogger func Info(msg string, args ...any) {
Logger.Info(msg, args...)
} }
func (l *logger) Security() *slog.Logger { // Warn logs a warning message
return l.securityLogger func Warn(msg string, args ...any) {
Logger.Warn(msg, args...)
} }
func (l *logger) Close() error { // Error logs an error message
var lastErr error func Error(msg string, args ...any) {
for _, file := range l.files { Logger.Error(msg, args...)
if file != nil {
if err := file.Close(); err != nil {
lastErr = err
}
}
}
return lastErr
} }
// multiHandler implements slog.Handler for multiple outputs // WithGroup adds a group to the logger context
type multiHandler []slog.Handler func WithGroup(name string) *slog.Logger {
return Logger.WithGroup(name)
func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h {
if handler.Enabled(ctx, level) {
return true
}
}
return false
} }
func (h multiHandler) Handle(ctx context.Context, r slog.Record) error { // With adds key-value pairs to the logger context
for _, handler := range h { func With(args ...any) *slog.Logger {
if err := handler.Handle(ctx, r); err != nil { return Logger.With(args...)
return err
}
}
return nil
}
func (h multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
handlers := make([]slog.Handler, len(h))
for i, handler := range h {
handlers[i] = handler.WithAttrs(attrs)
}
return multiHandler(handlers)
}
func (h multiHandler) WithGroup(name string) slog.Handler {
handlers := make([]slog.Handler, len(h))
for i, handler := range h {
handlers[i] = handler.WithGroup(name)
}
return multiHandler(handlers)
} }