0

Raspberry Pi/C++ Log Class

With having written many Raspberry Pi programs now, it made sense for me to create a little Log class which I’ve used in all my projects now which I feel is really quite nice to use and intuitive. In this post I’ll run through a few of its features.

Source Code: https://github.com/comeradealexi/RaspberryPiShared

One of the features I wanted from my logging class was the ability for it to automatically add new line characters, it takes that extra thing to remember away from the programmer and removes the chance of printing problems in the log.

Initially with my logging class I had it so that whenever the operator<< was invoked, it would automatically print a new line character. This meant that lines of code such as the following would result in 2 lines in the log, which is undesired. Also, every time the operator<< is invoked, it’s locking and unlocking the mutex each time which is inefficient.

Log::Get() << "Received a new door status update of: " << DoorStatusToString(ds);

So I came up with a system whereby the operator<< for the Log class, instead of returning the same Log class, returns a Log::LogReturn class type.  This little class also overloads the operator<< and writes data to the log but keeps track if it has written anything to the log. When the destructor for the Log::LogReturn class is invoked, it will only print a new line character if nothing has been written to it.  This results in the desired behaviour where the final call to the operator<<, where nothing is written, a new line character is added and the mutex is unlocked.

With the above code working well, this also allowed me to make the initial call to the Log class to add extra information to the output such as the current date and time as well as the thread id.

Below is a snapshot of the entire log class at the time of publishing this post, see GitHub for the latest version.

#pragma once

#include <iostream>
#include <ostream>
#include <istream>
#include <mutex>
#include <stdarg.h>
#include <fstream>
#include <iomanip> // put_time
#include <chrono>  // chrono::system_clock
#include <ctime>   // localtime
#include <sstream> // stringstream
#include <ctime>
#include <string>  // string
#include <thread>

class Log
{
private:
	std::ofstream m_LogFile;
	std::mutex m_LogMutex;

public:
	//The LogReturn is used to ensure when the LogReturn destructor is called, it writes the newline to the end of the current line.
	//Since there is never an actual LogReturn
	struct LogReturn
	{
		LogReturn(Log& log) : m_Log(log) { }

		~LogReturn()
		{
			if (m_bWrittenTo == false)
			{
				m_Log.m_LogFile << "\n";
				m_Log.m_LogFile.flush();
				std::cout << "\n";
				m_Log.m_LogMutex.unlock();
			}
		}

		template <typename T>
		LogReturn operator<<(const T& TData)
		{
			m_bWrittenTo = true;
			m_Log.m_LogFile << TData;
			std::cout << TData;
			return LogReturn(m_Log);
		}

		Log& m_Log;
		bool m_bWrittenTo = false;
	};

	Log()
	{
		OpenLog();
	}
	~Log()
	{
		CloseLog();
	}
	static inline Log& Get()
	{
		static Log log;
		return log;
	}

	static std::string Return_current_time_and_date()
	{
		auto now = std::chrono::system_clock::now();
		auto in_time_t = std::chrono::system_clock::to_time_t(now);

		std::stringstream ss;
		ss << "(" << std::this_thread::get_id() << ") ";
		ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X: "); 
		return ss.str();
	}

	static std::string ToString(const std::chrono::system_clock::time_point& timePoint)
	{
		auto in_time_t = std::chrono::system_clock::to_time_t(timePoint);

		std::stringstream ss;
		ss << std::put_time(std::localtime(&in_time_t), "%Y-%m-%d %X");
		return ss.str();
	}

	//Allows writing to ofstream as well as cout!
	template <typename T>
	LogReturn operator<<(const T& TData)
	{
		m_LogMutex.lock();
		m_LogFile << Return_current_time_and_date();
		m_LogFile << TData;
		std::cout << TData;
		return LogReturn(*this);
	}

	void Printf(const char* __fmt, ...)
	{
		std::unique_lock<std::mutex> lk(m_LogMutex);

		const int k_bufferSize = 1024 * 1024;
		static char cLogBuffer[k_bufferSize];
		if (m_LogFile.good())
		{
			va_list argptr;
			va_start(argptr, __fmt);
			vsprintf(cLogBuffer, __fmt, argptr);
			va_end(argptr);
			m_LogFile << Return_current_time_and_date();
			m_LogFile << cLogBuffer;
			m_LogFile.flush();
			std::cout << cLogBuffer;
		}
	}

	void OpenLog()
	{
#ifdef DEBUG
#define BUILD "DEBUG"
#else
#define BUILD "RELEASE"
#endif

		//Use epoc time as a file log stamp
		std::chrono::milliseconds ms = std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::system_clock::now().time_since_epoch());
		std::string strName = "Log_" BUILD "_";
		strName += std::to_string(ms.count());
		strName += ".txt";

		std::unique_lock<std::mutex> lk(m_LogMutex);
		m_LogFile.open(strName.c_str(), std::ofstream::out);
	}

	void CloseLog()
	{
		*this << "Exiting Program With Normal Termination";

		std::unique_lock<std::mutex> lk(m_LogMutex);
		if (m_LogFile.good())
		{
			m_LogFile << "Log Closed.";
			m_LogFile.close();
		}
	}
};

Potential Bugs:

The main bug I can foresee with this system is if, for some reason the user were to hold onto the Log::LogReturn class type and not have its destructor invoked at the end of a line of code. I believe I should be able to get around this however by disabling the copy/assignment operator.