FCF 2.0 development in progress...
> > > >
[News] [C++ Libraries API] [C++ Downloads] [Donate to the project] [Contacts]

Logging System: Monitoring and Debugging

In complex testing environments, simply knowing whether a test passed or failed is often not enough. You might need to track the internal state of your system, monitor long-running processes, or capture diagnostic information during a specific phase of execution. The fcfTest library provides a built-in logging system designed specifically for this purpose.

How the Logger Works: Internal Nuances

To use the logging system effectively, it is important to understand its internal architecture.

The Singleton Pattern

The logger is implemented as a global singleton. This means that when you call logger(), you are interacting with the exact same instance throughout your entire application. Any configuration change (like changing the log level) made in one part of the code will immediately affect all subsequent logging calls in all tests.

The Threshold Mechanism

The filtering is not based on "importance" in the traditional sense, but on a strict numerical threshold. Internally, each level is assigned a value. When a log call is made, the framework compares the message level to the current global threshold. If Message_Level < Threshold_Level, the message is discarded immediately, minimizing the performance overhead of string construction in production runs.

Output Targets

The logger does not simply print to std::cout. It directs messages to one or more Targets. A target is an abstraction of an output destination. By default, the framework provides a console target, but the architecture allows the logger to simultaneously send output to multiple streams, such as:

  • The standard terminal output.
  • A log file on disk.
  • A std::stringstream for capturing logs during automated testing of the testing framework itself.

This decoupling of what is logged from where it is sent is what makes fcfTest's logging system both flexible and powerful.


Basic Usage

Logging in fcfTest is designed to be as intuitive as using std::cout. You access the global logger through the log function, which returns a stream object.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> #include <iostream> FCF_TEST_DEFINE("Demo", "Logging", "Basic") { // Simple logging during test execution fcf::NTest::log() << "Starting the process..." << std::endl; int status = 42; fcf::NTest::log() << "Current status: " << status << std::endl; FCF_TEST(status == 42); } int main(int argc, char** argv) { bool error = false; fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }

Terminal output:

Performing the test: "Demo" -> "Logging" -> "Basic" ... > Starting the process... > Current status: 42 [SUCCESS] Test completed successfully (0.000`016`017 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`016`017 sec

Controlling Verbosity (Log Levels)

To prevent your console from being flooded with unnecessary information, fcfTest uses a threshold-based filtering system. Every log message is associated with a specific level. If the current global logging level is "higher" than the message's level, the message is ignored.

The following table lists all available levels, ordered from least verbose (highest priority/restriction) to most verbose:

Level (str) Level (int) Level (function) Description
"off" fcf::NTest::LL_OFF All logging is disabled.
"ftl" fcf::NTest::LL_FTL fcf::NTest::ftl() Fatal: Critical errors that prevent further execution.
"err" fcf::NTest::LL_ERR fcf::NTest::err() Error: Significant issues that caused a test failure.
"wrn" fcf::NTest::LL_WRN fcf::NTest::wrn() Warning: Potential issues that don't break the test.
"att" fcf::NTest::LL_ATT fcf::NTest::att() Attention: Important notices for the developer.
"att" fcf::NTest::LL_ATT fcf::NTest::att() Attention: Important notices for the developer.
"log" fcf::NTest::LL_LOG fcf::NTest::log() Log: Standard execution messages (Default).
"inf" fcf::NTest::LL_INF fcf::NTest::inf() Info: General information about the system state.
"dbg" fcf::NTest::LL_DBG fcf::NTest::dbg() Debug: Detailed information for troubleshooting.
"trc" fcf::NTest::LL_TRC fcf::NTest::trc() Trace: Extremely granular step-by-step execution details.
"all" fcf::NTest::LL_ALL All messages are shown.
"def" fcf::NTest::LL_DEF Uses the application's default level.

Configuration Options

You can change the logging threshold in two ways: via the command line (ideal for CI/CD and manual runs) or directly in your C++ code (ideal for hardcoding specific debug modes).

1. Command Line Interface

Use the --test-log-level parameter to set the threshold without recompiling:

# Run tests with only Error and Fatal messages $ ./my_tests --test-log-level err # Run tests with maximum verbosity for deep tracing $ ./my_tests --test-log-level trc
2. Programmatic Configuration

You can control the logger using the logger singleton. This is useful if you want to programmatically enable more verbosity when certain conditions are met.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") { fcf::NTest::inf() << "This is a trace message." << std::endl; } int main(int argc, char** argv) { // Set the logging threshold to INFO via code. // This means 'dbg' and 'trc' will be hidden. // And 'ftl', 'err', 'wrn', 'att', 'log', and 'inf' will be shown. // Note that calling fcf::NTest::cmdRun will override this value // if the --test-log-level parameter is specified. // Therefore, this method sets the default logging level. fcf::NTest::logger().level(fcf::NTest::ELogLevel::LL_INF); bool error = false; fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }

Launching the application normally will start it with the "info" logging level.

$ test_app

Terminal output:

Performing the test: "Demo" -> "Logging" -> "CodeConfig" ... > This is a trace message. [SUCCESS] Test completed successfully (0.000`009`807 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`009`807 sec

However, you can specify a particular logging level if needed.

$ test_app --test-log-level log

Terminal output:

Performing the test: "Demo" -> "Logging" -> "CodeConfig" ... [SUCCESS] Test completed successfully (0.000`003`085 sec) [SUCCESS] All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`003`085 sec

Formats

A Format determines the structural layout of the log message. The framework allows you to set a global default format or specify a unique format for each individual target stream.

By default, the framework supports two output formats:

  • default
  • junit
1. Command Line Interface

Set the global default format for all targets:

# Set the global default format to JUnit $ ./my_tests --test-format junit

Terminal output:

<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="1" failure="0" skipped="0" time="0.000010434"> <testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000010434"> <testcase classname="Demo/Logging" name="CodeConfig" time="0.000010434"/> </testsuite> </testsuites>
2. Programmatic Configuration

You can specify the default format for your application programmatically.

#define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") { FCF_TEST(true); } int main(int argc, char** argv) { // Set the logger output format via default options. // The fcf::NTest::cmdRun function will apply the output format upon execution. fcf::NTest::Options options; options.format = "junit"; bool error = false; fcf::NTest::cmdRun(options, argc, argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }

Let's run the application without specifying the format (default format):

$ ./my_tests

Terminal output:

<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="1" failure="0" skipped="0" time="0.000000804"> <testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000000804"> <testcase classname="Demo/Logging" name="CodeConfig" time="0.000000804"/> </testsuite> </testsuites>

The logger class can simultaneously output logs to different stream targets, and you can specify a separate output format for each stream. We will cover this approach to setting the format in the next section.


Output Targets

The logger does not simply print to std::cout. It directs messages to one or more Targets. A target is an abstraction of an output destination. This decoupling of what is logged from where it is sent is what makes fcfTest's logging system both flexible and powerful.

1. Command Line Interface

You can define multiple output targets and assign specific formats to them using the command line:

# Log to a file using the default format $ ./my_tests --test-file output.log # Log to a file using a specific pre-defined format (e.g., JUnit) $ ./my_tests --test-file-junit report.xml
2. Programmatic Configuration

You can dynamically add new targets to the logger during runtime. This is useful for redirecting output to files or memory streams.

#include <fstream> #define FCF_TEST_IMPLEMENTATION #include <fcfTest/test.hpp> FCF_TEST_DEFINE("Demo", "Logging", "CodeConfig") { FCF_TEST(true); } int main(int argc, char** argv) { // 1. Add an output target directed to a file. // Use the default format, which can be set via command-line arguments. std::ofstream log_file("debug.log"); fcf::NTest::Logger::OutputTarget fileOutputTarget; fileOutputTarget.name = "file_target"; // The target name is defined by the user. fileOutputTarget.stream = &log_file; // Log output stream fileOutputTarget.format = ""; // The name of the format in which the data will be output. // An empty string indicates the format currently set for the logger. // Add a new target that writes to the file stream using the default format fcf::NTest::logger().appendTarget(fileOutputTarget); // 2. Replace the default output target // so that it outputs the log in JUnit format fcf::NTest::Logger::OutputTarget coutOutputTarget; coutOutputTarget.name = "default"; // The name "default" is already reserved, so this will update the existing target. coutOutputTarget.stream = &std::cout; // Log output stream coutOutputTarget.format = "junit"; // Set the output format // Replace the default target that is directed to the terminal output stream. fcf::NTest::logger().appendTarget(coutOutputTarget); bool error = false; fcf::NTest::cmdRun(argc, argv, fcf::NTest::CRM_RUN, &error); return error ? 1 : 0; }

Terminal output:

<?xml version="1.0" encoding="UTF-8"?> <testsuites tests="1" failure="0" skipped="0" time="0.000000482"> <testsuite name="Demo/Logging" tests="1" failure="0" skipped="0" time="0.000000482"> <testcase classname="Demo/Logging" name="CodeConfig" time="0.000000482"/> </testsuite> </testsuites>

debug.log file:

Performing the test: "Demo" -> "Logging" -> "CodeConfig" ... ^[[1;32m[SUCCESS]^[[0m Test completed successfully (0.000`000`555 sec) ^[[1;32m[SUCCESS]^[[0m All tests were completed. Tests: 1 passed, 0 failed, 0 skipped, 1 total Duration: 0.000`000`555 sec