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,
LogDir: "./logs",
LogLevel: logging.INFO,
ConsoleOutput: true,
ConsoleOutput: false,
}
}
@@ -134,8 +134,6 @@ func LoadConfig() (*Config, error) {
if parsed, err := strconv.ParseBool(consoleOutput); err == nil {
config.ConsoleOutput = parsed
}
} else if config.IsDevelopment {
config.ConsoleOutput = true
}
// 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
import (
"context"
"fmt"
"io"
"log/slog"
"os"
"path/filepath"
"time"
)
// LogLevel defines the severity of a log message
type LogLevel int
// LogLevel represents the logging level
type LogLevel slog.Level
// Log levels
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
DEBUG LogLevel = LogLevel(slog.LevelDebug)
INFO LogLevel = LogLevel(slog.LevelInfo)
WARN LogLevel = LogLevel(slog.LevelWarn)
ERROR LogLevel = LogLevel(slog.LevelError)
)
func (l LogLevel) String() string {
switch l {
case DEBUG:
return "DEBUG"
case INFO:
return "INFO"
case WARN:
return "WARN"
case ERROR:
return "ERROR"
default:
return "INFO"
}
// 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
}
// ParseLogLevel converts a string to a LogLevel
// 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":
@@ -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
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{
logDir: logDir,
minLevel: minLevel,
consoleOut: consoleOut,
files: make([]*os.File, 0, 3),
}
// Setup application log output
appFile, err := os.OpenFile(
filepath.Join(logDir, "app.log"),
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0644,
)
// 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{
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 {
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)
}
// 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)
return nil, fmt.Errorf("failed to create %s log file: %w", lt.name, err)
}
l.files = append(l.files, file)
// Setup security log output
securityFile, err := os.OpenFile(
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
// Prepare outputs
outputs := []Output{{Type: OutputTypeJSON, Writer: file}}
if consoleOut {
l.appOutput = io.MultiWriter(appFile, os.Stdout)
outputs = append(outputs, Output{Type: OutputTypeText, Writer: os.Stdout})
}
l.auditOutput = auditFile
l.secOutput = io.MultiWriter(securityFile, os.Stderr) // Security logs always go to stderr
// Create and set logger
handler := createLogger(opts, outputs)
lt.setLogger(slog.New(handler))
}
return l, nil
}
// writeToOutput writes to the output and handles errors appropriately
func (l *logger) writeOutput(w io.Writer, format string, args ...interface{}) {
_, 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) App() *slog.Logger {
return l.appLogger
}
func (l *logger) logApp(level LogLevel, msg string, fields map[string]interface{}) {
if level < l.minLevel {
return
}
entry := logEntry{
Timestamp: time.Now(),
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),
)
func (l *logger) Audit() *slog.Logger {
return l.auditLogger
}
// 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) Security() *slog.Logger {
return l.securityLogger
}
func (l *logger) Close() error {
var errs []error
var lastErr error
for _, file := range l.files {
if file != nil {
if err := file.Close(); err != nil {
lastErr = err
}
}
}
return lastErr
}
if closer, ok := l.appOutput.(io.Closer); ok {
if err := closer.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close app output: %w", err))
}
}
// multiHandler implements slog.Handler for multiple outputs
type multiHandler []slog.Handler
if closer, ok := l.auditOutput.(io.Closer); ok {
if err := closer.Close(); err != nil {
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 {
if err := closer.Close(); err != nil {
errs = append(errs, fmt.Errorf("failed to close security output: %w", err))
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
}
}
if len(errs) > 0 {
return fmt.Errorf("failed to close some outputs: %v", errs)
}
return nil
}
func formatFields(fields map[string]interface{}) string {
if len(fields) == 0 {
return ""
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 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)
}