A step-by-step guide to extending CppTrail with your own sinks and formats.
CppTrail is designed to be extensible. While the library provides BasicOstreamLogger, most professional use cases require custom formatting (like JSON) or custom sinks (like a remote database or a specialized hardware interface).
This tutorial explains how to implement a custom logger by inheriting from the internal implementation classes.
⚡ 1. Creating a Synchronous Logger
Synchronous loggers are the simplest extension point. They perform the "work" (formatting and writing) on the same thread that calls the log() method. This is ideal for low-latency local logging where the overhead of context switching to a background thread is not desired.
Step 1: Define the Handle (Factory)
The handle class is what the user interacts with. It acts as a lightweight factory that manages the lifecycle of the internal implementation via a std::shared_ptr. This prevents object slicing and allows the logger to be copied safely across scopes.
#include <iostream>
template<typename char_t>
class BasicOstreamLogger : public BasicLogger<char_t> {
public:
BasicOstreamLogger(
std::basic_ostream<char_t> &oOstream
) : BasicLogger<char_t>(
std::make_shared<Impl>(
std::reference_wrapper{oOstream}
)
) {
}
High-level wrapper for the CppTrail logging system.
Step 2: Implement the Logic
The Impl class inherits from BasicSyncLoggerImpl. This base class provides built-in mutex protection, ensuring that if multiple threads use the same logger handle, their messages won't interleave and "jumble" in the output.
private:
class Impl : public BasicSyncLoggerImpl<char_t> {
public:
using char_type = typename BasicLoggerImpl<char_t>::char_type;
using string_type = typename BasicLoggerImpl<char_t>::string_type;
using message_type = typename BasicLoggerImpl<char_t>::message_type;
public:
Impl(
std::reference_wrapper<std::basic_ostream<char_t> > oOstream
) : m_oOstream(oOstream) {
}
~Impl() override {
this->stop();
}
protected:
void work(message_type oMessage) override{
std::basic_ostream<char_t> &oOstream = m_oOstream.get();
oOstream.put(static_cast<char_t>(' '));
oOstream << oMessage.stealLevel();
oOstream.put(static_cast<char_t>(' '));
oOstream << oMessage.stealString();
oOstream.put(static_cast<char_t>('\n'));
oOstream.flush();
}
Status serviceStatus()
override {
return Status::TRASCENDENT;
}
void serviceStart() override {
}
void serviceStop() override {
}
private:
std::reference_wrapper<std::basic_ostream<char_t> > m_oOstream;
};
};
Status
Represents the current operational state of a Logger implementation.
Definition def.h:31
🔄 2. Creating an Asynchronous Logger
For high-performance "wire stuff," you cannot afford to block the main thread while waiting for I/O (like a slow terminal or disk). Asynchronous loggers use an internal producer-consumer queue and a background worker thread.
The Asynchronous Worker Model
When you extend BasicAsyncLoggerImpl, your work() method is executed by a background thread. This allows your application to "fire and forget" log messages into a high-speed buffer.
Implementation Details
Unlike synchronous loggers, you must ensure the worker thread is properly joined during destruction to prevent data loss or memory access violations.
#include <iostream>
template<typename char_t>
class BasicAsyncOstreamLogger : public BasicLogger<char_t> {
public:
BasicAsyncOstreamLogger(
std::basic_ostream<char_t> &oOstream
) : BasicLogger<char_t>(
std::make_shared<Impl>(
std::reference_wrapper{oOstream}
)
) {
}
BasicAsyncOstreamLogger(
std::basic_ostream<char_t> &oOstream,
std::size_t nMaxItemCount,
bool bThrowOnOverflow,
std::size_t nWorkerCount
) : BasicLogger<char_t>(
std::make_shared<Impl>(
std::reference_wrapper{oOstream},
nMaxItemCount,
bThrowOnOverflow,
nWorkerCount
)
) {
}
private:
class Impl : public BasicAsyncLoggerImpl<char_t> {
public:
using char_type = typename BasicLoggerImpl<char_t>::char_type;
using string_type = typename BasicLoggerImpl<char_t>::string_type;
using message_type = typename BasicLoggerImpl<char_t>::message_type;
public:
Impl(
std::reference_wrapper<std::basic_ostream<char_t> > oOstream
) : m_oOstream(oOstream) {
}
Impl(
std::reference_wrapper<std::basic_ostream<char_t> > oOstream,
std::size_t nMaxItemCount,
bool bThrowOnOverflow,
std::size_t nWorkerCount
) : BasicAsyncLoggerImpl<char_t>(
nMaxItemCount,
bThrowOnOverflow,
nWorkerCount
), m_oOstream(oOstream){
}
~Impl() override {
this->stop();
this->join();
}
protected:
void work(message_type oMessage) override{
std::basic_ostream<char_t> &oOstream = m_oOstream.get();
oOstream.put(static_cast<char_t>(' '));
oOstream << oMessage.stealLevel();
oOstream.put(static_cast<char_t>(' '));
oOstream << oMessage.stealString();
oOstream.put(static_cast<char_t>('\n'));
oOstream.flush();
}
Status serviceStatus()
override {
return Status::TRASCENDENT;
}
void serviceStart() override {
}
void serviceStop() override {
}
private:
std::reference_wrapper<std::basic_ostream<char_t> > m_oOstream;
};
};