Logger and FND* macros
ifw-fnd provides a small, pluggable logging abstraction. Call sites use
a fixed set of macros – FNDTRACE, FNDDEBUG, FNDINFO,
FNDWARNING, FNDERROR, FNDTHROW – and never name a concrete
backend. The backend is selected once at program startup by installing
a Logger subclass.
Quick usage
#include <ifw/fnd/defs/base.hpp>
int main() {
ifw::fnd::InstallLogger(ifw::fnd::MakeStdoutLogger());
FNDINFO("starting on port {}", port);
try {
// ...
} catch (const std::exception& e) {
FNDERROR("startup failed: {}", e.what());
}
}
The FND* macros at a glance
Macro |
Behaviour |
|---|---|
|
Scoped tracer. Emits |
|
Single-line emission at |
|
Single-line emission at |
|
Single-line emission at |
|
Single-line emission at |
|
Emits an |
Every macro takes an fmt-style format string and arguments. The format
is processed by {fmt}; the same syntax that std::format /
fmt::format accept.
Each non-throw macro performs a single integer-comparison level check
before formatting the message, so disabled-level calls are essentially
free. Leaving instrumentation at DEBUG level in the code is
encouraged – future troubleshooting wants those breadcrumbs.
Scoped trace: FNDTRACE
FNDTRACE() at the top of a function logs an ENTERING: line at
the macro site and a LEAVING: line + elapsed time when the
enclosing scope ends (RAII). Nesting depth is tracked per thread; one
= of indentation is added per level so a multi-level call chain
renders with visual hierarchy.
void Client::Connect() {
FNDTRACE();
OpenSocket();
HandshakeSession();
}
Sample output (level set to TRACE):
TRACE ENTERING: Client::Connect()
TRACE =ENTERING: Client::OpenSocket()
TRACE =LEAVING: Client::OpenSocket() Time: 0.012s
TRACE =ENTERING: Client::HandshakeSession()
TRACE =LEAVING: Client::HandshakeSession() Time: 0.031s
TRACE LEAVING: Client::Connect() Time: 0.046s
FNDTRACE also accepts a format string. The extra text appears on
both the ENTERING: and LEAVING: lines so they can be correlated
in interleaved multi-thread output:
void Client::Read(const ReadRequest& r) {
FNDTRACE("items={}", r.items.size());
// ...
}
renders as
TRACE ENTERING: Client::Read(...): items=3
TRACE LEAVING: Client::Read(...): items=3 Time: 0.004s
If the function exits via an exception, the LEAVING: line still
fires (destructor runs during unwind). The destructor is
exception-safe; logging that throws is swallowed.
Placement: FNDTRACE(...) expands to a local RAII variable
declaration. It belongs at function (or scope) statement level – not
inside a conditional expression. Multiple FNDTRACE in nested scopes
is fine.
The Logger interface
ifw::fnd::Logger is a pure-virtual sink that the FND* macros
emit into. It exposes:
namespace ifw::fnd {
enum class LogLevel { TRACE, DEBUG, INFO, WARNING, ERROR, OFF };
class Logger {
public:
virtual ~Logger() = default;
virtual void Emit(LogLevel, const std::string& message) = 0;
void SetLogLevel(LogLevel) noexcept;
LogLevel GetLogLevel() const noexcept;
// Convenience: Trace()/Debug()/Info()/Warning()/Error()/Throw()
// -- all funnel through Emit().
// ...
};
void InstallLogger(std::unique_ptr<Logger>) noexcept;
Logger& Log();
bool HasLogger() noexcept;
std::unique_ptr<Logger> MakeStdoutLogger();
std::unique_ptr<Logger> MakeNullLogger();
std::string_view LogLevelName(LogLevel) noexcept;
}
Two factories ship in defs.cpp:
MakeStdoutLogger()– prints to stdout in the CII canonical console layout (YYYY-MM-DDTHH:MM:SS.mmm+0000, LEVEL, ifwfnd/<tid>, <message>). Good for standalone tools, command-line apps, development.MakeNullLogger()– discards every message. Useful for unit tests that don’t want ifw-fnd’s chatter on stdout.
Downstream applications that want log4cplus, CII, or syslog routing
implement their own Logger subclass and install it.
Calling Log() before InstallLogger() throws – ifw-fnd refuses
to silently swallow log output. Every executable that uses the macros
must install a logger at startup.
FNDLOC (and the temporary IFWLOC alias)
FNDLOC is a macro that expands to a source-location identifier of
the form "<iso-time>:<file>:<line>:<function>:<thread>". The
FND* macros prepend it to every message so a reader can locate
where a log line came from.
For backwards compatibility, IFWLOC is currently defined as
#define IFWLOC FNDLOC. IFWLOC is the legacy name, kept as a
temporary alias while downstream projects migrate. New code should
write FNDLOC. The alias will be removed once every consumer has
migrated; until then, both names work identically.
Why <base.hpp> and not <logger.hpp>
Consumers include <ifw/fnd/defs/base.hpp>, not <logger.hpp>
directly. The reason is an ordering constraint:
logger.hppdefines theFND*macros, which expandFNDLOC.FNDLOCis defined inbase.hpp.The inline helpers in
base.hpp(e.g.SleepSecs) themselves callFNDTRACE– sobase.hppmust includelogger.hppafter definingFNDLOC.
The chain that makes this work is:
user.cpp
-> base.hpp
-> declares forward decls + #define FNDLOC ...
-> includes logger.hpp (FNDLOC now visible to FND* macros)
-> inline function bodies that use FNDTRACE (macros now defined)
If logger.hpp were included directly without going through
base.hpp, FNDLOC would not be defined and the FND* macros
would fail to compile. Always include base.hpp.
Threading
Log()is lock-free after install. The hot path is one atomic pointer load.InstallLogger()is not safe to call concurrently with other threads logging. Install once at startup, before any worker thread emits a message.FNDTRACE’s nesting depth is thread-local; each thread starts at depth zero.The default
MakeStdoutLoggerserialises writes (onestd::cout << line << std::flushperEmit) so concurrent loggers don’t interleave half-lines.