154 lines
7.0 KiB
C++
154 lines
7.0 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);
|
|
|
|
// --- tap ----------------------------------------------------------------
|
|
//
|
|
// A THIRD independent slot: sees every line that reaches the console (the
|
|
// same lines, the same filter), whether the console is the dashboard's pane
|
|
// or stdout. It exists so a command that arrived over MQTT can be answered
|
|
// with what it logged, without taking over either sink above.
|
|
//
|
|
// The same WARNING as the file sink applies: it runs under this logger's
|
|
// non-recursive mutex, so it must only record - never log, never block,
|
|
// never publish. Pass an empty function to remove it.
|
|
using TapSink = std::function<void(LogLevel level, const std::string& line)>;
|
|
static void setTap(TapSink tap);
|
|
|
|
// 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()
|