fwt_software/src/core/FileLogSink.cpp

215 lines
6.9 KiB
C++

#include "fgc/FileLogSink.h"
#include "fgc/Paths.h"
#include <algorithm>
#include <chrono>
#include <cstring>
#include <ctime>
#include <filesystem>
#include <system_error>
// NOTE: no "fgc/Logger.h" logging in this file, by design. Logger calls this
// sink while holding a non-recursive mutex, so a LOG_* from here deadlocks the
// process. Errors go to failed()/lastError() for the caller to report.
namespace fgc {
namespace {
namespace fs = std::filesystem;
// "YYYY-MM-DD ", prepended to each line. Logger's own stamp is HH:MM:SS.mmm with
// no date, which is fine on screen and ambiguous in a file that may span days.
std::string today() {
const auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm tm{};
localtime_r(&t, &tm);
char buf[16];
std::strftime(buf, sizeof(buf), "%Y-%m-%d ", &tm);
return buf;
}
// "YYYYMMDD-HHMMSS", matching paths::timestampedLogName()'s stamp so every file
// in the log dir sorts the same way regardless of which writer made it.
std::string runStamp() {
const auto t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now());
std::tm tm{};
localtime_r(&t, &tm);
char buf[32];
std::strftime(buf, sizeof(buf), "%Y%m%d-%H%M%S", &tm);
return buf;
}
std::string dirOf(const FileLogPolicy& p) {
return p.dir.empty() ? paths::defaultLogDir() : p.dir;
}
} // namespace
FileLogSink::~FileLogSink() { close(); }
std::vector<std::string> FileLogSink::existingFiles(const FileLogPolicy& policy) {
std::vector<std::string> out;
std::error_code ec;
const fs::path dir = dirOf(policy);
if (!fs::exists(dir, ec)) return out;
const std::string want = policy.prefix + "_";
for (const auto& e : fs::directory_iterator(dir, ec)) {
if (ec) break;
if (!e.is_regular_file(ec)) continue;
const std::string name = e.path().filename().string();
if (name.rfind(want, 0) != 0) continue;
if (name.size() < 4 || name.substr(name.size() - 4) != ".log") continue;
out.push_back(e.path().string());
}
// Names embed YYYYMMDD-HHMMSS, so lexicographic order is chronological.
// Newest first.
std::sort(out.begin(), out.end(), std::greater<std::string>());
return out;
}
bool FileLogSink::open(const FileLogPolicy& policy, const std::string& header) {
close();
policy_ = policy;
header_ = header;
failed_ = false;
last_error_.clear();
rotation_ = 0;
std::error_code ec;
fs::create_directories(dirOf(policy_), ec);
if (ec) {
fail("cannot create " + dirOf(policy_) + ": " + ec.message());
return false;
}
// "<dir>/<prefix>_<stamp>"; the segment number and ".log" are appended by
// segmentPath(). paths::timestampedLogName() is not reused: it bakes in the
// ".log", and the segment has to sit BEFORE the extension.
base_ = (fs::path(dirOf(policy_)) / (policy_.prefix + "_" + runStamp())).string();
if (!openFile(segmentPath(0))) return false;
// Prune AFTER opening, so the file we just created is counted and an
// aggressive keep_files cannot leave us with nothing.
prune();
return true;
}
bool FileLogSink::openFile(const std::string& path) {
fp_ = std::fopen(path.c_str(), "ab");
if (!fp_) {
fail("cannot open " + path + ": " + std::strerror(errno));
return false;
}
path_ = path;
bytes_ = 0;
lines_ = 0;
std::error_code ec;
const auto sz = fs::file_size(path, ec);
if (!ec) bytes_ = static_cast<long long>(sz);
if (!header_.empty()) {
const std::string h = header_ + "\n";
if (std::fwrite(h.data(), 1, h.size(), fp_) != h.size()) {
fail("write failed on " + path);
return false;
}
bytes_ += static_cast<long long>(h.size());
std::fflush(fp_);
}
return true;
}
void FileLogSink::write(LogLevel, LogCat, const std::string& line) {
if (failed_ || !fp_) return;
// Nothing here may escape: this runs inside an arbitrary LOG_* statement,
// under Logger's lock. A throw would propagate out of, say, a LOG_INFO in
// the control loop.
try {
const std::string out = today() + line + "\n";
// Rotate BEFORE the write that would cross the limit, so a file never
// exceeds max_bytes rather than overshooting by one line.
//
// `lines_` guards two degenerate cases: a max_bytes smaller than the
// header would otherwise rotate on every single line, burning through
// the whole retention set in a loop; and a single line longer than
// max_bytes would rotate into a fresh file and still not fit. Requiring
// at least one line in the current file makes both terminate - an
// oversized line simply lands whole in a file of its own.
if (policy_.max_bytes > 0 && lines_ > 0 &&
bytes_ + static_cast<long long>(out.size()) > policy_.max_bytes) {
rotate();
if (failed_ || !fp_) return;
}
if (std::fwrite(out.data(), 1, out.size(), fp_) != out.size()) {
fail("write failed on " + path_);
return;
}
bytes_ += static_cast<long long>(out.size());
++lines_;
// Flush, never fsync: a crashed process keeps these bytes, and fsync per
// line on eMMC would cost far more than the power-cut tail it would save.
std::fflush(fp_);
} catch (...) {
fail("exception while writing " + path_);
}
}
// "<base>.<NNN>.log". The segment is ZERO-PADDED and always present, including
// on the first file, so that a plain lexicographic sort of the directory is
// chronological. Without the padding ".log" sorts above ".2.log" ('l' > '2'),
// and pruning would keep the oldest segment and delete the newest.
std::string FileLogSink::segmentPath(int n) const {
char suffix[16];
std::snprintf(suffix, sizeof(suffix), ".%03d.log", n);
return base_ + suffix;
}
void FileLogSink::rotate() {
if (fp_) {
std::fclose(fp_);
fp_ = nullptr;
}
++rotation_;
if (!openFile(segmentPath(rotation_))) return;
prune();
}
void FileLogSink::prune() {
if (policy_.keep_files <= 0) return;
auto files = existingFiles(policy_); // newest first
if (static_cast<int>(files.size()) <= policy_.keep_files) return;
std::error_code ec;
for (size_t i = static_cast<size_t>(policy_.keep_files); i < files.size(); ++i) {
if (files[i] == path_) continue; // never delete the file we are writing
fs::remove(files[i], ec); // best effort; a failure here is not fatal
}
}
void FileLogSink::close() {
if (fp_) {
std::fflush(fp_);
std::fclose(fp_);
fp_ = nullptr;
}
path_.clear();
bytes_ = 0;
}
void FileLogSink::fail(const std::string& what) {
failed_ = true;
last_error_ = what;
if (fp_) {
std::fclose(fp_);
fp_ = nullptr;
}
}
} // namespace fgc