9
\$\begingroup\$

I've been exploring design based around some of the more advanced C++11 features lately, and some of them are turning out to be rather useful for some projects I'm working on. One is this policy-based, variadic logger class that supports any combination of policies to determine where it logs output to.

Most loggers I've seen that adopt this design pattern only accept one, and there are cases where you would want more, so this expands on that without having to instantiate several loggers each with a distinct policy.

It's also easily extensible, flexible, being able to add additional logging policies and log levels.

/*! @brief Defines level severities for logging. */
enum class LogLevel : uint32_t
{
    Info    = 0,
    Warning = 1,
    Error   = 2
};

template <class ... Policies>
class Logger
{
    typedef std::tuple<Policies...> policies_t;

private:

    policies_t _policies;

    uint32_t _numWarnings;
    uint32_t _numErrors;

    static const size_t s_MaxPrefixSize = 128;
    static const size_t s_MaxBufferSize = 1024;

public:

    Logger ()
        : _policies { std::forward<Policies>(Policies{})... }
        , _numWarnings(0)
        , _numErrors(0)
    {}

    static const size_t numPolicies = sizeof...(Policies);

    template <typename T>
    T* getPolicy ()
    {
        // Implementation of FindTypeIndex omitted for brevity.
        static const auto index = FindTypeIndex<policies_t, T>::value;
        return getPolicy<index>();
    }

    template <size_t Index>
    auto getPolicy () -> decltype(&std::get<Index>(_policies))
    {
        static_assert(Index < numPolicies, "policy index out of range");
        return &std::get<Index>(_policies);
    }

    template <LogLevel L>
    void log (const char* format, ...)
    {
        std::time_t time = std::time(nullptr);
        std::string timeString = std::ctime(&time);

        char prefixString[s_MaxPrefixSize];
        snprintf(prefixString, s_MaxPrefixSize, "[ %s ] : %s ",
                 timeString.substr(0, timeString.size() - 1).c_str(), // Strip off newline character.
                 logLevelString(Int2Type<(size_t)L>()).c_str());

        va_list args;
        va_start(args, format);
        char buffer[s_MaxBufferSize];
        vsnprintf(buffer, s_MaxBufferSize, format, args);
        va_end(args);

        std::string message;
        message += prefixString;
        message += buffer;
        message += "\n";

        doPolicy<0>(_policies, message);
    }

    template <LogLevel L>
    void log (std::string msg)
    {
        log<L>(msg.c_str());
    }

    void logCounts ()
    {
        char buffer[s_MaxBufferSize];
        snprintf(buffer, s_MaxBufferSize, "\nWarnings: %d\nErrors: %d\n", _numWarnings, _numErrors);
        std::string countsString = buffer;
        doPolicy<0>(_policies, countsString);
    }

private:
    // Int2Type implementation omitted for brevity.

    std::string logLevelString (Int2Type<(size_t)LogLevel::Info>)
    {
        return std::string("< INFO >");
    }

    std::string logLevelString (Int2Type<(size_t)LogLevel::Warning>)
    {
        _numWarnings++;
        return std::string("< WARNING >");
    }

    std::string logLevelString (Int2Type<(size_t)LogLevel::Error>)
    {
        _numErrors++;
        return std::string("< ERROR >");
    }

    template <size_t I>
    typename std::enable_if<I != numPolicies>::type doPolicy (policies_t& p, const std::string& msg)
    {
        std::get<I>(p).write(msg);
        doPolicy<I+1>(p, msg);
    }

    template <size_t I>
    typename std::enable_if<I == numPolicies>::type doPolicy (policies_t& p, const std::string& msg) {}

};

And then two simple logger policies as a sample. Additional policies could be added for network logging, user display, etc.

/*! @class ConsoleLogPolicy
    @brief Logger policy that outputs messages to the debug console.
 */
class ConsoleLogPolicy
{
protected:
    template <class ... T> friend class Logger;

    void write (const std::string& buffer)
    {
        std::cout << buffer;
    }
};


/*! @class FileLogPolicy
    @brief Logger policy that outputs messages to an external text file.
    @details The file must be opened by calling \ref open, with the path and filename. When a file stream goes out of scope, 
    or is destroyed, it will automatically flush its contents, so it is not absolutely necessary to call \ref close.
 */
class FileLogPolicy
{
public:

    FileLogPolicy () {}
    FileLogPolicy (const FileLogPolicy& copy) = delete;
    FileLogPolicy (FileLogPolicy&& other) : _fileStream(std::move(other._fileStream)) {}
    ~FileLogPolicy ()
    {
        close();
    }

    bool open (const std::string& path, const std::string& fileName)
    {
        _fileStream.open(path + fileName);
        if ( ! _fileStream.is_open() ) {
            std::cerr << "FileLogPolicy Error: Failed to open file '" << fileName << "' at " << path << "\n";
            return false;
        }

        _fileStream << "-- File Log Started --\n\n";

        return true;
    }

    void close ()
    {
        if (_fileStream.is_open()) {
            _fileStream << "\n-- File Log Ended --\n\n";
            _fileStream.close();
        }
    }

protected:
    std::ofstream _fileStream;

    template <class ... T> friend class Logger;

    void write (const std::string& buffer)
    {
        if (_fileStream) {
            _fileStream << buffer;
        } else {
            std::cerr << "FileLogPolicy Error: File stream is not open for writing.\n";
        }
    }
};

Finally, a usage example of a logger that outputs to both the console and a file:

Logger<ConsoleLogPolicy, FileLogPolicy> logger;
logger.getPolicy<FileLogPolicy>()->open("/Users/Me/path/", "theLog.txt");
// ..

logger.log<LogLevel::Info>("Something informative %d", aValue);
// ..

std::string aMessage("Warning.");
logger.log<LogLevel::Warning>(aMessage);
// ..

logger.log<LogLevel::Error>(anException.description());
// ..

logger.logCount();
logger.getPolicy<1>()->close();
\$\endgroup\$
6
  • \$\begingroup\$ Please do not reinvent features that are done so well already. Everybody tries to do this and everybody does it so badly. I you want to write a C++ logger it should be a wrapper around syslog. That thing (syslog) does so much more than you can imagine and so much better than any other user written implementation. It also standardizes a lot of factors about log files so a maintainer does not need to search for them of figure out how to filter/sort/organize/retain/compress/centraliz them (as tools already exist) which your specific implementation does not provide. \$\endgroup\$ Commented Sep 13, 2014 at 17:37
  • \$\begingroup\$ I'm not actually familiar with syslog, but I'll have a look at it to see what I can learn from it. \$\endgroup\$
    – Chris
    Commented Sep 13, 2014 at 19:07
  • \$\begingroup\$ en.wikipedia.org/wiki/Syslog \$\endgroup\$ Commented Sep 13, 2014 at 20:00
  • \$\begingroup\$ @Loki - syslog does not provide an Audit log level. Often, that's the only level I care about. The other levels pretty much allow debugging in the field. I'll hold my opinions on debugging in the field, but I will say: debug your warez before its provided to customers and unsuspecting users. \$\endgroup\$
    – user53032
    Commented Sep 13, 2014 at 21:17
  • 1
    \$\begingroup\$ Consider using __attribute__(format) or SA_FormatString to get some compile time checking for the format strings. \$\endgroup\$
    – glampert
    Commented Sep 14, 2014 at 4:26

2 Answers 2

4
\$\begingroup\$

First off, you are using std:: for everything else, might as well use it for std::uint32_t and std::size_t

I'm not very familiar with variadic templates, but I feel like dropping into C's varargs is not the way to go.

  1. You have to do the messy snprintf to make the prefix (which now means you need the max prefix length)
  2. Only primitive types can be logged.

About the Log levels, is it possible to add levels without access to the header? The way logLevelString() works is beyond me.

So this some code for the way I might try to do it. The main thing is that I don't use varargs:

#include <iostream>
#include <cstdint>
#include <string>
#include <ctime>

struct LogLevel
{
  const std::string name;

  LogLevel( const std::string& n ): name( n ){}
  virtual ~LogLevel( void ) = default;
};

struct Info : public LogLevel
{
  Info( void ):LogLevel( "Info" ){}
};

template <typename T >
void print( T only )
{
    std::cout << only << std::endl;
}

template <typename T, typename ... args >
void print( T current, args... next )
{
    std::cout << current << ' ';
    print( next... );
}

template < typename Level, typename ... args >
void print( args ... to_print )
{
  std::cout << Level().name << ": ";

  std::time_t time = std::time(nullptr);
  std::string time_str = std::ctime( &time );
  time_str.pop_back();

  std::cout << time_str << ": ";

  print( to_print... );
}

struct Point
{
  int x; int y;
  friend std::ostream& operator<<( std::ostream& out, const Point& p )
    {  return out << '{' << p.x << ',' << p.y << '}'; }
};


int main( void )
{
  Point p = { 1, 2 };
  print< Info >( 1, "this", 1.2, p );
  return 0;
}
\$\endgroup\$
1
  • \$\begingroup\$ I agree about the messy snprintf and varargs. I like your idea of a variadic print function instead of it, which would be able to handle non-primitive types, which is another good point. And no, if you didn't have access to the header, you couldn't add additional log levels as it is now. Perhaps I can turn it into a struct/class that you could inherit from if this was the case. \$\endgroup\$
    – Chris
    Commented Sep 13, 2014 at 19:03
2
\$\begingroup\$

The main problem I have with this is:

void log (const char* format, ...)

Its not what you think.

The problem is that all the parameters have to be evaluated before the method is called. This can potentially be expensive (or it may be cheap but you are doing it a lot). If the logging is turned off this can be a lot of work with in the end no result being generated.

Most (all the good ones) logging systems I have seen, make sure that the parameters are only evaluated if the logging is actually going to be done. There is no point in doing work if you are not going to use the values in a message.

\$\endgroup\$

Your Answer

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

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