Module Logging

Changes made in the current development cycle can be found in the changelog.

Description (classes)

Logging.h: Send, record, and filter informational messages.

Prerequisite

Review Status

Reviewed By:
wbrouw
Date Reviewed:
1996/08/21
Programs:
Demos:
Tests:

Etymology

Logging, as in "log book", or "processing log."

Synopsis

The classes in the logging module have two essential purposes:
  1. To attach processing logs to datasets to retain a permanent history of informational messages that describe how the dataset arrived at its present state; and
  2. To inform the user about the progress and decisions made by various algorithms, such as those used in the Measures system.

The two fundamental classes in the Logging module are the LogMessage and LogSink classes. However, the class which is most of interest to application programmers is the LogIO class since it forms the usual interface to logging.

A LogMessage consists of an informational message tagged with the time, a priority (DEBUGGING, NORMAL,, WARN, or SEVERE) and the source code location of the origin of the message (for use in debugging primarily).

The LogSink is used to send the LogMessage to its destinations. Usually the message will be sent to both a global sink, intended for user information (e.g., a GUI window), and to a sink associated with the dataset(s) which are being modified. In practice, the application programmer does not need to worry about where the global messages go, that policy is implemented by the Tasking system. In practice, global messages will be sent to Glish, where they will appear in a GUI, unless Glish is not available, in which case SEVERE messsages (only) will be sent to stdout. However, the principle is that "ordinary" application programmers shouldn't worry about the details of the global sink - they should just use it.

A LogFilter can be used to filter messages (based on priority only at the moment) before they are sent to their appropriate sink(s).

The LogIO class puts an ostream like interface on top of the loggins system. Basically, the application programmer just has to create messages using and os << items type interface.

The first issue that the application programmer has to decide is whether to use logging at all, or if instead he should put his messages into a String or an ostream. It is never wrong to use log messages, however it is reasonable for low level classes to use Strings or ostreams, since the caller of that class will have the opportunity to put the text in a log message if he decides that's the most appropriate thing to do. Note that it is always wrong to write directly to cout or (other then for debugging) - use an ostream, so the caller can replace it with, for example, an ostringstream.

Once you decide to use logging, the application programmer only has to decide at every location he wants to log:

  1. What content do you want the message to have; and
  2. what priority does the message have (DEBUGGING, NORMAL, WARN, SEVERE).
Schematically, application programmers would use the logging system as follows:
    #include <aips/Logging.h>
    ...
    void MyClass:myFunction(LogIO &os)
    {
       os << LogIO::NORMAL << LogOrigin("MyClass", "myFunction()", WHERE);   // 1
       ...
       os << WHERE << "An informative message") << LogIO::POST;              // 2
       if (error()) {
           os << WHERE << LogIO::SEVERE << "Error!" << LogIO::POST;          // 3
           sink.post(msg);
           ...
       }
    }
    
  1. Set up the location where log messages come from. WHERE will expand into the file name and line number (useful for debugging). Set the priority to NORMAL (this is the default, but you don't know what the state of os is when it is passed in to this function).
  2. Set the message and the new line number (optional but encouraged) and post it.
  3. Change the priority to SEVERE and post an error message.

When a dataset is created from several other datasets, their input "histories" should be merged if possible. This can be done if the local log sink is in fact a Table. The way you do this is by interrogating the local sink to find out if it is in fact a TableLogSink. If it is, you can use a concatenate method of TableLogSink. Schematically this would be implemented as follows in some DataSet class that has a logSink method that returns a LogIO reference:

    void merge(DataSet &out, const DataSet &in1, const DataSet &in2) {
        ... copy the data from in1 and in2 to out
        if (out.logSink().localSink().isTableLogSink()) { // can write to out
            if (in1.logSink().localSink().isTableLogSink()) {
                out.logSink().localSink().castToTableLogSink().concatenate(
                     in1.logSink().localSink().castToTableLogSink());
        }
        if (... the same for in2 ...)
    }
    
Of course, DataSet might provide some convenience function for merging histories. However the point is that given a sink, you can safely determing whether or not it is in fact a TableLogSink, and if it is you can call its concatenate function, which takes another TableLogSink.

Example

The following example code is checked into the system as dLogging.cc. It is found in the Logging test directory.

    class DataClass
    {
    public:
      DataClass(const IPosition &shape, const LogSink &sink);  // 1
      void set(Int toWhat);                                    // 2
      LogIO   &sink() return os_p;}                            // 3
      Array<Int> &data() {return data_p;}                      // 4
      const Array<Int> &data() const {return data_p;}          // 5
    private:                                                   // 6
      Vector<Int> data_p;                                      // 7
      LogSink log_sink_p;                                      // 8
      LogIO os_p;                                              // 9
    };
    

This toy class is meant to represent one which is to have "attached" logging information. Generally, these classes would be fairly high level astronomical classes, e.g. Image, not Array. Note that only operations which change the data should be logged in the internal log. Operations which only read the data should be logged either globally, or in the class that is taking the results and modifying its own data.

1.
Depending on the application, the LogSink to be used might either be handed in to the class, as is the case here, or it might be created by the class. For example, a MeasurementSet will have a processing log table with a known name.
2.
A sample function that changes the state of the class. Here, it just sets all the elements of the internal array to toWhat.
3.
Return the LogIO that is used by this object. A member function like this should be provided for use by global functions which manipulate the object. Note that it is non-const --- the internal sink should be modified only by functions which CHANGE the object, otherwise the global sink should be used.
4.
Return the internal data. Arguably this should be logged at at least DEBUGGING level.
5.
Non-const version of the above. Note that it should not be logged since the state cannot be changed with this function.
7.
The internal data member.
8.
The location to which log mesages are sent.
9.
The LogIO object that will be the actual interface to the logging system.

    DataClass::DataClass(const IPosition &shape, const LogSink &sink)
      : log_sink_p(sink), os_p(log_sink_p)                                     // 1
    {                                                                          // 2
      os_p << LogOrigin("DataClass",                                           // 3
                   "DataClass(const IPosition &shape, const LogSink &sink)");  // 4
                                                                               // 5
      if (shape.nelements() != 1) {                                            // 6
        os_p << LogIO::SEVERE << WHERE <<                                      // 7
          "Illegal Shape! Must be one dimensional." << LogIO::EXCEPTION;       // 8
      }                                                                        // 9
                                                                               // 10
      data_p.resize(shape(0));                                                 // 11
      os_p << "Inital shape " << shape << "and value 2" <<                     // 12
          LogIO::NORMAL << LogIO::POST;                                        // 13
                                                                               // 14
      set(2);                                                                  // 15
    }
    
1.
The private LogSink data member is initialized with one that the caller provides. Note that LogSink uses reference semantics, so that if another "copy" of the sink is made then all the log messages will go to the same place. For example:
          LogSink a("mylogtable");
          LogSink b(a);
          LogSink c;
          c = a;
          ...
          c.post(...);  // ends up in mylogtable
          ...
          b.post(...);  // as does this
This can be useful if several classes might be modifying the same data, or if a data is spread over several objects.

Also, os_p is intialized from the sink.

3.
For a member function, the first argument to LogOrigin is the class name.
4.
The next argument is the function name. You should use the full name with arguments so that you can use the argument name in your messages. Leave off the return type. Cutting and pasting is easier than typing!
7.
WHERE is a predefined macro that gives the file name and line number.
8.
Create a SEVERE level error message, post it and throw an exception.
11.
This will post the message locally and globally, and then throw an exception. Another possibility would be to call postGloballyThenThrow() if you only wanted to send the message to the global sink (for example, if the object is hopelessly corrupted, or if the problem occurs in a read-only operation). The thrown exception is an AipsError. The post*Throw() functions will always set the priority to SEVERE, however it doesn't hurt to show your intentions
12.
Create and send a NORMAL priority message.
15.
Call set() from the constructor to give the data values an initial value.

    void DataClass::set(Int toWhat)
    {
      os_p << LogIO::NORMAL << LogOrigin("DataClass", "set(Int toWhat)");      // 1
      os_p << "Setting data values to " << toWhat << WHERE << LogIO::POST;     // 2
      uInt n = data_p.nelements();                                             // 3
      for (uInt i=0; i < n; i++) {                                             // 4
    #ifdef AIPS_DEBUG                                                          // 5
        os_p << LogIO::DEBUGGING << WHERE <<                                   // 6
          "Setting element  " << i << " to " << toWhat << LogIO::POST;         // 7
    #endif                                                                     // 8
        data_p(i) = toWhat;                                                    // 9
      }
    }
    

2.
This and the previous line set up and send a normal priority log message much as we did previously.
7.
LogMessages are relatively expensive to produces and consume. Use of them in a very tight loop should either be #ifdef'd out as in this example, or like:
      if (aips_debug_on) {
      ... set up and send log message ...
      }
The advantage of this code is that it's always available - so, for example, you can turn it on and off by manipulating the global variable aips_debug_on. However very tight loops cannot even afford this extra if, and should prefer the ifdef.

Normally the DEBUGGING messages are "boring but low-volume", and you should just send them normally.

    void square(DataClass &object)
    {
      object.sink() << LogIO::NORMAL << WHERE <<                              // 1
        LogOrigin("square(DataClass &object)") << "Squaring data elements"    // 2
    	<< LogIO::POST;                                                    // 3
      object.data() *= object.data();                                         // 4
    }
    

This function shows how a global function that modifies an object can send log messages to that objects LogSink using a function of that object to get access to its sink.

    float sum(const DataClass &object)
    {
      LogIO global(LogOrigin("sum(const DataClass &object)"));        // 1
      float theSum = sum(object.data());                              // 2
      global << WHERE << "Sum of object is: " << theSum;              // 3 
      return theSum;                                                  // 4
    }
    
This is an example of a global function that only reads -- does not change -- an object.
3.
Since we are not changing the data object, we only post the message globally, we don't write it to the data object's log sink. The caller of sum() might log the message somewhere else if the return value is used to modify data in some other object. Instead we send it to the global sink. Here we don't POST the message ourselves, we rely on the LogIO destructor to do it for us.

    int main()
    {
        LogSink::globalSink().filter(LogMessage::DEBUGGING);           // 1
        LogSink logger(LogMessage::NORMAL, "dLogging_messages_tmp");   // 2
                                                                       // 3
        IPosition legalShape(1, 10);                                   // 4
        DataClass dc(legalShape, logger);                              // 5
                                                                       // 6
        square(dc);                                                    // 7
                                                                       // 8
        Float total = sum(dc);                                         // 9
                                                                       // 10
        return 0;                                                      // 11
    }
    
1.
Change the priority of messages to display on the global sink's filter to DEBUGGING from the default NORMAL. The default global sink logs to cerr. The global sink can be replaced with LogSink::globalSink().
2.
Create the sink that we are going to use. This constructor will use a Table. If the table doesn't exist it will be created. If it does exist, new log messages will be appended to the end.
5.
Create an object with the provided sink. The alternative strategy, which will be used with classes like MeasurementSet is for the object to make it's own LogSink if it knows where it wants its messages to go.
7.
Changes the data - log messages go to its local sink.
9.
Reads the data - log messages go only to the global sink.

Motivation

  1. Attaching informational messages to datasets to describe their processing history.
  2. Informational messages to inform the user about the progress and parameters of algorithms - for example those used for reference frame conversions in the Measures module.

To Do


Classes

LogFilter -- Filter LogMessages on message priority. (full description)
LogFilterExpr -- Class to deal with a TaQL expression to filter messages. (full description)
LogFilterInterface -- Abstract base class for filtering LogMessages. (full description)
LogFilterTaql -- Filter LogMessages using a TaQL expression. (full description)
LogHolderIter -- Class doing the actual iteration through an LoggerHolder. (full description)
LogHolderIterEntry -- Class representing an entry in a LoggerHolder. (full description)
LogIO -- ostream-like interface to creating log messages. (full description)
LogMessage -- Informational log messages with with time, priority, and origin. (full description)
LogMessage_ostream -- Write a LogMessage to an ostream. (full description)
LogOrigin -- LogOrigin: The source code location of the originator of a LogMessage. (full description)
LogOrigin_ostream -- Write a LogOrigin to an ostream. (full description)
LogSink -- Distribute LogMessages to their destination(s) (full description)
LogSinkInterface -- Accepts LogMessages and posts them to some destination (full description)
LoggerHolder -- Class holding a hierarchy of loggers. (full description)
LoggerHolderIterator -- Class to iterate through an LoggerHolder. (full description)
LoggerHolderRep -- Representation of the class holding a hierarchy of loggers. (full description)
MemoryLogSink -- Save log messages in memory. (full description)
NullLogSink -- Throw away all messages. (full description)
SourceLocation -- Helper struct to get the source line. (full description)
StreamLogSink -- Send log messages to an ostream. (full description)
TableLogSink -- Save log messages in an AIPS++ Table (full description)
command -- Functions to send commands to a LogIO object. (full description)
output -- Functions to accumulate text in the output message. (full description)