Initial logging implementation

This commit is contained in:
2024-12-10 22:16:50 +01:00
parent 3d03da221b
commit ea916c3ecc
3 changed files with 304 additions and 0 deletions

14
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,14 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Launch NovaMD Server",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/server/cmd/server/main.go",
"cwd": "${workspaceFolder}",
"envFile": "${workspaceFolder}/server/.env"
}
]
}

View File

@@ -2,6 +2,7 @@ package app
import ( import (
"fmt" "fmt"
"novamd/internal/logging"
"novamd/internal/secrets" "novamd/internal/secrets"
"os" "os"
"strconv" "strconv"
@@ -25,6 +26,9 @@ type Config struct {
RateLimitRequests int RateLimitRequests int
RateLimitWindow time.Duration RateLimitWindow time.Duration
IsDevelopment bool IsDevelopment bool
LogDir string
LogLevel logging.LogLevel
ConsoleOutput bool
} }
// DefaultConfig returns a new Config instance with default values // DefaultConfig returns a new Config instance with default values
@@ -37,6 +41,9 @@ 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: true,
} }
} }
@@ -108,6 +115,29 @@ 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 {
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 if config.IsDevelopment {
config.ConsoleOutput = true
}
// Validate all settings // Validate all settings
if err := config.validate(); err != nil { if err := config.validate(); err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,260 @@
// Package logging provides a logging interface for the application.
package logging
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
// LogLevel defines the severity of a log message
type LogLevel int
// Log levels
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
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"
}
}
// ParseLogLevel converts a string to 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)
}
}
// 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,
}
// Setup application log output
appFile, err := os.OpenFile(
filepath.Join(logDir, "app.log"),
os.O_APPEND|os.O_CREATE|os.O_WRONLY,
0644,
)
if err != nil {
return nil, fmt.Errorf("failed to open app log file: %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)
}
// 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
if consoleOut {
l.appOutput = io.MultiWriter(appFile, os.Stdout)
}
l.auditOutput = auditFile
l.secOutput = io.MultiWriter(securityFile, os.Stderr) // Security logs always go to stderr
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) 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),
)
}
// 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 {
var errs []error
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))
}
}
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))
}
}
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))
}
}
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 ""
}
return fmt.Sprintf(" - Fields: %v", fields)
}