215 lines
6.9 KiB
C++
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
|