fwt_software/include/fgc/Logger.h

141 lines
6.3 KiB
C++

#pragma once
#include <functional>
#include <ostream>
#include <sstream>
#include <string>
namespace fgc {
enum class LogLevel { Trace = 0, Debug, Info, Warn, Error, Off };
// Wire-tracing categories. Orthogonal to LogLevel: a category emits verbatim
// TX/RX lines and is toggled independently of the linear level, so you can
// watch e.g. all serial traffic without turning everything else to Trace.
// Values are a bitmask (multiple categories enabled at once).
enum class LogCat : unsigned {
None = 0,
Serial = 1u << 0,
Mqtt = 1u << 1,
Camera = 1u << 2,
Control = 1u << 3,
All = Serial | Mqtt | Camera | Control,
};
// Minimal leveled, thread-safe logger. Each log line is assembled in a
// per-statement buffer and written atomically under a shared mutex, so lines
// from different threads never interleave. Level filtering is global.
//
// Usage: LOG_INFO << "starting, rate=" << rate;
// Lines at Warn/Error go to stderr; everything else to stdout.
class Logger {
public:
// The CONSOLE threshold. Kept as the plain name because every existing
// caller - `--log-level`, [Logging] level, the `debug` console command -
// means the console. See setFileLevel() for the file's own.
static void setLevel(LogLevel level);
static LogLevel level();
static bool enabled(LogLevel level);
// Parse "trace"|"debug"|"info"|"warn"|"error"|"off" (case-insensitive).
// Returns false (and leaves the level unchanged) on an unknown string.
static bool setLevelFromString(const std::string& s);
// Wire-trace categories (independent of the linear level above).
static void setCategories(unsigned mask);
static unsigned categories();
static void enableCategory(LogCat cat);
static void disableCategory(LogCat cat);
static bool categoryEnabled(LogCat cat);
// Parse a comma list: "serial,mqtt" | "all" | "none" (case-insensitive,
// whitespace-trimmed). Recognized tokens always apply; an unknown token
// sets *ok=false (when ok != nullptr) but does not discard the rest.
static unsigned parseCategories(const std::string& csv, bool* ok = nullptr);
// Map a single category name to its enum value. Unknown => LogCat::None and
// *ok=false (when ok != nullptr).
static LogCat catFromString(const std::string& name, bool* ok = nullptr);
// Output sink for finished log lines. The string is the fully formatted line
// (timestamp + "[LABEL]" + message, no trailing newline); `level` lets a sink
// colour or route by severity. Installing a sink REPLACES the default
// stdout/stderr writer entirely - used by the TUI to divert logs into an
// on-screen pane so they never corrupt the rendered screen. Pass an empty
// function to restore the default writer. Thread-safe.
using LogSink = std::function<void(LogLevel level, const std::string& line)>;
static void setSink(LogSink sink);
// --- persistent file logging ------------------------------------------
//
// A SECOND, independent sink slot. setSink() above belongs to the TUI, which
// installs its pane writer on start and clears it on stop; a file sink put
// through that one slot would be silently replaced, and the shutdown lines -
// logged after ui->stop() - are exactly the ones worth keeping.
//
// It takes the CATEGORY as well as the level. A wire-trace line arrives with
// level == Trace and is otherwise indistinguishable from a plain LOG_TRACE,
// and the file has to tell them apart to honour "traces only when asked for".
//
// WARNING: the sink runs while this logger's mutex is held, and that mutex is
// NOT recursive. A sink that calls LOG_* deadlocks the process - including
// the control loop, possibly mid-move. Sinks must report failure by latching
// a flag the caller polls, never by logging.
using FileSink = std::function<void(LogLevel level, LogCat cat, const std::string& line)>;
static void setFileSink(FileSink sink);
// The file's own threshold, independent of the console's, so the dashboard
// can stay at info while the file keeps debug.
//
// The LOG_* macros gate on the MORE verbose of the two, and each sink then
// filters to its own. Off (the default) means the file wants nothing, which
// leaves every existing path byte-for-byte as it was.
static void setFileLevel(LogLevel level);
static LogLevel fileLevel();
static bool setFileLevelFromString(const std::string& s);
// min(level(), fileLevel()) - what the macros actually gate on.
static LogLevel effectiveLevel();
};
// RAII helper that buffers one log line and flushes it on commit().
// Designed to be driven by the LOG_* macros' for-loop guard.
class LogStream {
public:
explicit LogStream(LogLevel level);
LogStream(LogLevel level, LogCat cat); // category (wire-trace) line
bool pending() const { return enabled_ && !done_; }
void commit();
std::ostream& stream() { return buffer_; }
private:
LogLevel level_;
LogCat cat_ = LogCat::None; // None => gated by level only
bool enabled_;
bool done_ = false;
std::ostringstream buffer_;
};
} // namespace fgc
// Brace-safe: the whole macro is a single for-statement, so it composes
// correctly inside if/else without dangling-else hazards, and the message
// expression is never evaluated when the level is disabled.
#define FGC_LOG_AT(level) \
for (::fgc::LogStream _fgc_ls(level); _fgc_ls.pending(); _fgc_ls.commit()) \
_fgc_ls.stream()
#define LOG_TRACE FGC_LOG_AT(::fgc::LogLevel::Trace)
#define LOG_DEBUG FGC_LOG_AT(::fgc::LogLevel::Debug)
#define LOG_INFO FGC_LOG_AT(::fgc::LogLevel::Info)
#define LOG_WARN FGC_LOG_AT(::fgc::LogLevel::Warn)
#define LOG_ERROR FGC_LOG_AT(::fgc::LogLevel::Error)
// Verbatim wire-trace line for one category. Prints when the category is
// enabled (independent of the linear level, except level Off silences all).
// The message expression is not evaluated when the category is off.
#define LOG_TRACE_CAT(cat) \
for (::fgc::LogStream _fgc_ls(::fgc::LogLevel::Trace, (cat)); _fgc_ls.pending(); \
_fgc_ls.commit()) \
_fgc_ls.stream()