Files
G-Launcher/pkg/logger/logger.go
T

242 lines
4.5 KiB
Go
Raw Normal View History

package logger
import (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"sync"
"time"
)
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
var levelNames = map[LogLevel]string{
DEBUG: "DEBUG",
INFO: "INFO",
WARN: "WARN",
ERROR: "ERROR",
}
type Logger struct {
mu sync.Mutex
logFile *os.File
logPath string
logDir string
}
var (
defaultLogger *Logger
once sync.Once
)
// Init initializes the centralized logger with file & console outputs.
func Init() (*Logger, error) {
var initErr error
once.Do(func() {
configDir, err := os.UserConfigDir()
if err != nil {
configDir = "."
}
logDir := filepath.Join(configDir, "g-launcher", "logs")
if err := os.MkdirAll(logDir, 0755); err != nil {
initErr = fmt.Errorf("create log dir: %w", err)
return
}
logPath := filepath.Join(logDir, "launcher.log")
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
initErr = fmt.Errorf("open log file: %w", err)
return
}
defaultLogger = &Logger{
logFile: file,
logPath: logPath,
logDir: logDir,
}
defaultLogger.write(INFO, "=== G-Launcher Log Session Started ===")
defaultLogger.write(INFO, fmt.Sprintf("Log Path: %s", logPath))
})
return defaultLogger, initErr
}
// GetInstance returns the default logger, initializing if necessary.
func GetInstance() *Logger {
if defaultLogger == nil {
_, _ = Init()
}
return defaultLogger
}
func (l *Logger) write(level LogLevel, message string) {
if l == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
lvl := levelNames[level]
line := fmt.Sprintf("[%s] [%-5s] %s\n", timestamp, lvl, message)
// Write to console
fmt.Print(line)
// Write to log file
if l.logFile != nil {
_, _ = l.logFile.WriteString(line)
_ = l.logFile.Sync()
}
}
func (l *Logger) Log(levelStr string, message string) {
lvl := INFO
switch strings.ToUpper(strings.TrimSpace(levelStr)) {
case "DEBUG":
lvl = DEBUG
case "WARN", "WARNING":
lvl = WARN
case "ERROR":
lvl = ERROR
}
l.write(lvl, message)
}
func (l *Logger) Debug(format string, args ...interface{}) {
l.write(DEBUG, fmt.Sprintf(format, args...))
}
func (l *Logger) Info(format string, args ...interface{}) {
l.write(INFO, fmt.Sprintf(format, args...))
}
func (l *Logger) Warn(format string, args ...interface{}) {
l.write(WARN, fmt.Sprintf(format, args...))
}
func (l *Logger) Error(format string, args ...interface{}) {
l.write(ERROR, fmt.Sprintf(format, args...))
}
func (l *Logger) GetLogPath() string {
if l == nil {
return ""
}
return l.logPath
}
func (l *Logger) GetLogDir() string {
if l == nil {
return ""
}
return l.logDir
}
func (l *Logger) ReadRecentLogs(maxLines int) (string, error) {
if l == nil || l.logPath == "" {
return "", fmt.Errorf("logger not initialized")
}
l.mu.Lock()
defer l.mu.Unlock()
data, err := os.ReadFile(l.logPath)
if err != nil {
return "", err
}
lines := strings.Split(string(data), "\n")
if len(lines) <= maxLines {
return string(data), nil
}
recent := lines[len(lines)-maxLines:]
return strings.Join(recent, "\n"), nil
}
func (l *Logger) Close() {
l.mu.Lock()
defer l.mu.Unlock()
if l.logFile != nil {
_ = l.logFile.Close()
l.logFile = nil
}
}
// Global package-level wrappers
func Debug(format string, args ...interface{}) {
GetInstance().Debug(format, args...)
}
func Info(format string, args ...interface{}) {
GetInstance().Info(format, args...)
}
func Warn(format string, args ...interface{}) {
GetInstance().Warn(format, args...)
}
func Error(format string, args ...interface{}) {
GetInstance().Error(format, args...)
}
func GetLogPath() string {
return GetInstance().GetLogPath()
}
func GetLogDir() string {
return GetInstance().GetLogDir()
}
func ReadRecentLogs(maxLines int) (string, error) {
return GetInstance().ReadRecentLogs(maxLines)
}
// CaptureProcessOutput attaches pipes to a command and logs its output in real-time.
func (l *Logger) CaptureProcessOutput(prefix string, stdout, stderr io.ReadCloser) {
go func() {
buf := make([]byte, 1024)
for {
n, err := stdout.Read(buf)
if n > 0 {
msg := strings.TrimSpace(string(buf[:n]))
if msg != "" {
l.Info("[%s:STDOUT] %s", prefix, msg)
}
}
if err != nil {
break
}
}
}()
go func() {
buf := make([]byte, 1024)
for {
n, err := stderr.Read(buf)
if n > 0 {
msg := strings.TrimSpace(string(buf[:n]))
if msg != "" {
l.Warn("[%s:STDERR] %s", prefix, msg)
}
}
if err != nil {
break
}
}
}()
}