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
RateLimitWindow time.Duration
IsDevelopment bool
LogDir string
LogLevel logging.LogLevel
ConsoleOutput bool
}
// DefaultConfig returns a new Config instance with default values
@@ -41,9 +39,6 @@ func DefaultConfig() *Config {
RateLimitRequests: 100,
RateLimitWindow: time.Minute * 15,
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
if logLevel := os.Getenv("NOVAMD_LOG_LEVEL"); logLevel != "" {
if parsed, err := logging.ParseLogLevel(logLevel); err == nil {
parsed := logging.ParseLogLevel(logLevel)
config.LogLevel = parsed
}
} else if config.IsDevelopment {
config.LogLevel = logging.DEBUG
}
// 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
}
} else {
config.LogLevel = logging.INFO
}
// Validate all settings

View File

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

View File

@@ -1,8 +1,8 @@
package app
import (
"log/slog"
"net/http"
"novamd/internal/logging"
"github.com/go-chi/chi/v5"
)
@@ -11,7 +11,6 @@ import (
type Server struct {
router *chi.Mux
options *Options
logger *slog.Logger
}
// NewServer creates a new server instance with the given options
@@ -19,7 +18,6 @@ func NewServer(options *Options) *Server {
return &Server{
router: setupRouter(*options),
options: options,
logger: options.Logger.App(),
}
}
@@ -27,7 +25,7 @@ func NewServer(options *Options) *Server {
func (s *Server) Start() error {
// Start server
addr := ":" + s.options.Config.Port
s.logger.Info("Starting server", "address", addr)
logging.Info("Starting server", "address", addr)
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
import (
"context"
"fmt"
"io"
"log/slog"
"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
// Log levels
@@ -22,208 +20,55 @@ const (
ERROR LogLevel = LogLevel(slog.LevelError)
)
// Logger defines the interface for logging operations
type Logger interface {
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
// Setup initializes the logger with the given minimum log level
func Setup(minLevel LogLevel) {
opts := &slog.HandlerOptions{
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
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
outputs := []Output{{Type: OutputTypeJSON, Writer: file}}
if consoleOut {
outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout})
}
// Create and set logger
handler := createLogger(opts, outputs)
lt.setLogger(slog.New(handler))
}
return l, nil
Logger = slog.New(slog.NewTextHandler(os.Stdout, opts))
}
func (l *logger) App() *slog.Logger {
return l.appLogger
// ParseLogLevel converts a string to a LogLevel
func ParseLogLevel(level string) LogLevel {
switch level {
case "debug":
return DEBUG
case "warn":
return WARN
case "error":
return ERROR
default:
return INFO
}
}
func (l *logger) Audit() *slog.Logger {
return l.auditLogger
// Debug logs a debug message
func Debug(msg string, args ...any) {
Logger.Debug(msg, args...)
}
func (l *logger) Security() *slog.Logger {
return l.securityLogger
// Info logs an info message
func Info(msg string, args ...any) {
Logger.Info(msg, args...)
}
func (l *logger) Close() error {
var lastErr error
for _, file := range l.files {
if file != nil {
if err := file.Close(); err != nil {
lastErr = err
}
}
}
return lastErr
// Warn logs a warning message
func Warn(msg string, args ...any) {
Logger.Warn(msg, args...)
}
// multiHandler implements slog.Handler for multiple outputs
type multiHandler []slog.Handler
func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h {
if handler.Enabled(ctx, level) {
return true
}
}
return false
// Error logs an error message
func Error(msg string, args ...any) {
Logger.Error(msg, args...)
}
func (h multiHandler) Handle(ctx context.Context, r slog.Record) error {
for _, handler := range h {
if err := handler.Handle(ctx, r); err != nil {
return err
}
}
return nil
// WithGroup adds a group to the logger context
func WithGroup(name string) *slog.Logger {
return Logger.WithGroup(name)
}
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)
// With adds key-value pairs to the logger context
func With(args ...any) *slog.Logger {
return Logger.With(args...)
}