fwt_software/include/fgc/FileLogSink.h

106 lines
4.5 KiB
C++

#pragma once
#include "fgc/Logger.h"
#include <cstdio>
#include <mutex>
#include <string>
#include <vector>
namespace fgc {
// A rotating, size-capped log file for the tower's own operation.
//
// A tower runs detached for days inside a tmux session, and in TUI mode every
// log line goes into a 500-line ring buffer on screen and nowhere else - so the
// record of what it did dies with the session. That is the one thing you want
// when somebody asks why an axis is not moving.
//
// This is deliberately a plain appender rather than anything clever:
//
// * It runs INSIDE Logger's lock. Logger holds a non-recursive mutex while it
// calls the sink, so every method here MUST be free of LOG_* - one logging
// call from this file deadlocks the whole process. Failures are recorded and
// reported through failed()/lastError(), which the caller polls from OUTSIDE
// the lock. That constraint is why paths::writeLogFile() is not reused: it
// LOG_WARNs on failure, and it truncates rather than appends.
// * It flushes every line but never fsyncs. A crashed process keeps everything
// (the bytes are already in the page cache); only a power cut loses the tail.
// fsync per line on a tower's eMMC would be far worse than that trade.
// * Rotation is by size, checked before each write. Files are pruned by count,
// oldest first.
struct FileLogPolicy {
// Empty => paths::defaultLogDir().
std::string dir;
// Rotate once a write would push the file past this. 0 disables rotation.
long long max_bytes = 20LL * 1024 * 1024;
// How many <prefix>_*.log to keep in `dir`, counting every run's files and
// every rotation of them. Oldest are deleted. 0 disables pruning.
int keep_files = 10;
// File name prefix. Deliberately NOT "test": Application::latestPriorTestReport()
// scans this same directory for test_*.log and would otherwise pick up ours.
std::string prefix = "fgc";
};
class FileLogSink {
public:
FileLogSink() = default;
~FileLogSink();
FileLogSink(const FileLogSink&) = delete;
FileLogSink& operator=(const FileLogSink&) = delete;
// Create the directory if needed and open a fresh file for this run. `header`
// is written first and re-written after every rotation, so each file on disk
// says for itself which build and tower produced it. Returns false and sets
// failed() if the file cannot be opened.
bool open(const FileLogPolicy& policy, const std::string& header);
// Append one already-formatted Logger line (no trailing newline, and it
// begins "HH:MM:SS.mmm"). A date is prepended here rather than in Logger,
// because the on-screen panes are width-constrained and a file that may span
// days is the only place the date is actually needed.
//
// `cat` distinguishes a wire-trace line from an ordinary one; the caller uses
// it to decide routing, and it is recorded here only for completeness.
// Never throws, never logs. A no-op once failed().
void write(LogLevel level, LogCat cat, const std::string& line);
void close();
bool isOpen() const { return fp_ != nullptr; }
// The file being written now. Safe from any thread: rotation changes it
// under Logger's lock, while the dashboard reads it for the LOG title.
std::string path() const {
std::lock_guard<std::mutex> lock(path_mutex_);
return path_;
}
// Set when an open, write or rotate failed. The sink then stops writing
// rather than retrying every line. Poll this from outside Logger's lock.
bool failed() const { return failed_; }
std::string lastError() const { return last_error_; }
// Exposed for tests: the files this policy would prune, newest first.
static std::vector<std::string> existingFiles(const FileLogPolicy& policy);
private:
std::string segmentPath(int n) const;
bool openFile(const std::string& path);
void rotate();
void prune();
void fail(const std::string& what);
std::FILE* fp_ = nullptr;
FileLogPolicy policy_;
std::string header_;
std::string path_; // written under path_mutex_; read freely by this sink
mutable std::mutex path_mutex_;
std::string base_; // path without the ".<NNN>.log" suffix
long long bytes_ = 0;
long long lines_ = 0; // lines in the CURRENT file; guards rotation loops
int rotation_ = 0;
bool failed_ = false;
std::string last_error_;
};
} // namespace fgc