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

FNDTRACE(...)

Scoped tracer. Emits ENTERING: at the macro site and LEAVING: + elapsed time when the enclosing scope ends. See below.

FNDDEBUG(fmt, ...)

Single-line emission at DEBUG level if enabled.

FNDINFO(fmt, ...)

Single-line emission at INFO level if enabled.

FNDWARNING(fmt, ...)

Single-line emission at WARNING level if enabled.

FNDERROR(fmt, ...)

Single-line emission at ERROR level if enabled.

FNDTHROW(fmt, ...)

Emits an ERROR line and then throws a std::runtime_error with the formatted message. Cannot be short-circuited – the throw always happens.

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.hpp defines the FND* macros, which expand FNDLOC.

  • FNDLOC is defined in base.hpp.

  • The inline helpers in base.hpp (e.g. SleepSecs) themselves call FNDTRACE – so base.hpp must include logger.hpp after defining FNDLOC.

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 MakeStdoutLogger serialises writes (one std::cout << line << std::flush per Emit) so concurrent loggers don’t interleave half-lines.