#include "fgc/FileLogSink.h" #include "fgc/Paths.h" #include #include #include #include #include #include // 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 FileLogSink::existingFiles(const FileLogPolicy& policy) { std::vector 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()); 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; } // "/_"; 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(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(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(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(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_); } } // "..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(files.size()) <= policy_.keep_files) return; std::error_code ec; for (size_t i = static_cast(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