Tutorial MUD, part 2: Logging

In this second part of making a simple MUD, we will try to add simple logging to our server. Logging is used to output information from the server, that can be read by an administrator in case of some problem.

This part continues where part 1 left of, and you can find the complete code from part 1 on github.

First lets talk about what kind of logging is needed from a MUD server:

  1. We of course have purely information text.
  2. As a server accepting remote connections, we might want to log information about new connections and disconnections, and other network events.
  3. A user may enter commands that might cause errors, or a function may return an unexpected error, so we need to log errors as well.
  4. Sometimes it might be good to enable debugging output, especially during development.

So we have at least four different classes of logging information that we should be able to handle. There might be more categories in the future, so it would be good if we easily could make our system so it will be easy to add new categories.

Also, the person running the server might want the logging output either on normal standard output, or written directly to a file.

Lets start by making a new header file with a separate namespace and also a new source file.

Header file:

[codesyntax lang=”cpp”]

#ifndef __LOGGER_H__
#define __LOGGER_H__

namespace logger
{
}

#endif // __LOGGER_H__

[/codesyntax]

And the source file:

[codesyntax lang=”cpp”]

#include "tm.h"
#include "logger.h"

[/codesyntax]

Don’t forget to add the source file to the Makefile so it will be built:

[codesyntax lang=”make”]

OBJFILES = main.o logger.o

[/codesyntax]

To initialize the logging system, we should have a function that takes either a file (std::ostream), a file name (std::string) or nothing (use std::clog for output). Since we can’t create a single function for all these three cases, we will use C++ overloading and create three functions with the same name and different arguments.

[codesyntax lang=”cpp”]

void logger::init()
{
	// Use std::clog as output
}

void logger::init(const std::string &filename)
{
	// Open a file and use that for output
}

void logger::init(std::ostream &file)
{
	// Use the provided file as output
}

[/codesyntax]

So far so good, but how and where should the output stream be stored? And how do we keep track of when and if it should be closed? This is a good match for a class, it has the stream as a private member variable, the stream is initialized by the class constructor, and closed (if it should be closed) as part of the destructor. This class should be private to the logging functions, so lets put it in an anonymous namespace in the source file:

[codesyntax lang=”cpp”]

#include <memory>
#include <fstream>

namespace
{
	class logger_helper
	{
	public:
		logger_helper(const std::string &filename)
			: m_file(filename), m_output(m_file)
			{ }
		logger_helper(std::ostream &file)
			: m_output(file)
			{ }
		logger_helper()
			: m_output(std::clog)
			{ }

		~logger_helper()
			{
				if (m_file.is_open())
					m_file.close();
			}

		std::ostream &get()
			{ return m_output; }

	private:
		std::ofstream m_file;    // File
		std::ostream &m_output;  // Actual output stream used
	};

	std::unique_ptr<logger_helper> logger_helper_ptr;
}

[/codesyntax]

Now lets finish those initialization functions:

[codesyntax lang=”cpp”]

void logger::init()
{
	// Use std::clog as output
	logger_helper_ptr.reset(new logger_helper());
}

void logger::init(const std::string &filename)
{
	// Open a file and use that for output
	logger_helper_ptr.reset(new logger_helper(filename));
}

void logger::init(std::ostream &file)
{
	// Use the provided file as output
	logger_helper_ptr.reset(new logger_helper(file));
}

[/codesyntax]

Now it comes to the function that will actually do the logging. It is as simple as just returning the output stream:

[codesyntax lang=”cpp”]

std::ostream &logger::get_logger()
{
	return logger_helper_ptr->get();
}

[/codesyntax]

Now we can use this functionality in main:

[codesyntax lang=”cpp”]

#include "logger.h"

int main(int argc, char *argv[])
{
	logger::init();  // Initialize logger using std::clog
	logger::get_logger() << "Hello world!\n";
}

[/codesyntax]

Now it’s time to add the categories. We do this by having a simple class containing the text we want to output for a specific category, and a number of variables, one for each category. The add a simple output operator overload to output it to a stream.

[codesyntax lang=”cpp”]

namespace logger
{
	namespace categories
	{
		class category
		{
		public:
			category(const std::string &text)
				: m_text(text)
				{ }

			const std::string &get() const
				{ return m_text; }

		private:
			std::string m_text;
		};

		const category debug{"[DEBUG]"};
		const category info {"[INFO ]"};
		const category error{"[ERROR]"};
		const category net  {"[NET  ]"};
	}
}

inline std::ostream & operator<<(std::ostream &os, const logger::categories::category &cat)
{
	os << cat.get();
	return os;
}

[/codesyntax]

Now we can do like this:

[codesyntax lang=”cpp”]

logger::get_logger() << logger::categories::info << "Hello world\n";

[/codesyntax]

This is kind of cumbersome, so we want to simplify this a little. This can be done through the C++ pre-processor:

[codesyntax lang=”cpp”]

#define LOG(cat, str) logger::get_logger() << logger::categories::cat << ' ' << str << '\n'

[/codesyntax]

[codesyntax lang=”cpp”]

LOG(info, "Hello world");

[/codesyntax]

We no longer need the newline, and is kind of neat as well. However, there is still something missing; A date and time for the logging! for that we need a new function, that returns a string containing the date and time in a nice format:

[codesyntax lang=”cpp”]

const std::string logger::get_date_time()
{
	std::time_t now = std::time(nullptr);
	char *now_s = asctime(localtime(&now));

	// asctime returns with a newline, remove it
	now_s[strlen(now_s) - 1] = '\0';

	return now_s;
}

[/codesyntax]

And modify the LOG macro:

[codesyntax lang=”cpp”]

#define LOG(cat, str) logger::get_logger() << logger::get_date_time() << " :: " << logger::categories::cat << ' ' << str << '\n'

[/codesyntax]

This concludes part two of the tutorial. For a complete listing please checkout github for the tag part-2.

Join the Conversation

1 Comment

Leave a comment