A fast, simple, and lightweight header-only C++ logging library with console output and optional memory buffer support.
- Header-only: Single header file, no compilation required
- Fast & Lightweight: <1000 LOC, optimized for speed and simplicity
- Thread-safe: All operations are thread-safe
- UTF-8 Support: Full Unicode support with automatic Windows console configuration
- Log level filtering: Built-in filtering by severity level (TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF)
- Message suppliers: Zero-cost abstraction for expensive log message calculations - suppliers only invoked when log level allows
- Flexible formatting: Support for anonymous (
{?}) and positional ({0},{1}) parameters - Memory buffer: Optional in-memory log storage with configurable capacity
- Observer pattern: Extensible through custom log observers
- Console control: Enable/disable console output at runtime
- RAII support: Automatic resource management for observers and auto-flushing scopes
- Cross-platform: Works on Linux, Windows, and macOS
#include "ulog/ulog.h"
int main() {
// Get global logger
auto logger = ulog::getLogger();
logger.info("Hello, world!");
// Get named logger
auto appLogger = ulog::getLogger("MyApp");
appLogger.debug("Debug message: {0}", value);
return 0;
}auto logger = ulog::getLogger("App");
// Anonymous parameters
logger.info("User: {?}, Age: {?}", "John", 25);
// Output: 2025-06-12 10:30:15.123 [INFO] [App] User: John, Age: 25
// Positional parameters
logger.info("Name: {0}, Age: {1}, Name again: {0}", "Alice", 30);
// Output: 2025-06-12 10:30:15.124 [INFO] [App] Name: Alice, Age: 30, Name again: Aliceauto logger = ulog::getLogger("BufferedApp");
// Enable buffer with capacity of 100 messages (0 = unlimited)
logger.enable_buffer(100);
logger.info("This message is buffered");
logger.debug("This too");
// Access buffer contents
auto buffer = logger.buffer();
for (auto it = buffer->cbegin(); it != buffer->cend(); ++it) {
// it->message contains formatted message with parameters substituted: "This message is buffered"
// it->formatted_message() contains full log line: "2025-06-15 10:30:15.123 [INFO] [BufferedApp] This message is buffered"
std::cout << it->formatted_message() << std::endl;
}
logger.clear_buffer();
logger.disable_buffer();class CustomObserver : public ulog::LogObserver {
public:
void handleNewMessage(const ulog::LogEntry& entry) override {
// Custom handling logic
std::cout << "Observed: " << entry.message << std::endl;
}
};
auto logger = ulog::getLogger("ObservedApp");
auto observer = std::make_shared<CustomObserver>();
// Manual observer management
logger.add_observer(observer);
logger.info("This will be observed");
logger.remove_observer(observer);
// RAII observer management
{
ulog::ObserverScope scope(logger, observer);
logger.info("This will be observed");
} // Observer automatically removedUse RAII to automatically flush loggers when scopes exit:
auto logger = ulog::getLogger("FlushApp");
{
ulog::AutoFlushingScope scope(logger);
logger.info("Message 1");
logger.warn("Message 2");
// flush() will be called automatically when scope exits
}
// Nested scopes work too
{
ulog::AutoFlushingScope outerScope(logger);
{
ulog::AutoFlushingScope innerScope(logger);
logger.info("Inner message");
} // Inner flush happens here
logger.info("Outer message");
} // Outer flush happens hereauto logger = ulog::getLogger("ControlApp");
logger.info("This appears on console");
logger.disable_console();
logger.info("This does not appear on console");
logger.enable_console();
logger.info("This appears on console again");Control which messages are logged based on their severity level:
auto logger = ulog::getLogger("FilterApp");
// Default level is INFO (logs INFO, WARN, ERROR, FATAL)
logger.trace("This will NOT appear"); // Filtered out
logger.debug("This will NOT appear"); // Filtered out
logger.info("This will appear");
logger.warn("This will appear");
// Set to TRACE to log all messages
logger.set_log_level(ulog::LogLevel::TRACE);
logger.trace("Now this will appear");
logger.debug("And this will appear");
// Set to ERROR to log only ERROR and FATAL
logger.set_log_level(ulog::LogLevel::ERROR);
logger.info("This will NOT appear"); // Filtered out
logger.error("This will appear");
// Set to OFF to disable all logging
logger.set_log_level(ulog::LogLevel::OFF);
logger.fatal("Even this won't appear"); // All messages filtered
// Check current level
LogLevel current = logger.get_log_level();Available log levels (in order of severity):
OFF- No loggingTRACE- Most verboseDEBUG- Debug informationINFO- General information (default)WARN- WarningsERROR- ErrorsFATAL- Fatal errors
Message suppliers provide a zero-cost abstraction for expensive log message calculations. The supplier function is only invoked if the log level allows the message to be logged, providing significant performance benefits for debug/trace logging in production environments.
auto logger = ulog::getLogger("PerformanceApp");
// Traditional logging - always evaluates expensive operations even when logging is disabled
logger.set_log_level(ulog::LogLevel::WARN); // Disable debug
logger.debug("Result: {}", expensive_calculation()); // expensive_calculation() is ALWAYS called!
// Message supplier - only evaluates when log level allows
logger.debug_supplier([]() {
// This lambda is ONLY called when DEBUG level is enabled
return "Result: " + std::to_string(expensive_calculation());
});
// Suppliers work with all log levels
logger.trace_supplier([]() { return "Trace: " + complex_operation(); });
logger.info_supplier([]() { return "Info: " + another_operation(); });
logger.error_supplier([]() { return "Error: " + error_details(); });#include <chrono>
auto logger = ulog::getLogger("Benchmark");
logger.set_log_level(ulog::LogLevel::WARN); // Disable debug logging
// Traditional approach - slow even when logging disabled
auto start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; ++i) {
logger.debug("Fibonacci(20) = {}", fibonacci(20)); // Always calculates!
}
auto traditional_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start);
// Supplier approach - zero cost when logging disabled
start = std::chrono::high_resolution_clock::now();
for (int i = 0; i < 1000; ++i) {
logger.debug_supplier([]() {
return "Fibonacci(20) = " + std::to_string(fibonacci(20)); // Never called!
});
}
auto supplier_time = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - start);
// supplier_time will be ~0ms, traditional_time will be hundreds of milliseconds
std::cout << "Traditional: " << traditional_time.count() << "ms\n";
std::cout << "Supplier: " << supplier_time.count() << "ms\n";- Zero-cost when disabled: Supplier functions are never called when log level prevents logging
- Full parameter support: Suppliers can calculate and format complex parameters internally
- Lambda-friendly: Works seamlessly with C++ lambdas and capture lists
- Thread-safe: Inherits all thread safety guarantees from the logging system
- Easy migration: Existing logging calls can be gradually converted to suppliers
Simply copy include/ulog/ulog.h to your project and include it:
#include "ulog/ulog.h"Clone the repository and use CMake:
git clone https://github.com/vpiotr/ulog.gitThen, in your CMakeLists.txt:
# Add ulog as a subdirectory
add_subdirectory(path/to/ulog)
# Link against the ulog library
target_link_libraries(your_target PRIVATE ulog)ulog can be configured to use an external to_string implementation from a library like ustr.h. This is useful if you already have a to_string utility or want more control over string conversions, especially for custom types or advanced formatting of STL containers.
To enable this feature:
-
Define
ULOG_USE_USTR: Before includingulog/ulog.h, define the macroULOG_USE_USTR.#define ULOG_USE_USTR #include "ulog/ulog.h" #include "ustr/ustr.h" // Your ustr.h header
-
Provide
ustr.h: Ensure that a header file namedustr.his available in your include paths. This file should contain a namespaceustrwithto_stringtemplate functions and specializations.A minimal
ustr.hstub might look like this (seedemos/ustr/ustr.hfor a more complete example):// demos/ustr/ustr.h (example stub) #pragma once #include <string> #include <sstream> // For std::ostringstream // Add includes for types you want to support, e.g., <vector>, <map> namespace ustr { template <typename T> inline std::string to_string(const T& value) { std::ostringstream oss; oss << value; // Default implementation return oss.str(); } // Add specializations as needed // inline std::string to_string(const std::vector<int>& vec) { ... } } // namespace ustr
-
Link
ulog: WhenULOG_USE_USTRis defined,ulogwill call::ustr::to_string()instead of its internalulog::ustr::to_string().
Example Usage:
#define ULOG_USE_USTR
#include "ulog/ulog.h"
#include "path/to/your/ustr.h" // Make sure this path is correct
// Example with a C-style array (assuming ustr.h has an overload for it)
int main() {
auto logger = ulog::getLogger("UstrApp");
int c_array[] = {10, 20, 30};
logger.info("C-style array: {?}", c_array);
// If ustr::to_string supports arrays, it will be formatted accordingly.
std::vector<std::string> my_vector = {"hello", "ustr"};
logger.info("STL container (vector): {?}", my_vector);
// If ustr::to_string supports std::vector<std::string>, it will be used.
return 0;
}Refer to the demos/demo_ustr_integration.cpp and demos/ustr/ustr.h for a runnable example.
- C++17 compatible compiler (GCC 7+, Clang 5+, MSVC 2017+)
- CMake 3.16 or later
- Threads library (usually available by default)
The project includes convenient shell scripts for common operations:
# Rebuild entire project (tests and demos)
./rebuild.sh
# Run all tests
./run_tests.sh
# Run demo applications
./run_demos.sh
# Run performance benchmarks
./run_benchmarks.sh
# Generate Doxygen documentation (requires Doxygen)
./build_docs.shmkdir build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make
# Run tests (multiple test executables)
./test_logger
./test_buffer
./test_formatter
./test_observer
# Run demo
./ulog_demoMain logging class with the following methods:
Standard Logging Methods:
trace(),debug(),info(),warn(),error(),fatal()- Log messages at different levels
Message Supplier Methods (Zero-Cost Abstraction):
trace_supplier(),debug_supplier(),info_supplier(),warn_supplier(),error_supplier(),fatal_supplier()- Log using supplier functions that are only invoked when log level allows
Configuration Methods:
set_log_level(LogLevel level)- Set minimum log level filterget_log_level()- Get current log level filterenable_buffer(size_t capacity)- Enable memory buffer with optional capacity limitdisable_buffer()- Disable memory bufferclear_buffer()- Clear buffer contentsenable_console()/disable_console()- Control console outputflush()- Flush console output
Observer Management:
add_observer()/remove_observer()- Manage observersbuffer()- Get read-only access to buffer
Structure containing log information:
timestamp- When the log entry was createdlevel- Log level (TRACE, DEBUG, INFO, WARN, ERROR, FATAL)logger_name- Name of the logger that created this entrymessage- The formatted message with all parameters already substitutedformatted_message()- Get fully formatted log line with timestamp, level, logger name, and message
Key Difference:
messagecontains the formatted message with parameters substituted (e.g., forlogger.info("Hello user {0}", "tom123"), this would be "Hello user tom123")formatted_message()returns the complete formatted log line (e.g., "2025-06-15 10:30:15.123 [INFO] [MyApp] Hello user tom123")
Parameter Processing:
When you call logger.info("Hello user {0}", "tom123"), the parameters are processed as follows:
- Parameters are converted to strings using
to_string() - Placeholders (
{0},{1},{?}) are replaced with the converted parameter values - The resulting formatted message ("Hello user tom123") is stored in the
messagefield - Original format string and parameters are not preserved in the LogEntry
Abstract base class for log observers:
handleRegistered()- Called when observer is added to loggerhandleUnregistered()- Called when observer is removed from loggerhandleNewMessage()- Called for each new log messagehandleFlush()- Called when logger is flushed
RAII class for automatic observer management:
ulog::ObserverScope scope(logger, observer); // Adds observer
// ... observer automatically removed when scope endsRAII class for automatic logger flushing:
ulog::AutoFlushingScope scope(logger); // Will flush when scope ends
logger.info("This message will be flushed automatically");
// ... logger flushed when scope endsulog::getLogger()- Get global loggerulog::getLogger(name)- Get named loggerulog::getLogger(name, factory)- Get logger using factory functionulog::getLogger(factory)- Get global logger using factory function
ulog can log any type that supports stream output (operator<<). For custom classes, simply provide an operator<< overload:
class Person {
public:
Person(const std::string& name, int age) : name_(name), age_(age) {}
// Provide operator<< for ulog support
friend std::ostream& operator<<(std::ostream& os, const Person& person) {
os << "Person(name=" << person.name_ << ", age=" << person.age_ << ")";
return os;
}
private:
std::string name_;
int age_;
};
int main() {
auto logger = ulog::getLogger("CustomDemo");
Person person("Alice", 30);
logger.info("Created user: {?}", person);
// Output: 2025-06-12 10:30:15.123 [INFO] [CustomDemo] Created user: Person(name=Alice, age=30)
return 0;
}For comprehensive examples including advanced formatting, template specialization, container support, and performance tips, see demos/demo_custom_formatting.cpp.
All log messages follow this consistent format:
<timestamp> [<log-level>] [<logger-name>] <message>
Example:
2025-06-12 10:30:15.123 [INFO] [MyApp] Application started successfully
2025-06-12 10:30:15.124 [DEBUG] [Database] Connected to database: localhost:5432
2025-06-12 10:30:15.125 [WARN] [Cache] Cache miss for key: user_123
2025-06-12 10:30:15.126 [ERROR] [Network] Failed to connect to remote service
For global logger (empty name), the logger name part is omitted:
2025-06-12 10:30:15.127 [INFO] Global message without logger name
- Minimal overhead: Header-only design with inline optimizations
- Thread-safe: Uses efficient locking mechanisms
- Memory efficient: Optional buffering with configurable limits
- Format optimization: Fast parameter substitution
- Compile-time optimization: Template-based design for optimal performance
ulog provides compile-time control over mutex usage for fine-tuning performance vs. thread-safety:
ULOG_USE_MUTEX_FOR_CONSOLE- Controls console output mutex (default: 1/enabled)ULOG_USE_MUTEX_FOR_BUFFER- Controls buffer operations mutex (default: 1/enabled)ULOG_USE_MUTEX_FOR_OBSERVERS- Controls observer operations mutex (default: 1/enabled)
Define these macros before including ulog.h:
#define ULOG_USE_MUTEX_FOR_CONSOLE 0 // Disable console mutex
#define ULOG_USE_MUTEX_FOR_BUFFER 0 // Disable buffer mutex
#define ULOG_USE_MUTEX_FOR_OBSERVERS 0 // Disable observer mutex
#include "ulog/ulog.h"With Mutexes (Default - Thread-Safe):
- Thread-safe console output
- Thread-safe buffer operations
- Thread-safe observer notifications
- No data races or corruption
- Slight performance overhead in single-threaded scenarios
Without Mutexes (Performance Optimized):
- Maximum performance in single-threaded scenarios
- NOT thread-safe - only use in single-threaded applications
- Undefined behavior if used from multiple threads
Use the provided benchmark demos to compare performance:
Use the provided benchmark script to compare performance:
# Run all benchmarks
./run_benchmarks.sh
# Or build and run specific benchmarks manually
cd build
make demo_buffer_benchmark_with_mutex demo_buffer_benchmark_no_mutex
make demo_observer_benchmark_with_mutex demo_observer_benchmark_no_mutex
# Buffer performance benchmarks
./demo_buffer_benchmark_with_mutex # With mutex protection
./demo_buffer_benchmark_no_mutex # Without mutex protection
# Observer performance benchmarks
./demo_observer_benchmark_with_mutex # With observer mutex protection
./demo_observer_benchmark_no_mutex # Without observer mutex protectionNote: Observer registry management (add/remove observers) uses mutex protection based on the ULOG_USE_MUTEX_FOR_OBSERVERS setting.
See the demos/ directory for comprehensive examples:
demos/demo_main.cpp- Core functionality including basic logging, parameter formatting, memory buffer usage, observer pattern, console control, thread safety, and logger factory usagedemos/demo_file_observer.cpp- File output via observer pattern with RAII management and multiple observersdemos/demo_log_level_filtering.cpp- Log level filtering examples with buffers and observersdemos/demo_message_supplier.cpp- Message supplier examples demonstrating zero-cost abstraction for expensive log message calculationsdemos/demo_custom_formatting.cpp- Custom formatting for both primitive and user-defined types including wrapper classes, operator<< overloads, template specialization, container support, and performance tipsdemos/demo_container_formatting.cpp- Advanced container formatting examples with STL containers and custom typesdemos/demo_auto_flushing.cpp- RAII auto-flushing scope examples including basic usage, nested scopes, multiple loggers, and exception safetydemos/demo_debug_scope.cpp- DebugScope RAII pattern with observer integration for automatic scope entry/exit logging, nested scopes, multiple loggers, and exception safetydemos/demo_slow_op_guard.cpp- SlowOpGuard RAII pattern for monitoring slow operations with configurable thresholds, static and lambda message suppliers, nested operations, and real-world scenariosdemos/demo_cerr_observer.cpp- Error message redirection to stderr via observer pattern with multiple observer support, RAII management, and exception safetydemos/demo_exception_formatting.cpp- Automatic exception formatting with custom exception wrappers, nested exception handling, system error integration, and real-world scenariosdemos/demo_ustr_integration.cpp- Integration with external ustr.h library for enhanced string conversion capabilitiesdemos/demo_buffer_assertions.cpp- Buffer assertion utilities and debugging features for development environmentsdemos/demo_buffer_stats.cpp- Buffer statistics and monitoring capabilities for performance analysisdemos/demo_threaded_buffer_stats.cpp- Multi-threaded buffer statistics demonstration with concurrent loggingdemos/demo_windows_utf8.cpp- Windows UTF-8 support demonstration with Unicode character handling
benchmarks/demo_buffer_benchmark_with_mutex.cpp- Buffer write performance benchmark with mutex protection enabledbenchmarks/demo_buffer_benchmark_no_mutex.cpp- Buffer write performance benchmark with mutex protection disabled (single-threaded only)benchmarks/demo_observer_benchmark_with_mutex.cpp- Observer notification performance benchmark with mutex protection enabledbenchmarks/demo_observer_benchmark_no_mutex.cpp- Observer notification performance benchmark with mutex protection disabled (single-threaded only)
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- Follow SOLID and DRY principles
- Maintain the <1000 LOC limit (excluding comments)
- Ensure thread safety for all public APIs
- Add comprehensive tests for new features
- Update documentation for API changes
- Maintain cross-platform compatibility
ulog is designed with the following principles:
- Simplicity: Minimal API surface, easy to learn and use
- Performance: Zero-overhead abstractions where possible
- Flexibility: Extensible through observers and factories
- Reliability: Thread-safe, exception-safe, and tested
- Portability: Works across different platforms and compilers
This project is licensed under the MIT License - see the LICENSE file for details.
- Built with modern C++17 features
- Inspired by popular logging frameworks like spdlog and log4cpp
- Uses utest framework for comprehensive testing
- Thanks to all contributors and users
For detailed API documentation, run ./build_docs.sh to generate Doxygen documentation.
ulog provides automatic UTF-8 support on Windows with Visual Studio/MSVC:
- Automatic Console Setup: Windows console is automatically configured for UTF-8 output
- Source File Encoding: CMake configuration uses
/utf-8flag for proper source encoding - Unicode String Literals: All Unicode characters use
u8prefix for consistent encoding - Cross-Platform: Same code works identically on Windows, Linux, and macOS
Example with Unicode characters:
auto& logger = ulog::getLogger("Unicode");
logger.info(u8"Status: ✓ Success, Temperature: 23.5°C");
logger.info(u8"Internationalization: café, 你好, Ω α β");See WINDOWS_UTF8.md for detailed implementation information and demo_windows_utf8.cpp for a comprehensive test.