Files
ctrld/cmd/cli/log_path.go
T
Anthony Wong 8934e09428 feat(cli): keep a log journal on disk with file headers and an upload limit
ctrld keeps a second log file, ctrld-journal.log, next to ctrld.log. The
journal holds all warnings and errors and each event with the field
journal=true. It stays on disk after a start of the service and after a
self-upgrade. Each log file starts with a Log header line. Each log send
upload starts with the same line, made at send time, and has a limit of
about 16 MB.
2026-09-28 16:47:40 +07:00

74 lines
2.2 KiB
Go

package cli
import (
"os"
"sync/atomic"
"github.com/Control-D-Inc/ctrld/internal/router"
)
// logPathFile is the open log_path file. The header renderer and the log
// readers need it, and both run outside this file.
var logPathFile atomic.Pointer[rotatingFile]
// pendingLogPathRotation holds the rotation of a start-time backup. The
// logger reaches no file at that point, so initLogging reports it later.
var pendingLogPathRotation atomic.Pointer[logRotation]
// openLogPathWriter opens the log_path file of this run and stores it for the
// header renderer and the log readers. It returns nil when no log_path is set.
// A start calls it more than once, so a call for the file that is already open
// keeps that file: a second open would put a second header in it.
func openLogPathWriter(doBackup bool) *rotatingFile {
logFilePath := normalizeLogFilePath(cfg.Service.LogPath)
if logFilePath == "" {
return nil
}
budget := debugLogBudget(&cfg.Service, router.Name() != "")
rf := logPathFile.Load()
if rf == nil || rf.currentPath() != logFilePath {
rf = newLogPathFile(logFilePath, budget)
} else {
rf.setBudget(budget)
}
if doBackup {
backupLogPathFile(rf)
}
return rf
}
// newLogPathFile opens path and puts it in place of the file of an earlier
// call. A ctrld that cannot write the log file its operator asked for leaves
// no record of the run, so the open failure ends the process.
func newLogPathFile(path string, budget logBudget) *rotatingFile {
rf, err := newRotatingFile(path, budget, nil)
if err != nil {
mainLog.Load().Error().Msgf("failed to create log file: %v", err)
os.Exit(1)
}
rf.onRotate = emitLogRotated
if old := logPathFile.Swap(rf); old != nil {
_ = old.close()
}
return rf
}
// backupLogPathFile moves the file of an earlier run away, so no run
// overwrites another.
func backupLogPathFile(rf *rotatingFile) {
if !logFileHasContent(rf.currentPath()) {
return
}
record, err := rf.rotateNow()
if err != nil {
mainLog.Load().Error().Msgf("could not backup old log file: %v", err)
return
}
pendingLogPathRotation.Store(record)
}
func logFileHasContent(path string) bool {
st, err := os.Stat(path)
return err == nil && st.Size() > 0
}