fix(logging): keep crash reports and rotate stall dumps
Queued log entries are lost on a fatal crash, so the runtime crash report (panic and every stack) now also goes to <run>.crash next to the run log. Crash reports and watchdog stall dumps rotate with the runs.
- commit
- d17a46fe3f3baf6c0263e2587c42b63528d9e4f4
- author
- brice · · 2026-10-10 14:44 UTC
- committer
- brice · · 2026-10-10 15:39 UTC
- parent
- 6558c1f
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +15 -5
M internal/adapters/logging/logging.go +15 -5
| @@ -7,6 +7,7 @@ import ( | |||
| 7 | 7 | "io" | |
| 8 | 8 | "os" | |
| 9 | 9 | "path/filepath" | |
| 10 | + | runtimedebug "runtime/debug" | |
| 10 | 11 | "sort" | |
| 11 | 12 | "strings" | |
| 12 | 13 | "time" | |
| @@ -89,6 +90,12 @@ func Open(ctx context.Context, backend, level, debug string) (context.Context, f | |||
| 89 | 90 | return nil, nil, err | |
| 90 | 91 | } | |
| 91 | 92 | pruneRuns(dir) | |
| 93 | + | // Queued entries are lost on a fatal crash: the runtime's own report | |
| 94 | + | // (panic and every stack) goes to its own file, besides stderr. | |
| 95 | + | if crash, err := os.OpenFile(filepath.Join(dir, strings.TrimSuffix(name, ".log")+".crash"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600); err == nil { | |
| 96 | + | _ = runtimedebug.SetCrashOutput(crash, runtimedebug.CrashOptions{}) | |
| 97 | + | _ = crash.Close() // SetCrashOutput keeps its own duplicate | |
| 98 | + | } | |
| 92 | 99 | // Hide Close from the diode: asyncWriter closes the file once. | |
| 93 | 100 | var sink io.Writer = struct{ io.Writer }{file} | |
| 94 | 101 | if term.IsTerminal(int(os.Stderr.Fd())) { | |
| @@ -127,11 +134,14 @@ func (a asyncWriter) Close() error { | |||
| 127 | 134 | } | |
| 128 | 135 | ||
| 129 | 136 | func pruneRuns(dir string) { | |
| 130 | - | runs, _ := filepath.Glob(filepath.Join(dir, "2*.log")) | |
| 131 | - | sort.Strings(runs) | |
| 132 | - | for len(runs) > keepRuns { | |
| 133 | - | _ = os.Remove(runs[0]) | |
| 134 | - | runs = runs[1:] | |
| 137 | + | // Watchdog stall dumps (stall-<time>-<loop>.txt) rotate like runs. | |
| 138 | + | for _, pattern := range []string{"2*.log", "2*.crash", "stall-*.txt"} { | |
| 139 | + | runs, _ := filepath.Glob(filepath.Join(dir, pattern)) | |
| 140 | + | sort.Strings(runs) | |
| 141 | + | for len(runs) > keepRuns { | |
| 142 | + | _ = os.Remove(runs[0]) | |
| 143 | + | runs = runs[1:] | |
| 144 | + | } | |
| 135 | 145 | } | |
| 136 | 146 | } | |
| 137 | 147 | ||
| @@ -7,6 +7,7 @@ import ( | |||||
| 7 | "io" | 7 | "io" | ||
| 8 | "os" | 8 | "os" | ||
| 9 | "path/filepath" | 9 | "path/filepath" | ||
| 10 | + | runtimedebug "runtime/debug" | |||
| 10 | "sort" | 11 | "sort" | ||
| 11 | "strings" | 12 | "strings" | ||
| 12 | "time" | 13 | "time" | ||
| @@ -89,6 +90,12 @@ func Open(ctx context.Context, backend, level, debug string) (context.Context, f | |||||
| 89 | return nil, nil, err | 90 | return nil, nil, err | ||
| 90 | } | 91 | } | ||
| 91 | pruneRuns(dir) | 92 | pruneRuns(dir) | ||
| 93 | + | // Queued entries are lost on a fatal crash: the runtime's own report | |||
| 94 | + | // (panic and every stack) goes to its own file, besides stderr. | |||
| 95 | + | if crash, err := os.OpenFile(filepath.Join(dir, strings.TrimSuffix(name, ".log")+".crash"), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o600); err == nil { | |||
| 96 | + | _ = runtimedebug.SetCrashOutput(crash, runtimedebug.CrashOptions{}) | |||
| 97 | + | _ = crash.Close() // SetCrashOutput keeps its own duplicate | |||
| 98 | + | } | |||
| 92 | // Hide Close from the diode: asyncWriter closes the file once. | 99 | // Hide Close from the diode: asyncWriter closes the file once. | ||
| 93 | var sink io.Writer = struct{ io.Writer }{file} | 100 | var sink io.Writer = struct{ io.Writer }{file} | ||
| 94 | if term.IsTerminal(int(os.Stderr.Fd())) { | 101 | if term.IsTerminal(int(os.Stderr.Fd())) { | ||
| @@ -127,11 +134,14 @@ func (a asyncWriter) Close() error { | |||||
| 127 | } | 134 | } | ||
| 128 | 135 | ||||
| 129 | func pruneRuns(dir string) { | 136 | func pruneRuns(dir string) { | ||
| 130 | - | runs, _ := filepath.Glob(filepath.Join(dir, "2*.log")) | 137 | + | // Watchdog stall dumps (stall-<time>-<loop>.txt) rotate like runs. |
| 131 | - | sort.Strings(runs) | 138 | + | for _, pattern := range []string{"2*.log", "2*.crash", "stall-*.txt"} { |
| 132 | - | for len(runs) > keepRuns { | 139 | + | runs, _ := filepath.Glob(filepath.Join(dir, pattern)) |
| 133 | - | _ = os.Remove(runs[0]) | 140 | + | sort.Strings(runs) |
| 134 | - | runs = runs[1:] | 141 | + | for len(runs) > keepRuns { |
| 142 | + | _ = os.Remove(runs[0]) | |||
| 143 | + | runs = runs[1:] | |||
| 144 | + | } | |||
| 135 | } | 145 | } | ||
| 136 | } | 146 | } | ||
| 137 | 147 | ||||