autotoc_md483
doc_id: "LOG-GUID-013" doc_title: "Creating Custom Writers" doc_version: "1.0.0" doc_date: "2026-04-04" doc_status: "Released" project: "logger_system"
category: "GUID"
Language: English | 한국어
Creating Custom Writers
SSOT: This document is the single source of truth for Creating Custom Writers.
This guide explains how to create custom writers for the Logger System to send logs to various destinations.
Overview
Writers are responsible for the actual output of log messages. The Logger System provides a flexible writer hierarchy that you can extend to create custom output destinations.
Writer Hierarchy
log_writer_interface (interface)
│
├── base_writer (abstract)
│ ├── thread_safe_writer [sync_writer_tag]
│ │ ├── console_writer [sync_writer_tag]
│ │ ├── file_writer [sync_writer_tag]
│ │ │ └── rotating_file_writer
│ │ └── your_custom_writer (Thread-safety handled automatically!)
│ │
│ ├── async_writer [async_writer_tag, decorator_writer_tag]
│ ├── batch_writer [async_writer_tag, decorator_writer_tag]
│ ├── critical_writer [decorator_writer_tag]
│ ├── encrypted_writer [decorator_writer_tag]
│ ├── network_writer [async_writer_tag]
│ └── hybrid_writer [composite_writer_tag, decorator_writer_tag]
│
└── composite_writer [composite_writer_tag]
(Pipeline Pattern: formatter + sink)
Note: Writers are categorized using type tags (v1.4.0+). See Writer Hierarchy for details on categories and Writer Selection Guide for choosing the right writer.
Recommended: Using thread_safe_writer (Since v1.3.0)
For most custom writers, inherit from thread_safe_writer instead of base_writer. This provides:
- Automatic thread-safety: Mutex handling is done by the base class
- Consistent locking: All writers use the same synchronization strategy
- Less boilerplate: No need to manage locks manually
- Template Method pattern: Implement
*_impl() methods, get thread-safety for free
public:
my_custom_writer() : thread_safe_writer() {}
std::string
get_name()
const override {
return "my_custom"; }
protected:
logger_system::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override
{
}
std::cout.flush();
}
};
std::string format_log_entry(const log_entry &entry) const
Format a log entry using the current formatter.
virtual std::string get_name() const override=0
Base class providing automatic thread-safety for writer implementations.
virtual common::VoidResult flush_impl()=0
Implementation of flush operation (override in derived classes)
Thread-safe base class for writer implementations kcenon.
Benefits of thread_safe_writer
- No mutex boilerplate: Derived classes focus only on output logic
- Cannot accidentally forget locking: Public methods are
final
- Deadlock prevention: Clear contract about when mutex is held
- RAII guarantee: Mutex is always released, even on exceptions
Base Writer Interface
For advanced use cases (async, batching, custom synchronization), inherit from base_writer:
class base_writer {
public:
virtual ~base_writer() = default;
logger_system::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) = 0;
virtual void set_use_color(bool use_color);
bool use_color() const;
protected:
std::string format_log_entry(const log_entry& entry) const;
};
Simple Examples
1. Simple File Writer (Using thread_safe_writer)
A basic file writer using the recommended thread_safe_writer base class:
#include <fstream>
private:
std::ofstream file_;
std::string filename_;
public:
explicit simple_file_writer(const std::string& filename)
: filename_(filename) {
file_.open(filename_, std::ios::app);
if (!file_.is_open()) {
throw std::runtime_error("Failed to open log file: " + filename);
}
}
~simple_file_writer() override {
if (file_.is_open()) {
file_.close();
}
}
std::string
get_name()
const override {
return "simple_file"; }
protected:
logger_system::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override
{
log_entry entry = log_entry(level, message, file, line, function, timestamp);
file_ << formatted << '\n';
if (!file_.good()) {
logger_error_code::file_write_failed,
"Failed to write to file: " + filename_);
}
}
file_.flush();
}
};
logger->add_writer(std::make_unique<simple_file_writer>(
"application.log"));
common::VoidResult make_logger_void_result(logger_error_code code, const std::string &message="")
Note: Compare this with the built-in file_writer class which provides additional features like buffering and byte counting.
2. Rotating File Writer
A more advanced file writer with size-based rotation:
class rotating_file_writer : public logger_module::base_writer {
private:
std::ofstream file_;
std::mutex mutex_;
std::string base_filename_;
size_t max_size_;
size_t current_size_;
int file_index_;
void rotate() {
file_.close();
std::string old_name = base_filename_ + "." + std::to_string(file_index_);
std::rename(base_filename_.c_str(), old_name.c_str());
file_.open(base_filename_);
current_size_ = 0;
file_index_++;
}
public:
rotating_file_writer(const std::string& filename, size_t max_size)
: base_filename_(filename)
, max_size_(max_size)
, current_size_(0)
, file_index_(0) {
file_.open(filename, std::ios::app);
file_.seekp(0, std::ios::end);
current_size_ = file_.tellp();
}
void write(thread_module::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override {
std::lock_guard<std::mutex> lock(mutex_);
std::string formatted = format_log_entry(level, message, file,
line, function, timestamp);
if (current_size_ + formatted.size() > max_size_) {
rotate();
}
file_ << formatted << std::endl;
current_size_ += formatted.size() + 1;
}
void flush() override {
std::lock_guard<std::mutex> lock(mutex_);
file_.flush();
}
};
logger->add_writer(std::make_unique<rotating_file_writer>(
"app.log", 10 * 1024 * 1024));
3. Network Writer
Send logs to a remote server:
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
class network_writer : public logger_module::base_writer {
private:
int socket_fd_;
struct sockaddr_in server_addr_;
std::mutex mutex_;
bool connected_;
void connect() {
socket_fd_ = socket(AF_INET, SOCK_STREAM, 0);
if (socket_fd_ < 0) {
throw std::runtime_error("Failed to create socket");
}
if (::connect(socket_fd_, (struct sockaddr*)&server_addr_,
sizeof(server_addr_)) < 0) {
close(socket_fd_);
throw std::runtime_error("Failed to connect to log server");
}
connected_ = true;
}
public:
network_writer(const std::string& host, int port) : connected_(false) {
memset(&server_addr_, 0, sizeof(server_addr_));
server_addr_.sin_family = AF_INET;
server_addr_.sin_port = htons(port);
if (inet_pton(AF_INET, host.c_str(), &server_addr_.sin_addr) <= 0) {
throw std::runtime_error("Invalid address: " + host);
}
connect();
}
~network_writer() override {
if (connected_) {
close(socket_fd_);
}
}
void write(thread_module::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override {
std::lock_guard<std::mutex> lock(mutex_);
if (!connected_) {
return;
}
std::string formatted = format_log_entry(level, message, file,
line, function, timestamp);
formatted += "\n";
ssize_t sent = send(socket_fd_, formatted.c_str(), formatted.size(), 0);
if (sent < 0) {
connected_ = false;
}
}
void flush() override {
}
};
logger->add_writer(std::make_unique<network_writer>(
"192.168.1.100", 5514));
4. Database Writer
Log to a database (using SQLite as example):
#include <sqlite3.h>
class database_writer : public logger_module::base_writer {
private:
sqlite3* db_;
sqlite3_stmt* insert_stmt_;
std::mutex mutex_;
public:
explicit database_writer(const std::string& db_path) {
if (sqlite3_open(db_path.c_str(), &db_) != SQLITE_OK) {
throw std::runtime_error("Failed to open database");
}
const char* create_table = R"(
CREATE TABLE IF NOT EXISTS logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
level TEXT NOT NULL,
message TEXT NOT NULL,
file TEXT,
line INTEGER,
function TEXT
)
)";
char* err_msg = nullptr;
if (sqlite3_exec(db_, create_table, nullptr, nullptr, &err_msg) != SQLITE_OK) {
std::string error = err_msg;
sqlite3_free(err_msg);
sqlite3_close(db_);
throw std::runtime_error("Failed to create table: " + error);
}
const char* insert_sql = R"(
INSERT INTO logs (timestamp, level, message, file, line, function)
VALUES (?, ?, ?, ?, ?, ?)
)";
if (sqlite3_prepare_v2(db_, insert_sql, -1, &insert_stmt_, nullptr) != SQLITE_OK) {
sqlite3_close(db_);
throw std::runtime_error("Failed to prepare statement");
}
}
~database_writer() override {
if (insert_stmt_) {
sqlite3_finalize(insert_stmt_);
}
if (db_) {
sqlite3_close(db_);
}
}
void write(thread_module::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override {
std::lock_guard<std::mutex> lock(mutex_);
sqlite3_reset(insert_stmt_);
auto time_t = std::chrono::system_clock::to_time_t(timestamp);
char time_buf[64];
std::strftime(time_buf, sizeof(time_buf), "%Y-%m-%d %H:%M:%S",
std::localtime(&time_t));
sqlite3_bind_text(insert_stmt_, 1, time_buf, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insert_stmt_, 2, level_to_string(level).c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insert_stmt_, 3, message.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(insert_stmt_, 4, file.c_str(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int(insert_stmt_, 5, line);
sqlite3_bind_text(insert_stmt_, 6, function.c_str(), -1, SQLITE_TRANSIENT);
if (sqlite3_step(insert_stmt_) != SQLITE_DONE) {
}
}
void flush() override {
}
};
logger->add_writer(std::make_unique<database_writer>(
"logs.db"));
Advanced Patterns
1. Filtering Writer
A writer that filters logs before outputting:
class filtering_writer : public logger_module::base_writer {
private:
std::unique_ptr<base_writer> inner_writer_;
std::function<bool(thread_module::log_level, const std::string&)> filter_;
public:
filtering_writer(std::unique_ptr<base_writer> inner,
std::function<bool(thread_module::log_level, const std::string&)> filter)
: inner_writer_(std::move(inner))
, filter_(std::move(filter)) {}
void write(thread_module::log_level level,
const std::string& message,
const std::string& file,
int line,
const std::string& function,
const std::chrono::system_clock::time_point& timestamp) override {
if (filter_(level, message)) {
inner_writer_->write(level, message, file, line, function, timestamp);
}
}
void flush() override {
inner_writer_->flush();
}
};
auto filtered = std::make_unique<filtering_writer>(
std::make_unique<console_writer>(),
[](thread_module::log_level level, const std::string& msg) {
return level >= thread_module::log_level::error &&
msg.find("critical") != std::string::npos;
}
);
logger->add_writer(std::move(filtered));
2. Async Writer Wrapper
The Logger System provides a built-in async_writer class that wraps any writer for asynchronous operation. For detailed documentation on async writers, including performance characteristics and usage patterns, see the Async Writers Guide.
Quick Example:
auto file_writer = std::make_unique<kcenon::logger::file_writer>("app.log");
auto async = std::make_unique<kcenon::logger::async_writer>(
std::move(file_writer),
10000,
std::chrono::seconds(5)
);
async->start();
logger->add_writer(std::move(async));
Asynchronous wrapper for log writers.
File writer for logging to files with optional buffering.
For high-throughput scenarios (>100K msg/sec), advanced async implementations are available. See Async Writers Guide for details.
Choosing the Right Base Class
| Base Class | Use When |
thread_safe_writer | Simple synchronous I/O (file, console, socket) - recommended |
base_writer | Custom synchronization, async patterns, or wrapper writers |
When to Use thread_safe_writer
✅ Simple output destinations (file, console, database) ✅ Standard mutex-based synchronization is sufficient ✅ Want to minimize boilerplate code
When to Use base_writer Directly
✅ Wrapper patterns (like async_writer, batch_writer) ✅ Writers with complex internal threading (like network_writer) ✅ Custom synchronization requirements (spinlock, RW-lock, lock-free)
Best Practices
- Use thread_safe_writer: For simple writers, inherit from
thread_safe_writer to get automatic thread-safety without boilerplate.
- Error Handling: Decide on failure behavior (throw, silent fail, retry/backoff) and expose counters for observability.
- Batching: Prefer batching for I/O heavy writers to reduce syscalls and context switches.
- Resource Management: Use RAII for file handles, sockets, and DB connections; ensure
flush() is efficient and idempotent.
- Configuration: Make writers configurable (paths, formats, thresholds), and document defaults.
- Security: Avoid writing secrets/PII; consider integrating
log_sanitizer upstream. If encrypting, use a vetted crypto library rather than demo components.
- Windows Networking: For socket-based writers, guard platform specifics (
#ifdef _WIN32) and initialize WinSock.
Testing Custom Writers
class writer_test {
public:
static void test_writer(std::unique_ptr<base_writer> writer) {
writer->write(thread_module::log_level::info,
"Test message",
__FILE__, __LINE__, __func__,
std::chrono::system_clock::now());
for (auto level : {log_level::trace, log_level::debug,
log_level::info, log_level::warning,
log_level::error, log_level::critical}) {
writer->write(level, "Level test", "", 0, "",
std::chrono::system_clock::now());
}
writer->flush();
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back([&writer, i]() {
for (int j = 0; j < 100; ++j) {
writer->write(log_level::info,
"Thread " + std::to_string(i),
"", 0, "",
std::chrono::system_clock::now());
}
});
}
for (auto& t : threads) {
t.join();
}
writer->flush();
}
};
Last Updated: 2025-01-11 (v1.4.0: Added writer category tags and hierarchy documentation)