Reader small image

You're reading from  Advanced C++

Product typeBook
Published inOct 2019
Reading LevelIntermediate
Publisher
ISBN-139781838821135
Edition1st Edition
Languages
Right arrow
Authors (5):
Gazihan Alankus
Gazihan Alankus
author image
Gazihan Alankus

Gazihan Alankus holds a PhD in computer science from Washington University in St. Louis. Currently, he is an assistant professor at the Izmir University of Economics in Turkey. He teaches and conducts research on game development, mobile application development, and human-computer interaction. He is a Google developer expert in Dart and develops Flutter applications with his students in his company Gbot, which he founded in 2019.
Read more about Gazihan Alankus

Olena Lizina
Olena Lizina
author image
Olena Lizina

Olena Lizina is a software developer with 5 years experience in C++. She has practical knowledge of developing systems for monitoring and managing remote computers with a lot of users for an international product company. For the last 4 years, she has been working for international outsourcing companies on automotive projects for well-known automotive concerns. She has been participating in the development of complex and high-performance applications on different projects, such as HMI (Human Machine Interface), navigation, and applications for work with sensors.
Read more about Olena Lizina

Rakesh Mane
Rakesh Mane
author image
Rakesh Mane

Rakesh Mane has over 18 years of experience in the software industry. He has worked with proficient programmers from a variety of regions such as India, the US, and Singapore. He has mostly worked in C++, Python, shell scripting, and database. In his spare time, he likes to listen to music and travel. Also, he likes to play with, experiment with, and break things using software tools and code.
Read more about Rakesh Mane

Vivek Nagarajan
Vivek Nagarajan
author image
Vivek Nagarajan

Vivek Nagarajan is a self-taught programmer who started out in the 1980s on 8-bit systems. He has worked on a large number of software projects and has 14 years of professional experience with C++. Aside from this, he has worked on a wide variety of languages and frameworks across the years. He is an amateur powerlifter, DIY enthusiast, and motorcycle racer. He currently works as an independent software consultant.
Read more about Vivek Nagarajan

Brian Price
Brian Price
author image
Brian Price

Brian Price has over 30 years experience working in a variety of languages, projects, and industries, including over 20 years' experience in C++. He was worked on power station simulators, SCADA systems, and medical devices. He is currently crafting software in C++, CMake, and Python for a next-generation medical device. He enjoys solving puzzles and the Euler project in a variety of languages.
Read more about Brian Price

View More author details
Right arrow

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:

  1. Open the project from Lesson6.
  2. Create a new directory called logger inside the src/ directory. You will get the following hierarchy:
    Figure 6.25: The hierarchy of the project
    Figure 6.25: The hierarchy of the project
  3. 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.
  4. 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_ */

  5. 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>

  6. 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();

    }

  7. 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();

    }

  8. 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();

    }

  9. 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();

    }

  10. 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();

    }

  11. 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();

    }

  12. 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();

    }

  13. 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_ */

  14. 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>

  15. 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

  16. 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;

         }

    }

    }

  17. 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_ */

  18. 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;

                        }

                   }

              }

         }

    }

  19. 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";

  20. Do the same replacement for the rest of code.
  21. 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.

Previous PageNext Page
You have been reading a chapter from
Advanced C++
Published in: Oct 2019Publisher: ISBN-13: 9781838821135

Authors (5)

author image
Gazihan Alankus

Gazihan Alankus holds a PhD in computer science from Washington University in St. Louis. Currently, he is an assistant professor at the Izmir University of Economics in Turkey. He teaches and conducts research on game development, mobile application development, and human-computer interaction. He is a Google developer expert in Dart and develops Flutter applications with his students in his company Gbot, which he founded in 2019.
Read more about Gazihan Alankus

author image
Olena Lizina

Olena Lizina is a software developer with 5 years experience in C++. She has practical knowledge of developing systems for monitoring and managing remote computers with a lot of users for an international product company. For the last 4 years, she has been working for international outsourcing companies on automotive projects for well-known automotive concerns. She has been participating in the development of complex and high-performance applications on different projects, such as HMI (Human Machine Interface), navigation, and applications for work with sensors.
Read more about Olena Lizina

author image
Rakesh Mane

Rakesh Mane has over 18 years of experience in the software industry. He has worked with proficient programmers from a variety of regions such as India, the US, and Singapore. He has mostly worked in C++, Python, shell scripting, and database. In his spare time, he likes to listen to music and travel. Also, he likes to play with, experiment with, and break things using software tools and code.
Read more about Rakesh Mane

author image
Vivek Nagarajan

Vivek Nagarajan is a self-taught programmer who started out in the 1980s on 8-bit systems. He has worked on a large number of software projects and has 14 years of professional experience with C++. Aside from this, he has worked on a wide variety of languages and frameworks across the years. He is an amateur powerlifter, DIY enthusiast, and motorcycle racer. He currently works as an independent software consultant.
Read more about Vivek Nagarajan

author image
Brian Price

Brian Price has over 30 years experience working in a variety of languages, projects, and industries, including over 20 years' experience in C++. He was worked on power station simulators, SCADA systems, and medical devices. He is currently crafting software in C++, CMake, and Python for a next-generation medical device. He enjoys solving puzzles and the Euler project in a variety of languages.
Read more about Brian Price