55 "fmt"
66 "os"
77 "path"
8+ "sort"
89 "strings"
910 "sync"
1011 "time"
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
244246func 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
295354func 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)
320387func 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