logging – cout style logger in C++

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