Chapter 6 – Streams and I/O
Activity 1 The Logging System for The Art Gallery Simulator
The thread-safe logger allows us to output data to the Terminal simultaneously. We implement this logger by inheriting from the std::ostringstream class and using a mutex for synchronization. We will implement a class that provides an interface for the formatted output and our logger will use it to extend the basic output. We define macro definitions for different logging levels to provide an interface that will be easy and clear to use. Follow these steps to complete this activity:
- Open the project from Lesson6.
- Create a new directory called logger inside the src/ directory. You will get the following hierarchy:
Figure 6.25: The hierarchy of the project
- Create a header and source file called LoggerUtils. In LoggerUtils.hpp, add include guards. Include the <string> header to add support for working with strings. Define a namespace called logger and then define a nesting namespace called utils. In the utils namespace, declare the LoggerUtils class.
- In the public section, declare the following static functions: getDateTime, getThreadId, getLoggingLevel, getFileAndLine, getFuncName, getInFuncName, and getOutFuncName. Your class should look as follows:
#ifndef LOGGERUTILS_HPP_
#define LOGGERUTILS_HPP_
#include <string>
namespace logger
{
namespace utils
{
class LoggerUtils
{
public:
static std::string getDateTime();
static std::string getThreadId();
static std::string getLoggingLevel(const std::string& level);
static std::string getFileAndLine(const std::string& file, const int& line);
static std::string getFuncName(const std::string& func);
static std::string getInFuncName(const std::string& func);
static std::string getOutFuncName(const std::string& func);
};
} // namespace utils
} // namespace logger
#endif /* LOGGERUTILS_HPP_ */
- In LoggerUtils.cpp, add the required includes: the "LoggerUtils.hpp" header, <sstream> for std::stringstream support, and <ctime> for date and time support:
#include "LoggerUtils.hpp"
#include <sstream>
#include <ctime>
#include <thread>
- Enter the logger and utils namespaces. Write the required function definitions. In the getDateTime() function, get the local time using the localtime() function. Format it into a string using the strftime() function. Convert it into the desired format using std::stringstream:
std::string LoggerUtils::getDateTime()
{
time_t rawtime;
struct tm * timeinfo;
char buffer[80];
time (&rawtime);
timeinfo = localtime(&rawtime);
strftime(buffer,sizeof(buffer),"%d-%m-%YT%H:%M:%S",timeinfo);
std::stringstream ss;
ss << "[";
ss << buffer;
ss << "]";
return ss.str();
}
- In the getThreadId() function, get the current thread ID and convert it into the desired format using std::stringstream:
std::string LoggerUtils::getThreadId()
{
std::stringstream ss;
ss << "[";
ss << std::this_thread::get_id();
ss << "]";
return ss.str();
}
- In the getLoggingLevel() function, convert the given string into the desired format using std::stringstream:
std::string LoggerUtils::getLoggingLevel(const std::string& level)
{
std::stringstream ss;
ss << "[";
ss << level;
ss << "]";
return ss.str();
}
- In the getFileAndLine() function, convert the given file and line into the desired format using std::stringstream:
std::string LoggerUtils::getFileAndLine(const std::string& file, const int& line)
{
std::stringstream ss;
ss << " ";
ss << file;
ss << ":";
ss << line;
ss << ":";
return ss.str();
}
- In the getFuncName() function, convert the function name into the desired format using std::stringstream:
std::string LoggerUtils::getFuncName(const std::string& func)
{
std::stringstream ss;
ss << " --- ";
ss << func;
ss << "()";
return ss.str();
}
- In the getInFuncName() function convert the function name to the desired format using std::stringstream.
std::string LoggerUtils::getInFuncName(const std::string& func)
{
std::stringstream ss;
ss << " --> ";
ss << func;
ss << "()";
return ss.str();
}
- In the getOutFuncName() function, convert the function name into the desired format using std::stringstream:
std::string LoggerUtils::getOutFuncName(const std::string& func)
{
std::stringstream ss;
ss << " <-- ";
ss << func;
ss << "()";
return ss.str();
}
- Create a header file called LoggerMacroses.hpp. Add include guards. Create macro definitions for each LoggerUtils function: DATETIME for the getDateTime() function, THREAD_ID for the getThreadId() function, LOG_LEVEL for the getLoggingLevel() function, FILE_LINE for the getFileAndLine() function, FUNC_NAME for the getFuncName() function, FUNC_ENTRY_NAME for the getInFuncName() function, and FUNC_EXIT_NAME for the getOutFuncName() function. As a result, the header file should look as follows:
#ifndef LOGGERMACROSES_HPP_
#define LOGGERMACROSES_HPP_
#define DATETIME \
logger::utils::LoggerUtils::getDateTime()
#define THREAD_ID \
logger::utils::LoggerUtils::getThreadId()
#define LOG_LEVEL( level ) \
logger::utils::LoggerUtils::getLoggingLevel(level)
#define FILE_LINE \
logger::utils::LoggerUtils::getFileAndLine(__FILE__, __LINE__)
#define FUNC_NAME \
logger::utils::LoggerUtils::getFuncName(__FUNCTION__)
#define FUNC_ENTRY_NAME \
logger::utils::LoggerUtils::getInFuncName(__FUNCTION__)
#define FUNC_EXIT_NAME \
logger::utils::LoggerUtils::getOutFuncName(__FUNCTION__)
#endif /* LOGGERMACROSES_HPP_ */
- Create a header and source file called StreamLogger. In StreamLogger.hpp, add the required include guards. Include the LoggerMacroses.hpp and LoggerUtils.hpp header files. Then, include the <sstream> header for std::ostringstream support, the <thread> header for std::thread support, and the <mutex> header for std::mutex support:
#include "LoggerMacroses.hpp"
#include "LoggerUtils.hpp"
#include <sstream>
#include <thread>
#include <mutex>
- Enter the namespace logger. Declare the StreamLogger class, which inherits from the std::ostringstream class. This inheritance allows us to use an overloaded left shift operator, <<, for logging. We don't set the output device, so the output will not be performed – just stored in the internal buffer. In the private section, declare a static std::mutex variable called m_mux. Declare constant strings so that you can store the logging level, file and line, and function name. In the public section, declare a constructor that takes the logging level, file and line, and function name as parameters. Declare a class destructor. The class declaration should look like as follows:
namespace logger
{
class StreamLogger : public std::ostringstream
{
public:
StreamLogger(const std::string logLevel,
const std::string fileLine,
const std::string funcName);
~StreamLogger();
private:
static std::mutex m_mux;
const std::string m_logLevel;
const std::string m_fileLine;
const std::string m_funcName;
};
} // namespace logger
- In StreamLogger.cpp, include the StreamLogger.hpp and <iostream> headers for std::cout support. Enter the logger namespace. Define the constructor and initialize all the members in the initializer list. Then, define the destructor and enter its scope. Lock the m_mux mutex. If the internal buffer is empty, output only the date and time, thread ID, logging level, file and line, and the function name. As a result, we will get the line in the following format: [dateTtime][threadId][logLevel][file:line: ][name() --- ]. If the internal buffer contains any data, output the same string with the buffer at the end. As a result, we will get the line in the following format: [dateTtime][threadId][logLevel][file:line: ][name() --- ] | message. The complete source file should look as follows:
#include "StreamLogger.hpp"
#include <iostream>
std::mutex logger::StreamLogger::m_mux;
namespace logger
{
StreamLogger::StreamLogger(const std::string logLevel,
const std::string fileLine,
const std::string funcName)
: m_logLevel(logLevel)
, m_fileLine(fileLine)
, m_funcName(funcName)
{}
StreamLogger::~StreamLogger()
{
std::lock_guard<std::mutex> lock(m_mux);
if (this->str().empty())
{
std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << std::endl;
}
else
{
std::cout << DATETIME << THREAD_ID << m_logLevel << m_fileLine << m_funcName << " | " << this->str() << std::endl;
}
}
}
- Create a header file called Logger.hpp and add the required include guards. Include the StreamLogger.hpp and LoggerMacroses.hpp headers. Next, create the macro definitions for the different logging levels: LOG_TRACE(), LOG_DEBUG(), LOG_WARN(), LOG_TRACE(), LOG_INFO(), LOG_ERROR(), LOG_TRACE_ENTRY(), and LOG_TRACE_EXIT().The complete header file should look as follows:
#ifndef LOGGER_HPP_
#define LOGGER_HPP_
#include "StreamLogger.hpp"
#include "LoggerMacroses.hpp"
#define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME}
#define LOG_DEBUG() logger::StreamLogger{LOG_LEVEL("Debug"), FILE_LINE, FUNC_NAME}
#define LOG_WARN() logger::StreamLogger{LOG_LEVEL("Warning"), FILE_LINE, FUNC_NAME}
#define LOG_TRACE() logger::StreamLogger{LOG_LEVEL("Trace"), FILE_LINE, FUNC_NAME}
#define LOG_INFO() logger::StreamLogger{LOG_LEVEL("Info"), FILE_LINE, FUNC_NAME}
#define LOG_ERROR() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_NAME}
#define LOG_TRACE_ENTRY() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_ENTRY_NAME}
#define LOG_TRACE_EXIT() logger::StreamLogger{LOG_LEVEL("Error"), FILE_LINE, FUNC_EXIT_NAME}
#endif /* LOGGER_HPP_ */
- Replace all the std::cout calls with the appropriate macro definition call. Include the logger/Logger.hpp header in the Watchman.cpp source file. In the runAdd() function, replace all instances of std::cout with macro definitions for different logging levels. The runAdd() function should look as follows:
void Watchman::runAdd()
{
while (true)
{
std::unique_lock<std::mutex> locker(m_AddMux);
while(!m_AddNotified)
{
LOG_DEBUG() << "Spurious awakening";
m_CondVarAddPerson.wait(locker);
}
LOG_INFO() << "New person came";
m_AddNotified = false;
while (m_CreatedPeople.size() > 0)
{
try
{
auto person = m_CreatedPeople.get();
if (m_PeopleInside.size() < CountPeopleInside)
{
LOG_INFO() << "Welcome in the our Art Gallery";
m_PeopleInside.add(std::move(person));
}
else
{
LOG_INFO() << "Sorry, we are full. Please wait";
m_PeopleInQueue.add(std::move(person));
}
}
catch(const std::string& e)
{
LOG_ERROR() << e;
}
}
LOG_TRACE() << "Check people in queue";
if (m_PeopleInQueue.size() > 0)
{
while (m_PeopleInside.size() < CountPeopleInside)
{
try
{
auto person = m_PeopleInQueue.get();
LOG_INFO() << "Welcome in the our Art Gallery";
m_PeopleInside.add(std::move(person));
}
catch(const std::string& e)
{
LOG_ERROR() << e;
}
}
}
}
}
- Notice how we use our new logger. We invoke the macro definition with parentheses and use the left shift operator:
LOG_ERROR() << e;
Or
LOG_INFO() << "Welcome in the our Art Gallery";
- Do the same replacement for the rest of code.
- Build and run the application. In the Terminal, you will see that log messages appear from the different threads with different logging levels and with useful information. After some time has passed, you will get some output similar to the following:
Figure 6.26: The execution result of the activity project
As you can see, it's really easy to read and understand logs. You can easily change the StreamLogger class to write logs to the file on the filesystem if your needs differ. You can add any other information that you may need to debug your application using logs, such as output function parameters. You can also override the left shift operator for your custom types to output debug information easily.
In this project, we employed many things that we have learned about during this chapter. We created an additional stream for thread-safe output, we formatted the output to the desired representation, we employed std::stringstream to perform formatting data, and we used macro definitions for convenient logger usage. Thus, this project demonstrates our skills in working with concurrent I/O.