fwt_software/src/core/Logger.cpp

209 lines
7.1 KiB
C++

#include "fgc/Logger.h"
#include <atomic>
#include <chrono>
#include <cstdio>
#include <ctime>
#include <iostream>
#include <mutex>
namespace fgc {
namespace {
std::atomic<LogLevel> g_level{LogLevel::Info}; // CONSOLE threshold
std::atomic<LogLevel> g_file_level{LogLevel::Off}; // Off => file wants nothing
std::atomic<unsigned> g_cats{0}; // every wire-trace category OFF by default
std::mutex g_mutex;
Logger::LogSink g_sink; // empty => default stdout/stderr writer (below)
Logger::FileSink g_file_sink; // empty => no file logging
// Default writer: the stdout/stderr split this logger has always used.
void writeDefault(LogLevel level, const std::string& line) {
std::ostream& out = (level >= LogLevel::Warn) ? std::cerr : std::cout;
out << line << '\n';
out.flush();
}
const char* tag(LogLevel l) {
switch (l) {
case LogLevel::Trace: return "TRACE";
case LogLevel::Debug: return "DEBUG";
case LogLevel::Info: return "INFO ";
case LogLevel::Warn: return "WARN ";
case LogLevel::Error: return "ERROR";
case LogLevel::Off: return "OFF ";
}
return "?????";
}
const char* catTag(LogCat c) {
switch (c) {
case LogCat::Serial: return "SERIAL";
case LogCat::Mqtt: return "MQTT";
case LogCat::Camera: return "CAMERA";
case LogCat::Control: return "CONTROL";
default: return "TRACE";
}
}
std::string lower(const std::string& s) {
std::string v;
for (char c : s) v += static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return v;
}
// Trim leading/trailing ASCII whitespace.
std::string trim(const std::string& s) {
size_t b = s.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return "";
size_t e = s.find_last_not_of(" \t\r\n");
return s.substr(b, e - b + 1);
}
std::string timestamp() {
using namespace std::chrono;
auto now = system_clock::now();
auto t = system_clock::to_time_t(now);
auto ms = duration_cast<milliseconds>(now.time_since_epoch()) % 1000;
std::tm tm{};
localtime_r(&t, &tm);
char buf[32];
std::snprintf(buf, sizeof(buf), "%02d:%02d:%02d.%03d", tm.tm_hour, tm.tm_min,
tm.tm_sec, static_cast<int>(ms.count()));
return buf;
}
} // namespace
void Logger::setSink(LogSink sink) {
std::lock_guard<std::mutex> lock(g_mutex);
g_sink = std::move(sink);
}
void Logger::setLevel(LogLevel level) { g_level.store(level); }
LogLevel Logger::level() { return g_level.load(); }
void Logger::setFileLevel(LogLevel level) { g_file_level.store(level); }
LogLevel Logger::fileLevel() { return g_file_level.load(); }
// The macros must let through anything EITHER destination wants, or a file set
// to debug could never receive a debug line while the console sits at info.
LogLevel Logger::effectiveLevel() {
const LogLevel c = g_level.load();
const LogLevel f = g_file_level.load();
return (f < c) ? f : c;
}
bool Logger::enabled(LogLevel level) { return level >= effectiveLevel(); }
void Logger::setFileSink(FileSink sink) {
std::lock_guard<std::mutex> lock(g_mutex);
g_file_sink = std::move(sink);
}
// Shared by both threshold setters; the config path needs the pure parse too.
static bool parseLevel(const std::string& s, LogLevel& out) {
std::string v = lower(s);
if (v == "trace") { out = LogLevel::Trace; return true; }
if (v == "debug") { out = LogLevel::Debug; return true; }
if (v == "info") { out = LogLevel::Info; return true; }
if (v == "warn" || v == "warning") { out = LogLevel::Warn; return true; }
if (v == "error") { out = LogLevel::Error; return true; }
if (v == "off" || v == "none") { out = LogLevel::Off; return true; }
return false;
}
bool Logger::setLevelFromString(const std::string& s) {
LogLevel l{};
if (!parseLevel(s, l)) return false;
setLevel(l);
return true;
}
bool Logger::setFileLevelFromString(const std::string& s) {
LogLevel l{};
if (!parseLevel(s, l)) return false;
setFileLevel(l);
return true;
}
void Logger::setCategories(unsigned mask) { g_cats.store(mask); }
unsigned Logger::categories() { return g_cats.load(); }
void Logger::enableCategory(LogCat cat) { g_cats.fetch_or(static_cast<unsigned>(cat)); }
void Logger::disableCategory(LogCat cat) { g_cats.fetch_and(~static_cast<unsigned>(cat)); }
bool Logger::categoryEnabled(LogCat cat) {
return (g_cats.load() & static_cast<unsigned>(cat)) != 0;
}
LogCat Logger::catFromString(const std::string& name, bool* ok) {
std::string v = lower(trim(name));
if (ok) *ok = true;
if (v == "serial") return LogCat::Serial;
if (v == "mqtt") return LogCat::Mqtt;
if (v == "camera") return LogCat::Camera;
if (v == "control") return LogCat::Control;
if (v == "all") return LogCat::All;
if (v == "none" || v.empty()) return LogCat::None;
if (ok) *ok = false;
return LogCat::None;
}
unsigned Logger::parseCategories(const std::string& csv, bool* ok) {
if (ok) *ok = true;
unsigned mask = 0;
size_t start = 0;
while (start <= csv.size()) {
size_t comma = csv.find(',', start);
std::string token = csv.substr(start, comma == std::string::npos ? std::string::npos
: comma - start);
std::string t = trim(token);
if (!t.empty()) {
bool tok_ok = true;
mask |= static_cast<unsigned>(catFromString(t, &tok_ok));
if (!tok_ok && ok) *ok = false;
}
if (comma == std::string::npos) break;
start = comma + 1;
}
return mask;
}
LogStream::LogStream(LogLevel level) : level_(level), enabled_(Logger::enabled(level)) {}
LogStream::LogStream(LogLevel level, LogCat cat)
: level_(level), cat_(cat),
// A category line survives if EITHER destination would take it, which is
// why this asks the effective level rather than the console's.
enabled_(Logger::categoryEnabled(cat) && Logger::effectiveLevel() != LogLevel::Off) {}
void LogStream::commit() {
done_ = true;
if (!enabled_) return;
const bool is_cat = (cat_ != LogCat::None);
const char* label = is_cat ? catTag(cat_) : tag(level_);
std::string line = timestamp() + " [" + label + "] " + buffer_.str();
std::lock_guard<std::mutex> lock(g_mutex);
// The file first: if the console path stalls or throws, the durable record
// is already out. Everything below runs with g_mutex HELD - a sink that
// logs from in here deadlocks the process.
//
// A category line bypasses BOTH level filters. It was already gated by
// categoryEnabled() at construction, and it carries level == Trace, so
// comparing it against a threshold would silently drop every wire trace at
// the default info level - i.e. break `--trace serial` for both outputs.
if (g_file_sink && (is_cat || level_ >= g_file_level.load())) {
g_file_sink(level_, cat_, line);
}
if (is_cat || level_ >= g_level.load()) {
if (g_sink) g_sink(level_, line);
else writeDefault(level_, line);
}
}
} // namespace fgc