// Package applog gives ipswap a rotating log file. Every netsh invocation and // its raw output lands here, which is the only forensic trail available when a // switch misbehaves on a customer site. package applog import ( "fmt" "io" "log" "os" "path/filepath" "sync" ) const ( maxBytes = 1 << 20 // 1 MB per file maxFiles = 5 // ipswap.log plus ipswap.log.1 .. .4 ) // Writer is an io.Writer that rotates at maxBytes and keeps maxFiles files. // It is small on purpose: a rotation dependency for one log file is not worth // the supply chain. type Writer struct { path string mu sync.Mutex f *os.File size int64 } // New opens the log at path, creating the directory if needed. func New(path string) (*Writer, error) { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return nil, fmt.Errorf("creating log directory: %w", err) } w := &Writer{path: path} if err := w.open(); err != nil { return nil, err } return w, nil } func (w *Writer) open() error { f, err := os.OpenFile(w.path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { return fmt.Errorf("opening %s: %w", w.path, err) } info, err := f.Stat() if err != nil { f.Close() return fmt.Errorf("stat %s: %w", w.path, err) } w.f, w.size = f, info.Size() return nil } func (w *Writer) Write(p []byte) (int, error) { w.mu.Lock() defer w.mu.Unlock() if w.f == nil { return len(p), nil // closed; drop rather than error out a log call } if w.size+int64(len(p)) > maxBytes { if err := w.rotate(); err != nil { return 0, err } } n, err := w.f.Write(p) w.size += int64(n) return n, err } // rotate shifts ipswap.log.N to N+1 and starts a fresh ipswap.log. The caller // holds w.mu. func (w *Writer) rotate() error { if err := w.f.Close(); err != nil { return err } w.f = nil // Walk downwards so nothing is overwritten before it has been moved. for i := maxFiles - 1; i >= 1; i-- { older := fmt.Sprintf("%s.%d", w.path, i) if i == maxFiles-1 { os.Remove(older) // the oldest file falls off the end } var newer string if i == 1 { newer = w.path } else { newer = fmt.Sprintf("%s.%d", w.path, i-1) } if _, err := os.Stat(newer); err == nil { os.Rename(newer, older) } } return w.open() } // Close closes the underlying file. func (w *Writer) Close() error { w.mu.Lock() defer w.mu.Unlock() if w.f == nil { return nil } err := w.f.Close() w.f = nil return err } // Setup points the standard logger at the rotating file and returns it so the // caller can close it on exit. // // With -H windowsgui there is no console, so stderr goes nowhere on Windows and // the file is the only output that survives. During development on Linux the // log is also echoed to stderr. func Setup(path string, alsoStderr bool) (*Writer, error) { w, err := New(path) if err != nil { return nil, err } var out io.Writer = w if alsoStderr { out = io.MultiWriter(w, os.Stderr) } log.SetOutput(out) log.SetFlags(log.LstdFlags | log.Lmsgprefix) return w, nil }