104 lines
2.9 KiB
Go
104 lines
2.9 KiB
Go
// Package applog routes the app's diagnostic output to a rotating log
|
|
// file inside the user's data dir. Wails builds with the Windows GUI
|
|
// subsystem by default — fmt.Println output is dropped, so launching
|
|
// from cmd never showed anything. The file gives us a reliable place to
|
|
// inspect what the UDP listener / cluster / CAT layer is doing.
|
|
package applog
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
mu sync.Mutex
|
|
file *os.File
|
|
path string
|
|
)
|
|
|
|
// Init opens (creates) the log file in dataDir. On rotation we truncate
|
|
// at startup if the file is too big; for now it's a single file, no
|
|
// rolling — the volume is low (a few KB per session).
|
|
func Init(dataDir string) (string, error) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if file != nil {
|
|
return path, nil
|
|
}
|
|
if dataDir == "" {
|
|
return "", fmt.Errorf("empty data dir")
|
|
}
|
|
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
|
return "", err
|
|
}
|
|
logPath := filepath.Join(dataDir, "opslog.log")
|
|
// One-shot rename for users coming from the HamLog era.
|
|
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
|
oldLog := filepath.Join(dataDir, "hamlog.log")
|
|
if _, err := os.Stat(oldLog); err == nil {
|
|
_ = os.Rename(oldLog, logPath)
|
|
}
|
|
}
|
|
// Truncate if the file grew past ~5MB so we don't accumulate logs
|
|
// forever. We keep one file — simple and adequate for diagnostics.
|
|
if fi, err := os.Stat(logPath); err == nil && fi.Size() > 5*1024*1024 {
|
|
_ = os.Remove(logPath)
|
|
}
|
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
file = f
|
|
path = logPath
|
|
|
|
// Redirect log.Print* and the standard logger to the file too, so
|
|
// any third-party output stays consistent.
|
|
log.SetOutput(io.MultiWriter(file, os.Stderr))
|
|
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
|
|
|
|
fmt.Fprintf(file, "\n────── OpsLog start %s ──────\n", time.Now().Format(time.RFC3339))
|
|
return logPath, nil
|
|
}
|
|
|
|
// Printf writes a formatted line with a timestamp. Caller's format may
|
|
// or may not end with a newline — we strip a trailing one before adding
|
|
// our own, so log entries always look like "HH:MM:SS.mmm msg\n".
|
|
func Printf(format string, args ...any) {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
stamp := time.Now().Format("15:04:05.000")
|
|
msg := fmt.Sprintf(format, args...)
|
|
for len(msg) > 0 && (msg[len(msg)-1] == '\n' || msg[len(msg)-1] == '\r') {
|
|
msg = msg[:len(msg)-1]
|
|
}
|
|
if file != nil {
|
|
fmt.Fprintf(file, "%s %s\n", stamp, msg)
|
|
}
|
|
// Also dump to stderr in case the binary was launched with a console
|
|
// attached (wails dev, custom build).
|
|
fmt.Fprintf(os.Stderr, "%s %s\n", stamp, msg)
|
|
}
|
|
|
|
// Path returns where the file is so the UI can surface it.
|
|
func Path() string {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
return path
|
|
}
|
|
|
|
// Close flushes and releases the handle. Called from shutdown.
|
|
func Close() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
if file != nil {
|
|
_ = file.Close()
|
|
file = nil
|
|
}
|
|
}
|