Use slog for logging

This commit is contained in:
2024-12-12 20:53:35 +01:00
parent ea916c3ecc
commit 9d82b6426c
2 changed files with 163 additions and 196 deletions

View File

@@ -43,7 +43,7 @@ func DefaultConfig() *Config {
IsDevelopment: false, IsDevelopment: false,
LogDir: "./logs", LogDir: "./logs",
LogLevel: logging.INFO, LogLevel: logging.INFO,
ConsoleOutput: true, ConsoleOutput: false,
} }
} }
@@ -134,8 +134,6 @@ func LoadConfig() (*Config, error) {
if parsed, err := strconv.ParseBool(consoleOutput); err == nil { if parsed, err := strconv.ParseBool(consoleOutput); err == nil {
config.ConsoleOutput = parsed config.ConsoleOutput = parsed
} }
} else if config.IsDevelopment {
config.ConsoleOutput = true
} }
// Validate all settings // Validate all settings

View File

@@ -1,41 +1,100 @@
// Package logging provides a logging interface for the application. // Package logging provides a structured logging interface for the application.
package logging package logging
import ( import (
"context"
"fmt" "fmt"
"io" "io"
"log/slog"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
) )
// LogLevel defines the severity of a log message // LogLevel represents the logging level
type LogLevel int type LogLevel slog.Level
// Log levels // Log levels
const ( const (
DEBUG LogLevel = iota DEBUG LogLevel = LogLevel(slog.LevelDebug)
INFO INFO LogLevel = LogLevel(slog.LevelInfo)
WARN WARN LogLevel = LogLevel(slog.LevelWarn)
ERROR ERROR LogLevel = LogLevel(slog.LevelError)
) )
func (l LogLevel) String() string { // Logger defines the interface for logging operations
switch l { type Logger interface {
case DEBUG: App() *slog.Logger // Returns logger for application logs
return "DEBUG" Audit() *slog.Logger // Returns logger for audit logs
case INFO: Security() *slog.Logger // Returns logger for security logs
return "INFO" Close() error // Cleanup and close log files
case WARN: }
return "WARN"
case ERROR: // logger implements the Logger interface
return "ERROR" type logger struct {
default: appLogger *slog.Logger
return "INFO" 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))
} }
} }
// ParseLogLevel converts a string to a LogLevel return multiHandler(handlers)
}
// ParseLogLevel parses a string into a LogLevel
func ParseLogLevel(level string) (LogLevel, error) { func ParseLogLevel(level string) (LogLevel, error) {
switch level { switch level {
case "DEBUG": case "DEBUG":
@@ -51,210 +110,120 @@ func ParseLogLevel(level string) (LogLevel, error) {
} }
} }
// Logger defines the interface for all logging operations
type Logger interface {
// Standard logging
Debug(msg string, fields map[string]interface{})
Info(msg string, fields map[string]interface{})
Warn(msg string, fields map[string]interface{})
Error(err error, fields map[string]interface{})
// Audit logging (always at INFO level)
Audit(userID int, workspaceID int, action, resource string, details map[string]interface{})
// Security logging (with levels)
Security(level LogLevel, userID int, event string, details map[string]interface{})
// Close closes all outputs
Close() error
}
type logger struct {
logDir string
minLevel LogLevel
consoleOut bool
appOutput io.Writer // Combined output for application logs
auditOutput io.Writer // Output for audit logs
secOutput io.Writer // Output for security logs
}
type logEntry struct {
Timestamp time.Time
Level LogLevel
Message string
Fields map[string]interface{}
}
// New creates a new Logger instance // New creates a new Logger instance
func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) { func New(logDir string, minLevel LogLevel, consoleOut bool) (Logger, error) {
if err := os.MkdirAll(logDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
l := &logger{ l := &logger{
logDir: logDir, files: make([]*os.File, 0, 3),
minLevel: minLevel,
consoleOut: consoleOut,
} }
// Setup application log output // Define our log types and their filenames
appFile, err := os.OpenFile( logTypes := []struct {
filepath.Join(logDir, "app.log"), name string
os.O_APPEND|os.O_CREATE|os.O_WRONLY, setLogger func(*slog.Logger)
0644, }{
) {"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{
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 != nil {
return nil, fmt.Errorf("failed to open app log file: %w", err) 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)
// Setup audit log output
auditFile, err := os.OpenFile(
filepath.Join(logDir, "audit.log"),
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0644,
)
if err != nil {
return nil, fmt.Errorf("failed to open audit log file: %w", err)
} }
l.files = append(l.files, file)
// Setup security log output // Prepare outputs
securityFile, err := os.OpenFile( outputs := []Output{{Type: OutputTypeJSON, Writer: file}}
filepath.Join(logDir, "security.log"),
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0644,
)
if err != nil {
return nil, fmt.Errorf("failed to open security log file: %w", err)
}
// Configure outputs
l.appOutput = appFile
if consoleOut { if consoleOut {
l.appOutput = io.MultiWriter(appFile, os.Stdout) outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout})
} }
l.auditOutput = auditFile // Create and set logger
l.secOutput = io.MultiWriter(securityFile, os.Stderr) // Security logs always go to stderr handler := createLogger(opts, outputs)
lt.setLogger(slog.New(handler))
}
return l, nil return l, nil
} }
// writeToOutput writes to the output and handles errors appropriately func (l *logger) App() *slog.Logger {
func (l *logger) writeOutput(w io.Writer, format string, args ...interface{}) { return l.appLogger
_, err := fmt.Fprintf(w, format, args...)
if err != nil {
// Log to stderr if writing fails
fmt.Fprintf(os.Stderr, "logging error: %v\n", err)
}
} }
func (l *logger) logApp(level LogLevel, msg string, fields map[string]interface{}) { func (l *logger) Audit() *slog.Logger {
if level < l.minLevel { return l.auditLogger
return
} }
entry := logEntry{ func (l *logger) Security() *slog.Logger {
Timestamp: time.Now(), return l.securityLogger
Level: level,
Message: msg,
Fields: fields,
}
l.writeOutput(l.appOutput, "[%s] [%s] %s%s\n",
entry.Timestamp.Format(time.RFC3339),
entry.Level,
entry.Message,
formatFields(entry.Fields),
)
}
// Debug logs a debug message
func (l *logger) Debug(msg string, fields map[string]interface{}) {
l.logApp(DEBUG, msg, fields)
}
// Info logs an informational message
func (l *logger) Info(msg string, fields map[string]interface{}) {
l.logApp(INFO, msg, fields)
}
// Warn logs a warning message
func (l *logger) Warn(msg string, fields map[string]interface{}) {
l.logApp(WARN, msg, fields)
}
// Error logs an error message
func (l *logger) Error(err error, fields map[string]interface{}) {
if fields == nil {
fields = make(map[string]interface{})
}
fields["error"] = err.Error()
l.logApp(ERROR, "error occurred", fields)
}
// Audit logs an audit event
func (l *logger) Audit(userID int, workspaceID int, action, resource string, details map[string]interface{}) {
if details == nil {
details = make(map[string]interface{})
}
details["user_id"] = userID
details["workspace_id"] = workspaceID
details["action"] = action
details["resource"] = resource
l.writeOutput(l.auditOutput, "[%s] [AUDIT] %s on %s%s\n",
time.Now().Format(time.RFC3339),
action,
resource,
formatFields(details),
)
}
// Security logs a security event
func (l *logger) Security(level LogLevel, userID int, event string, details map[string]interface{}) {
if level < l.minLevel {
return
}
l.writeOutput(l.secOutput, "[%s] [%s] [%d] [SECURITY] %s%s\n",
time.Now().Format(time.RFC3339),
level,
userID,
event,
formatFields(details),
)
} }
func (l *logger) Close() error { func (l *logger) Close() error {
var errs []error var lastErr error
for _, file := range l.files {
if closer, ok := l.appOutput.(io.Closer); ok { if file != nil {
if err := closer.Close(); err != nil { if err := file.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close app output: %w", err)) lastErr = err
} }
} }
}
return lastErr
}
if closer, ok := l.auditOutput.(io.Closer); ok { // multiHandler implements slog.Handler for multiple outputs
if err := closer.Close(); err != nil { type multiHandler []slog.Handler
errs = append(errs, fmt.Errorf("failed to close audit output: %w", err))
func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
for _, handler := range h {
if handler.Enabled(ctx, level) {
return true
} }
} }
return false
}
if closer, ok := l.secOutput.(io.Closer); ok { func (h multiHandler) Handle(ctx context.Context, r slog.Record) error {
if err := closer.Close(); err != nil { for _, handler := range h {
errs = append(errs, fmt.Errorf("failed to close security output: %w", err)) if err := handler.Handle(ctx, r); err != nil {
return err
} }
} }
if len(errs) > 0 {
return fmt.Errorf("failed to close some outputs: %v", errs)
}
return nil return nil
} }
func formatFields(fields map[string]interface{}) string { func (h multiHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
if len(fields) == 0 { handlers := make([]slog.Handler, len(h))
return "" for i, handler := range h {
handlers[i] = handler.WithAttrs(attrs)
} }
return fmt.Sprintf(" - Fields: %v", fields) 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)
} }