1
\$\begingroup\$

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
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Why not a std::clog-style logger? ;-) \$\endgroup\$ Dec 31, 2020 at 15:58
  • \$\begingroup\$ I would expect a logger class to send the data to the system log. \$\endgroup\$ Dec 31, 2020 at 20:11
  • 1
    \$\begingroup\$ The problem here (that most system logger handle) is if the logging level is not matched then no work is done. Here you are still doing work when logging level is created then simply discarding that output when operator<< is called. So you are still forcing the application to crate the formatted message (which is wasted work). \$\endgroup\$ Dec 31, 2020 at 20:16

2 Answers 2

5
\$\begingroup\$

There's very little difference between Debug, Info and Error; to me it looks like those three can be a single class, with only one extra member (to store the logging threshold).

The whole business of log rotation seems to be duplicating standard utilities such as logrotate. I recommend doing one thing and doing it well, rather than sprawling over several responsibilities like that.

Talking of which, it would have been nice to include an adapter to syslog(), which instantly gives the ability to log to files (with rotation), to network, or to database, without any extra code.

\$\endgroup\$
2
\$\begingroup\$

One of the main problems of cout is that it is not suitable for multithreading logging, as characters from different prints might interleave. It can be fixed without syntax change (with some trickery) by making streaming operation return a handle that can be further streamed, and that locks the logger's mutex in constructor and releases in destructor.

The other problem is that cout is inconvenient for logging, as its formatting is poorly designed to begin with. Check out the fmt open source library for better alternatives. Its primary function, format, is included in C++20.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.