243 lines
4.5 KiB
Go
243 lines
4.5 KiB
Go
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
|
|
}
|
|
}
|
|
}()
|
|
}
|