mirror of
https://github.com/lordmathis/lemma.git
synced 2025-11-06 16:04:23 +00:00
Use slog for logging
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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:
|
|
||||||
return "ERROR"
|
|
||||||
default:
|
|
||||||
return "INFO"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
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{
|
|
||||||
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug logs a debug message
|
func (l *logger) Security() *slog.Logger {
|
||||||
func (l *logger) Debug(msg string, fields map[string]interface{}) {
|
return l.securityLogger
|
||||||
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 file != nil {
|
||||||
|
if err := file.Close(); err != nil {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
if closer, ok := l.appOutput.(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 app output: %w", err))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if closer, ok := l.auditOutput.(io.Closer); ok {
|
func (h multiHandler) Enabled(ctx context.Context, level slog.Level) bool {
|
||||||
if err := closer.Close(); err != nil {
|
for _, handler := range h {
|
||||||
errs = append(errs, fmt.Errorf("failed to close audit output: %w", err))
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user