Pages

Thursday, May 11, 2017

Logging - Basics #1

Logging is an important aspect of computer science as it provides a way of tracing back exactly or approximately to some degree what have happened in relation to a certain computation in-between a certain time interval. Of course there are many kinds of log with respect to what, where and how the operation is done and what, where and how its results are archived for later retrieval and analysis. This definition should suffice for a quick startup :-)

Depending on the system there may be different kinds of logging support and facilities but I don't know of a single system that doesn't provide at least one kind of logging per each of its subsystems. But that is of course strongly platform dependent, thus not easily or possibly portable. Because of this it is generally useful for an application (or program) to also plan its own logging features that will work across any platform it supports.

On this post I present one very simple solution consisting on a mixed strategy of encapsulating an old-style (C) logging in a C++ object. It's definitely not a final and optimal solution. There is a long way to perfect it, but nevertheless it's a start. As having traces of an old-style logging it suffers from typical problems from the past related to the care that must be taken with respect to printf-like functions and its formatting strings and respective variadic parameters. As being a helper C++ concrete object the logging interface could be more elaborated and generalized by a new interface layer consisting of an abstract C++ class factoring out the most fundamental aspects of logging. But for now it's just as it is, and it works despite of these main deficiencies.

The implementation here is target at a 64-bit Unix system but it should be easily ported to another platform such as a Windows system. The log is recorded to a standard line-oriented file where each record begins with a simple timestamp marker. Each line is "freely" composed by means of an encapsulated standard C printf-like call. Let's see how...

#ifndef TOOLBOX_HXX
#define TOOLBOX_HXX

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

#include <ctime>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <cstdarg>

#include <string>

namespace toolbox
{

struct log
{

  //
  // Arguably there could be an overloaded version
  // taking no parameters at all, a default constructor.
  // The constructor below could take an optional parameter
  // for indicating the desired creation mode of the log file.
  //
  log( const std::string & filename ) :
   fd { -1 }, filename { full_path( filename.c_str() ) }
  {
  }

  ~log()
  {
    close();
  }

  //

  // Arguably there could be an overloaded version
  // taking a new filename and mode parameters...
  // The mode parameter could be optional.
  //
  bool open()
  {

    // This may be too invasive.
    // Maybe open() should be "nilpotent",
    // that is, if it's already open, leave it open...
    close();

    const int flags { O_WRONLY | O_APPEND | O_CREAT };
    const ::mode_t mode { S_IRUSR | S_IWUSR | S_IRGRP };

    fd = ::open( filename.c_str(), flags, mode );

    return is_open();
  }

  bool close()
  {
    return
is_open()
      ? ::close( fd ) == 0
        ? ( fd = -1, true )
        : false
      : true;
  }

  const int fileno() const
  {
    return fd;
  }

  bool write( const char * fmt, ... ) const
  {
    if ( ! timestamp() )
      return false;

    va_list ap;
    va_start( ap, fmt );

    // MT-safe if setlocale(3C) not called
    // Hence, possibly a serious limitation!
    // More advanced C++ to the rescue!

    char * buffer = 0;
    ::vasprintf( &buffer, fmt, ap );
    ::ssize_t rv = ::write( fd, buffer, ::strlen(buffer) );
    ::free( buffer );

    va_end( ap );

    return rv != -1;
  }

  
  bool is_open() const
  {
    return fd >= 0;
  }

  
public:

  const std::string filename;

private:

  //

  // In fact, this function shouldn't be here.
  // File paths should be resolved outside this object.
  // That is, the filename parameter should be OK beforehand.
  //
  const std::string full_path( const char * filename ) const
  {
    // MT-safe
    char * path = ::getcwd( 0, 0 );
    std::string name = path;
    ::free( path );

    name += "/";

    name += filename;

    return name;
  }

  bool timestamp() const
  {
    ::time_t utc;
    if ( ::time( &utc ) < 0 )
      return false;

    ::tm local;
    if ( ::localtime_r( &utc, &local ) == 0 )
      return false;

    const int size = 32;
    char ts[ size ];
    if ( ::strftime( ts, size, "[%F %T] ", &local ) == 0 )
      return false;

    return ::write( fd, ts, ::strlen(ts) ) != -1;
  }

private:

  int fd;
};

}

#endif /* TOOLBOX_HXX */


This C++ object may be used like this:

#include "toolbox.hxx"

int main()
{
  toolbox::log log( "trace.log" );

  if ( ! log.open() )

    return EXIT_FAILURE;

  const int seconds = 30;

 
  if ( ! log.write( "Resting for %d seconds...\n", seconds ) )

    return EXIT_FAILURE;
  
  ::sleep( seconds );
 
  return EXIT_SUCCESS;
}
 
And may generate a line similar to the following in the log file:

[2017-05-11 16:29:29] Resting for 30 seconds...
 
But I know I'm far from a close-to-the-ideal solution.
For instance, among things I recognize as missing or needing change are:

  1. The class is concrete, not derived from an abstract class, so it's not usable as pluggable subsystem to another class needing general logging services.
       
  2. As for logging to a file, not enough file control has been "exposed" upon creating / opening the file corresponding to the parameters to open(2).
       
  3. The write member function uses error-prone old C-style for variable parameters and old-C formatting functions that exhibits potential concurrency limitations. One could consider a sort of variadic function template mimicking legacy printf functions. Or, at least, using a std::stringstream reference as a parameter for working-around the lack of safety of vasprintf().
       
  4. The class stores the file name as a std::string member which can be considered waste of space and less-than-optimal design, because as the full_path() member function these are file system details outside the logging problematic itself.
       
  5. The design could be much better if the implementation took advantage of the standard I/O streams for formatting and buffering interfaces.
 
I've been trying to address some of the above pending issues...