Skip to content

Commit a2da3a2

Browse files
committed
feat(logging): enhance file logging with cleanup and exec ID prefix
1 parent 322c946 commit a2da3a2

1 file changed

Lines changed: 98 additions & 1 deletion

File tree

core/env/env.go

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"fmt"
66
"os"
77
"path"
8+
"sort"
89
"strings"
910
"sync"
1011
"time"
@@ -46,6 +47,7 @@ var (
4647
// File logging
4748
debugLogFile *os.File
4849
traceLogFile *os.File
50+
logFileInit = false
4951
logFileMux sync.Mutex
5052
GetOAuthMap = func() map[string]map[string]any {
5153
return map[string]map[string]any{}
@@ -242,6 +244,10 @@ func InitLogger() {
242244

243245
// setupFileLogging initializes file logging based on SLING_DEBUG_FILE and SLING_TRACE_FILE env vars
244246
func setupFileLogging() {
247+
if IsThreadChild {
248+
return // don't write log from child processes
249+
}
250+
245251
logFileMux.Lock()
246252
defer logFileMux.Unlock()
247253

@@ -255,6 +261,9 @@ func setupFileLogging() {
255261
traceLogFile = nil
256262
}
257263

264+
// setup env from env.yaml
265+
LoadSlingEnvFile()
266+
258267
// Open debug log file
259268
if debugPath := os.Getenv("SLING_DEBUG_FILE"); debugPath != "" {
260269
f, err := os.OpenFile(debugPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
@@ -265,6 +274,29 @@ func setupFileLogging() {
265274
}
266275
}
267276

277+
// Open debug log file from SLING_LOG_DIR (date-based rotation)
278+
// Only if SLING_DEBUG_FILE wasn't set and this is not a thread child process
279+
if logDir := os.Getenv("SLING_LOG_DIR"); logDir != "" && debugLogFile == nil {
280+
// Expand ~ to home directory
281+
if strings.HasPrefix(logDir, "~/") {
282+
logDir = path.Join(g.UserHomeDir(), logDir[2:])
283+
}
284+
if err := os.MkdirAll(logDir, 0755); err != nil {
285+
g.Warn("could not create log directory: %s", err.Error())
286+
} else {
287+
logFileName := "sling_debug_" + time.Now().Format("2006_01_02") + ".log"
288+
logPath := path.Join(logDir, logFileName)
289+
290+
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
291+
if err != nil {
292+
g.Warn("could not open log file: %s", err.Error())
293+
} else {
294+
debugLogFile = f
295+
cleanupOldLogFiles(logDir, 15)
296+
}
297+
}
298+
}
299+
268300
// Open trace log file
269301
if tracePath := os.Getenv("SLING_TRACE_FILE"); tracePath != "" {
270302
f, err := os.OpenFile(tracePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
@@ -291,6 +323,33 @@ func CloseFileLogging() {
291323
}
292324
}
293325

326+
// cleanupOldLogFiles removes old .log files from the directory, keeping the latest `keep` files.
327+
// Files are sorted by name (which sorts chronologically for date-based filenames).
328+
func cleanupOldLogFiles(dir string, keep int) {
329+
entries, err := os.ReadDir(dir)
330+
if err != nil {
331+
g.Warn("could not read log directory for cleanup: %s", err.Error())
332+
return
333+
}
334+
335+
var logFiles []string
336+
for _, entry := range entries {
337+
if !entry.IsDir() && strings.HasSuffix(entry.Name(), ".log") {
338+
logFiles = append(logFiles, entry.Name())
339+
}
340+
}
341+
342+
sort.Strings(logFiles)
343+
344+
if len(logFiles) > keep {
345+
for _, name := range logFiles[:len(logFiles)-keep] {
346+
if err := os.Remove(path.Join(dir, name)); err != nil {
347+
g.Warn("could not remove old log file %s: %s", name, err.Error())
348+
}
349+
}
350+
}
351+
}
352+
294353
// stripANSI removes ANSI escape codes from a string
295354
func stripANSI(text string) string {
296355
// Match ANSI escape sequences: ESC[ followed by any number of params and a letter
@@ -316,6 +375,14 @@ func stripANSI(text string) string {
316375
return result.String()
317376
}
318377

378+
func shortExecID() string {
379+
val := ExecID
380+
if len(val) > 8 {
381+
val = val[len(val)-8:]
382+
}
383+
return val
384+
}
385+
319386
// formatLogLine formats a log line for file output (no colors)
320387
func formatLogLine(ll *g.LogLine) string {
321388
var levelPrefix string
@@ -358,7 +425,23 @@ func formatLogLine(ll *g.LogLine) string {
358425
// Strip any ANSI codes from the text
359426
text = stripANSI(text)
360427

361-
return fmt.Sprintf("%s %s%s\n", timeText, levelPrefix, text)
428+
return fmt.Sprintf("%s | %s %s%s\n", shortExecID(), timeText, levelPrefix, text)
429+
}
430+
431+
func writeHeader(logFile *os.File) {
432+
// Write session header
433+
wd, _ := os.Getwd()
434+
header := fmt.Sprintf(
435+
"\n%s\n== %s | version: %s | exec_id: %s\n== dir: %s | command: %s\n%s\n",
436+
strings.Repeat("=", 100),
437+
time.Now().Format("2006-01-02 15:04:05"),
438+
core.Version,
439+
ExecID,
440+
wd,
441+
strings.Join(os.Args, " "),
442+
strings.Repeat("=", 80),
443+
)
444+
logFile.WriteString(header)
362445
}
363446

364447
// writeToLogFile writes the log entry to configured log file(s)
@@ -371,6 +454,16 @@ func writeToLogFile(ll *g.LogLine) {
371454
return
372455
}
373456

457+
if !logFileInit {
458+
if debugLogFile != nil {
459+
writeHeader(debugLogFile)
460+
}
461+
if traceLogFile != nil {
462+
writeHeader(traceLogFile)
463+
}
464+
logFileInit = true
465+
}
466+
374467
level := zerolog.Level(ll.Level)
375468

376469
// Handle Print/Println entries (level 9) - these are raw output from child processes
@@ -380,6 +473,10 @@ func writeToLogFile(ll *g.LogLine) {
380473
if strings.TrimSpace(text) == "" {
381474
return
382475
}
476+
477+
// Add execID prefix
478+
text = shortExecID() + " | " + text
479+
383480
// Ensure text ends with newline
384481
if !strings.HasSuffix(text, "\n") {
385482
text = text + "\n"

0 commit comments

Comments
 (0)