Files
wisp-open/internal/logger/logger.go
T

151 lines
3.4 KiB
Go

// Package logger provides a simple file-based logging system for the application.
// Logs are written to date-stamped files in the user's config directory.
package logger
import (
"fmt"
"os"
"path/filepath"
"sync"
"time"
)
// Log level constants
const (
logFileMode = 0644
logDirMode = 0755
)
var (
logFile *os.File
logMutex sync.Mutex
isInitialized bool
)
// Init initializes the logger with a new log file for today's date.
// It creates the log directory if it doesn't exist.
func Init() error {
logMutex.Lock()
defer logMutex.Unlock()
if isInitialized {
return nil
}
logPath, err := getLogPath()
if err != nil {
return fmt.Errorf("failed to get log path: %w", err)
}
// Ensure log directory exists
logDir := filepath.Dir(logPath)
if err := os.MkdirAll(logDir, logDirMode); err != nil {
return fmt.Errorf("failed to create log directory: %w", err)
}
// Open or create log file
logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, logFileMode)
if err != nil {
return fmt.Errorf("failed to open log file: %w", err)
}
isInitialized = true
Info("Logger initialized")
return nil
}
// Close closes the log file handle.
func Close() {
logMutex.Lock()
defer logMutex.Unlock()
if logFile != nil {
logFile.Close()
logFile = nil
}
isInitialized = false
}
// Info logs an informational message with timestamp.
func Info(format string, args ...interface{}) {
log("INFO", format, args...)
}
// Error logs an error message with timestamp.
func Error(format string, args ...interface{}) {
log("ERROR", format, args...)
}
// Debug logs a debug message with timestamp.
func Debug(format string, args ...interface{}) {
log("DEBUG", format, args...)
}
// log writes a formatted log message to the log file and console.
func log(level, format string, args ...interface{}) {
logMutex.Lock()
defer logMutex.Unlock()
// Auto-initialize if needed
if !isInitialized {
if err := initWithoutLock(); err != nil {
// Fall back to stderr if logging fails
fmt.Fprintf(os.Stderr, "[%s] %s: %s\n", level, time.Now().Format(time.RFC3339), fmt.Sprintf(format, args...))
return
}
}
timestamp := time.Now().Format("2006-01-02 15:04:05")
message := fmt.Sprintf(format, args...)
logLine := fmt.Sprintf("[%s] %s: %s\n", timestamp, level, message)
// Print to console for real-time terminal output
if level == "ERROR" {
fmt.Fprint(os.Stderr, logLine)
} else {
fmt.Fprint(os.Stdout, logLine)
}
if logFile != nil {
logFile.WriteString(logLine)
// Do NOT call Sync() on every write — that's a massive performance
// bottleneck (forces fsync to disk for every single log line).
// The kernel will flush buffered writes in its own time.
}
}
// initWithoutLock initializes the logger without acquiring the mutex.
// This should only be called when the lock is already held.
func initWithoutLock() error {
if isInitialized {
return nil
}
logPath, err := getLogPath()
if err != nil {
return err
}
logDir := filepath.Dir(logPath)
if err := os.MkdirAll(logDir, logDirMode); err != nil {
return err
}
logFile, err = os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, logFileMode)
if err != nil {
return err
}
isInitialized = true
return nil
}
// getLogPath returns the path to the log file.
func getLogPath() (string, error) {
homeDir, err := os.UserHomeDir()
if err != nil {
return "", err
}
return filepath.Join(homeDir, ".wis-free-v3", "logs", time.Now().Format("2006-01-02")+".log"), nil
}