Neko Logging (nlog) is an easy-to-use, modern, lightweight, and efficient C++20 logging library.
- No Macro
- Header Only (No build/link required)
- Auto SrcLoc (Automatically capture source code location)
- Supports multiple appenders (Console, File, Custom)
- Supports custom formatted log messages
- Supports asynchronous logging
- Thread-safe
- RAII-style scope logging
- C++20 or higher compatible compiler
- CMake 3.14 or higher (if using CMake)
Configure: CMake | Manual | Tests
Example: Basic | Logging | Level | Set Thread Name | RAII Scope Logging
Advanced: Appenders | Formatting Logs | Asynchronous Logging | Disable Tests
- Using CMake's
FetchContent
to include NekoLog in your project:
include(FetchContent)
# Add NekoLog to your CMake project
FetchContent_Declare(
NekoLog
GIT_REPOSITORY https://github.com/moehoshio/nlog.git
GIT_TAG main
)
FetchContent_MakeAvailable(NekoLog)
# Add your target and link NekoLog
add_executable(your_target main.cpp)
target_link_libraries(your_target PRIVATE Neko::Log)
- Include the header in your source code
#include <neko/log/nlog.hpp>
When installing manually, you need to manually fetch the dependency NekoSchema
.
After installing the dependency, please continue:
- Clone or download the repository to your host
git clone https://github.com/moehoshio/nlog.git
or
curl -L -o nlog.zip https://github.com/moehoshio/nlog/archive/refs/heads/main.zip
unzip nlog.zip
- Copy the
include
folder to your include directory
cp -r nlog/include/ /path/to/your/include/
- Include the header in your source code
#include <neko/log/nlog.hpp>
Now you can start logging with minimal setup:
#include "neko/log/nlog.hpp"
int main() {
using namespace neko;
log::setCurrentThreadName("Main Thread"); // Set the current thread name
log::setLevel(log::Level::Debug); // Set the log level
log::info("This is an info message.");
log::debug("This is a debug message.");
log::warn("This is a warning message.");
log::error("This is an error message.");
}
output:
[2025-09-16 01:53:58.678] [Info] [Main Thread] [main.cpp:9] This is an info message.
[2025-09-16 01:53:58.679] [Debug] [Main Thread] [main.cpp:10] This is a debug message.
[2025-09-16 01:53:58.679] [Warn] [Main Thread] [main.cpp:11] This is a warning message.
[2025-09-16 01:53:58.679] [Error] [Main Thread] [main.cpp:12] This is an error message.
Logging is simple, just like in the example above.
Use the neko::log::info
, neko::log::debug
, neko::log::warn
, and neko::log::error
functions to log.
Each of these functions has two versions.
Single string:
inline void debug(const std::string &message, const neko::SrcLocInfo &location = {});
debug("msg"); // (basic format)... msg
And with format arguments (via std::format):
template <typename... Args>
void debug(const neko::SrcLocInfo &location, std::format_string<Args...> fmt, Args &&...args);
debug( {} , "Hello , {} . 1 + 1 = {}", "World" , 1 + 1); // (basic format)... Hello , World . 1 + 1 = 2
Functions for other levels are the same.
Tip: SrcLoc
can automatically get the source code location. You just need a default object, which you can generate via {}
or a default parameter.
You can set the log level for the logger, which controls which level of logs should be recorded.
For example, if you set setLevel
to Info
, only logs of Info
level and above will be recorded (Debug messages will be discarded).
log::setLevel(log::Level::Info);
log::info("Info"); // Outputs Info
log::warn("Warn"); // Outputs Warn
log::debug("Debug"); // More detailed messages will be discarded
If needed, you can add more log levels and log with the log
function.
For example:
// nlog.hpp
enum class Level : neko::uint8 {
Debug = 1, ///< Debug
Info = 2, ///< General information
Warn = 3, ///< Potential issues
Error = 4, ///< Error
lv5 = 5, /// Custom level
lv6 = 6,
lv10 = 10,
Off = 255 ///< Logging off
};
// main.cpp
using namespace neko;
log::logger.log(log::Level::lv10,"Hello Lv10");
You can set the names of different threads in the logs using neko::log::setCurrentThreadName
and neko::log::setThreadName
.
Example:
using namespace neko;
// Set the current thread
log::setCurrentThreadName("Thread 1");
log::info(""); // ... [Thread 1] ...
// Specify by id
auto id = std::this_thread::get_id();
log::setThreadName(id, "Thread-1");
log::info(""); // ... [Thread-1] ...
You can add multiple appenders simultaneously to output logs to different places. By default, appenders for console output and file writing are provided.
// Add a file appender and overwrite the file
log::addFileAppender("app.log", true);
log::addConsoleAppender(); // Add a console appender
You can easily add your own appender to output to any destination.
Just inherit from neko::log::IAppender
and override the append
and flush
methods for your output.
Example:
using namespace neko;
class MyAppender : public log::IAppender {
public:
void append(const log::LogRecord &record) override {
std::unique_ptr<log::IFormatter> formatter = std::make_unique<log::DefaultFormatter>(); // Create a default log formatter
auto formatted = formatter->format(record); // Format the log
// Output the string to your target
yourOutput << formatted;
}
void flush() override {
yourOutput.flush();
}
};
// Add a custom log appender
log::addAppender(std::make_unique<MyAppender>());
A formatter is a helper for an appender, used to format logs. It is independent for each appender, and the appender calls the formatter's method to format the log. So, it is recommended to have a built-in formatter object when creating a custom appender.
The default formatter's format is: [date time] [level] [thread] [file:line] [msg]
When constructing it, you can specify a root path to truncate and whether to use the full path. The function is defined as follows:
explicit DefaultFormatter(const std::string &rootPath = "", bool useFullPath = false)
When rootPath
is an empty string (default), file
= main.cpp
.
When rootPath
is a path, e.g., /to/path/
, and the file is at /to/path/src/main.cpp
, file
= /src/main.cpp
.
When useFullPath
is true
, rootPath
is ignored, and the full path is always displayed. file
= /to/path/src/main.cpp
.
Inherit from neko::log::IFormatter
and override the format
function.
You need to implement the formatting of the incoming record in the format
function.
Example:
using namespace neko;
class MyFormatter : public log::IFormatter {
public:
std::string format(const log::LogRecord &record) override {
// You can use any method to combine the record format, std::format, ostringstream, etc.
std::ostringstream oss;
oss << "lv: " << log::levelToString(record.level) << " , msg: "<< record.message ;
return oss.str();
}
};
// Create a console appender and specify MyFormatter as the formatter
log::ConsoleAppender consoleAppender(std::make_unique<MyFormatter>());
log::info("Hello");
output:
lv: Info , msg: Hello
By default, logging is written to IO by the logging thread.
For better performance, you can enable asynchronous mode. This lets logs be processed in a background thread.
#include "neko/log/nlog.hpp"
#include <thread>
using namespace neko;
int main() {
// Set to asynchronous mode
log::setMode(neko::SyncMode::Async);
// Start the log processing loop (usually in a dedicated thread)
std::thread logThread([]{ log::runLogLoop(); });
// Main thread submits logs
log::info("This will be logged asynchronously.");
// ... application execution ...
// Stop the log loop and flush remaining logs
log::stopLogLoop();
logThread.join();
}
In async mode, logs are not flushed in real-time. It's possible for a log to be submitted before a crash, but not be recorded in time. It is recommended to disable this during debugging.
Tip: When using asynchronous mode, a thread must be running the neko::log::runLogLoop()
function.
Otherwise, no logs will be processed.
Use neko::log::autoLog
to automatically log the start and end of a scope.
void someFunction() {
// Logs "Start" when someFunction is entered, and "End" when it is exited
log::autoLog log("Start", "End");
// ... function body ...
}
You can run the tests to verify that everything is working correctly.
If you haven't configured the build yet, please run:
cmake -B ./build . -DNEKO_BUILD_TESTS=ON -DNEKO_AUTO_FETCH_DEPS=ON
Now, you can build the test files (you must build them manually at least once before running the tests!).
cmake --build ./build --config Debug
Then, you can run the tests with the following commands:
Unix Makefile / Ninja generator:
cmake --build ./build --target test
Visual Studio generator:
cmake --build ./build --config Debug --target RUN_TESTS
If everything is set up correctly, you should see output similar to the following:
Test project /path/to/nlog/build
Start 1: NLogTest.FileLogging
1/36 Test #1: NLogTest.FileLogging ................
................. Passed 0.02 sec
# ......
Start 36: PerformanceTest.ConstexprMapLookupSpeed
36/36 Test #36: PerformanceTest.ConstexprMapLookupSpeed .............. Passed 0.02 sec
100% tests passed, 0 tests failed out of 36
Total Test time (real) = 0.78 sec
If you want to disable building and running tests, you can set the following CMake option when configuring your project:
cmake -B ./build . -DNEKO_BUILD_TESTS=OFF
This will skip test targets during the build process.
License MIT OR Apache-2.0
- NekoLog: An easy-to-use, modern, lightweight, and efficient C++20 logging library.
- NekoEvent: A modern easy to use type-safe and high-performance event handling system for C++.
- NekoSchema: A lightweight, header-only C++20 schema library.
- NekoSystem: A modern C++20 cross-platform system utility library.
- NekoFunction: A comprehensive modern C++ utility library that provides practical functions for common programming tasks.
- NekoThreadPool: An easy to use and efficient C++ 20 thread pool that supports priorities and submission to specific threads.