Please review my ostream logger, ologger, which attempts to make logging as easy as using cout. The ostream operator<< is leveraged so that anything that can be logged to cout can be logged to this logger. To log custom classes/structs the user would need to overload operator<< in their custom class.
Any feedback would be much appreciated.
ologger.hpp:
/*
ologger or ostream logger, a logger as convenient as using std::cout
Features:
1. logging as per cout/cerr/clog ie logger.error << "i=" << 3 << std::endl;
2. logging obeys log level set. If you set log level error, then logging to info or debug is a no-op.
3. log rotation - specify a maximum file size in bytes and maximum number of files
4. Configure via path, file name prefix and file name extension.
End a line with std::endl
Example usage:
ologger logger(".", "projectz", "log", 10000, 5, ologger::log_level::LOG_INFO);
// nothing should be logged because we have set logger at ERROR level only
logger.debug << "low level developer data, X level exceeded, x=" << 9.75 << std::endl;
// nothing should be logged because we have set logger at ERROR level only
logger.info << "informational stuff about sending x protocol data to server, bytes transferred: " << 1250 << std::endl;
// this should be logged
logger.error << "!!! Invalid Data Received !!!" << std::endl;
*/
#ifndef OLOGGER_HPP_
#define OLOGGER_HPP_
#include <string>
#include <iostream>
#include <fstream>
class ologger {
public:
using endl_type = std::ostream& (std::ostream&);
enum class log_level {
LOG_NONE,
LOG_ERROR,
LOG_INFO,
LOG_DEBUG
};
/* Construct with log files path, file name prefix and suffix, max_file_size in bytes, max_files before rotation and logging level */
ologger(const std::string& path,
const std::string& file_prefix,
const std::string& file_suffix,
size_t max_file_size,
size_t max_files,
log_level level = log_level::LOG_NONE);
// prevent copying object
ologger(const ologger&) = delete;
ologger(ologger&&) = delete;
ologger& operator=(const ologger&) = delete;
ologger& operator=(ologger&&) = delete;
// debug level logging
class Debug {
public:
Debug(ologger& parent);
template<typename T>
Debug& operator<< (const T& data)
{
if (parent_.level_ >= log_level::LOG_DEBUG) {
if (start_of_line_) {
parent_.prefix_message();
start_of_line_ = false;
}
parent_.log_stream_ << data;
}
return *this;
}
Debug& operator<<(endl_type endl);
private:
ologger& parent_;
bool start_of_line_;
};
// info level logging
class Info {
public:
Info(ologger& parent);
template<typename T>
Info& operator<< (const T& data)
{
if (parent_.level_ >= log_level::LOG_INFO) {
if (start_of_line_) {
parent_.prefix_message();
start_of_line_ = false;
}
parent_.log_stream_ << data;
}
return *this;
}
Info& operator<<(endl_type endl);
private:
ologger& parent_;
bool start_of_line_;
};
// error level logging
class Error {
public:
Error(ologger& parent);
template<typename T>
Error& operator<< (const T& data)
{
if (parent_.level_ >= log_level::LOG_ERROR) {
if (start_of_line_) {
parent_.prefix_message();
start_of_line_ = false;
}
parent_.log_stream_ << data;
}
return *this;
}
Error& operator<<(endl_type endl);
private:
ologger& parent_;
bool start_of_line_;
};
private:
size_t changeover_if_required();
const std::string to_string(ologger::log_level level);
void prefix_message();
void make_logger(const std::string& path, const std::string& file_prefix, const std::string& file_suffix);
const std::string path_;
const std::string file_prefix_;
const std::string file_suffix_;
size_t max_file_size_;
size_t max_files_;
log_level level_;
std::fstream log_stream_;
public:
Debug debug;
Info info;
Error error;
};
#endif // OLOGGER_HPP_
ologger.cpp:
#ifdef _WIN32
#define _CRT_SECURE_NO_WARNINGS
#define SEPARATOR ('\\')
#else
#define SEPARATOR ('/')
#endif
#include "ologger.hpp"
#include <cstring>
#include <ctime>
#include <chrono>
#include <stdexcept> // domain_error for bad path
static size_t get_next_file_suffix(const std::string& path) {
size_t next { 0 };
std::fstream fstrm(path + SEPARATOR + "next", std::ofstream::in | std::ofstream::out);
if (fstrm) {
fstrm >> next;
}
return next;
}
// convert blank string to . and remove trailing path separator
static const std::string make_path(const std::string& original_path) {
std::string massaged_path{ original_path };
if (massaged_path.empty()) {
massaged_path = ".";
}
if (massaged_path[massaged_path.size() - 1] == SEPARATOR) {
massaged_path = massaged_path.substr(0, massaged_path.size() - 1);
}
return massaged_path;
}
const std::string ologger::to_string(ologger::log_level level) {
switch (level) {
case ologger::log_level::LOG_NONE: return "";
case ologger::log_level::LOG_ERROR: return "error";
case ologger::log_level::LOG_INFO: return "info";
case ologger::log_level::LOG_DEBUG: return "debug";
default:
return "";
}
}
void ologger::make_logger(const std::string& path, const std::string& file_prefix, const std::string& file_suffix) {
size_t next_id = get_next_file_suffix(path);
log_stream_.open(path + SEPARATOR + file_prefix + std::to_string(next_id) + "." + file_suffix, std::ofstream::out | std::ofstream::app);
if (!log_stream_.good()) {
throw std::domain_error("ologger error: unable to open log file: " + path + SEPARATOR + file_prefix + std::to_string(next_id) + "." + file_suffix);
}
}
ologger::ologger(const std::string& path,
const std::string& file_prefix,
const std::string& file_suffix,
size_t max_file_size,
size_t max_files,
log_level level)
:
path_(make_path(path)),
file_prefix_(file_prefix),
file_suffix_(file_suffix),
max_file_size_(max_file_size),
max_files_(max_files),
level_(level),
debug(*this), info(*this), error(*this) {
make_logger(path_, file_prefix_, file_suffix);
}
size_t ologger::changeover_if_required() {
size_t next_id{0};
if (log_stream_) {
const std::streampos pos = log_stream_.tellp();
if (static_cast<size_t>(pos) > max_file_size_) {
next_id = get_next_file_suffix(path_);
next_id = (next_id + 1) % max_files_;
std::fstream fstrm(path_ + SEPARATOR + "next", std::ofstream::out | std::ofstream::trunc);
if (fstrm) {
fstrm << next_id;
}
log_stream_.close();
log_stream_.clear();
// if next file exists, delete so we start with empty file
const std::string next_file{ path_ + SEPARATOR + file_prefix_ + std::to_string(next_id) + "." + file_suffix_ };
std::remove(next_file.c_str());
log_stream_.open(next_file, std::ofstream::out | std::ofstream::app);
}
}
return next_id;
}
std::string get_time_stamp()
{
const auto now = std::chrono::system_clock::now();
const std::time_t now_time_t = std::chrono::system_clock::to_time_t(now);
char timestamp[50]{};
std::strftime(timestamp, sizeof(timestamp), "%Y-%m-%d,%H:%M:%S", std::localtime(&now_time_t));
const int millis = std::chrono::time_point_cast<std::chrono::milliseconds>(now).time_since_epoch().count() % 100;
snprintf(timestamp + strlen(timestamp), sizeof(timestamp) - strlen(timestamp), ".%03d,", millis);
return timestamp;
}
void ologger::prefix_message() {
log_stream_ << get_time_stamp() << to_string(level_) << ',';
}
//inner logging level classes for ostream overloading
ologger::ologger::Debug::Debug(ologger& parent)
: parent_(parent), start_of_line_(true)
{}
ologger::Debug& ologger::Debug::operator<<(endl_type endl)
{
if (parent_.level_ >= log_level::LOG_INFO) {
parent_.log_stream_ << endl;
}
parent_.changeover_if_required();
start_of_line_ = true;
return *this;
}
ologger::ologger::Info::Info(ologger& parent)
: parent_(parent), start_of_line_(true)
{}
ologger::Info& ologger::Info::operator<<(endl_type endl)
{
if (parent_.level_ >= log_level::LOG_INFO) {
parent_.log_stream_ << endl;
}
parent_.changeover_if_required();
start_of_line_ = true;
return *this;
}
ologger::ologger::Error::Error(ologger& parent)
: parent_(parent), start_of_line_(true)
{}
ologger::Error& ologger::Error::operator<<(endl_type endl)
{
if (parent_.level_ >= log_level::LOG_ERROR) {
parent_.log_stream_ << endl;
}
parent_.changeover_if_required();
start_of_line_ = true;
return *this;
}
example main.cpp to exercise:
#include "ologger.hpp"
int main() {
ologger logger(".", "projectz", "log", 10000, 5, ologger::log_level::LOG_ERROR);
// nothing should be logged because we have set logger at ERROR level only
logger.debug << "low level developer data, X level exceeded, x=" << 9.75 << std::endl;
// nothing should be logged because we have set logger at ERROR level only
logger.info << "informational stuff about sending x protocol data to server, bytes transferred: " << 1250 << std::endl;
// this should be logged
logger.error << "!!! Invalid Data Received !!!" << std::endl;
for (int i = 0; i < 10000; i++) {
logger.error << "Beware the Ides of March, i=" << i << std::endl;
}
}
Makefile:
CFLAGS=-ggdb3 -Wall -Werror -pedantic -std=c++11
logger: ologger.cpp main.cpp
g++ -o logger $(CFLAGS) main.cpp ologger.cpp
std::clog
-style logger? ;-) \$\endgroup\$