Journal Tutorial
From DANSE
NOTE: An additional journal tutorial is also now available in the DANSE meeting notes...
| Table of contents |
Introduction
"journal" is a powerful package for producing output from programs. It works in both Python and C++, and it works in both parallel and serial programs. Journal streams can be turned on or off individually; they can be implanted in libraries and activated by other programs that link to that library. Journals in C++ libraries that are bound to Python through extension modules will show up, when requested, in Python.
No special preprocessor directives clutter up the code. So if you're writing some code that is going to have a "print" or "std::cout << " statement, you should definitely consider using journal instead.
Basics
I assume you have journal correctly installed in your system.
Python
To use a journal in Python, first import journal and then instantiate a journal object (start a Python shell and follow along):
>>> import journal
>>> myjournal = journal.debug("test")
The journal attribute that you instantiate is called the severity, it is an indication of the kind of information flowing on this channel. Severity choices are debug, error, info, firewall, and warning. After indicating the severity, pick a name for this journal channel: in this case it's "test". Together, the severity and name describe a unique channel, you need to know these two pieces of information later to turn on the journal. The local name 'myjournal' is just a way of holding on to the journal object for the time being.
So what can you do with a journal? The most common operation is log:
>>> myjournal.log("hello journal world!")
>>>
That's right: nothing happens when you log to a journal. The journal must first be activated:
>>> myjournal.activate()
>>> myjournal.log("hello journal world!")
>> <stdin>:1:?
>> test(debug)
-- hello journal world!
>>>
The log method adds an entry to the journal, and if the journal is active, prints out the entry. An entry consists of a header line including the file from which it was called (stdin in this case, because we're using an interactive Python session), the line number, and the function it was in when called. On the next line it prints the name of the journal and the severity in parantheses. Finally, it prints the string it was told to log. (In the interactive session, you'll also see the string representation of the journal object after each call, something like "<journal.diagnostics.Diagnostic.Diagnostic object at 0x2a955f4650>"; I've omitted it for clarity.
Another frequently used journal feature is "line". The line method adds a line to the current journal entry, but doesn't cause the whole entry to be printed. Suppose you have the following module:
#!/usr/bin/env python
# Put this in a file called example1.py
import journal
def example():
info = journal.info("example")
for j in range(3):
info.line("This is the %ith trip through the loop" % j)
info.log("and now the loop is done")
return
if __name__ == '__main__':
journal.info("example").activate()
example()
# version
__id__ = "$Id$"
# End of file
(For more on the if __name__ == '__main__' construct, see PUT A LINK HERE). Running this module with "python example1.py" produces this output:
$ python example1.py >> example1.py:10:example >> example(info) -- This is the 0th trip through the loop -- This is the 1th trip through the loop -- This is the 2th trip through the loop -- and now the loop is done $
Notes:
- The line number in the header comes from where the log() method was invoked.
- You had to use log at the end to cause the entry to be printed. The following example, without log(), produces no output:
#!/usr/bin/env python
import journal
def example():
info = journal.info("example")
for j in range(5):
info.line("This is the %ith trip through the loop" % j)
info.line("and now the loop is done")
return
if __name__ == '__main__':
journal.info("example").activate()
example()
# version
__id__ = "$Id$"
# End of file
C++
Using journal in C++ is a bit richer. Here's a quick example:
#include "journal/debug.h"
int main( int argc, char **argv)
{
journal::debug_t debug("test");
debug.activate();
debug << "hello journal whirled" << journal::endl;
return 0;
}
First, note that everything lives in namespace journal. (If you're uncertain about why namespaces are good, please see PUT LINK HERE). The output of this program looks like this:
$ ./a.out >> debug(test) >> <unknown>:<unknown>:<unknown> -- hello journal whirled $
More notes:
- It's customary to keep the journal header files in a directory called "journal" and point the compiler to the directory that holds the journal directory. That way you avoid header file name clashes. For example, suppose the journal directory is in /home/you/products/include, you would want -I/home/you/products/include to show up in the compiler command line; if you're using the Caltech build system this should happen automatically.
- In this example, there was no output for file, line, or function; instead the context line has <unknown>:<unknown>:<unknown>. To get some context, use the "at" manipulator, as in this example:
#include "journal/debug.h"
int main( int argc, char **argv)
{
journal::debug_t debug("test");
debug.activate();
debug << journal::at(__HERE__)
<< "hello journal whirled"
<< journal::endl;
return 0;
}
which produces this output:
$ ./a.out >> debug(test) >> example2.cc:10:<unknown> -- hello journal whirled $
The at manipulator, combined with the __HERE__ macro, supply location information to journal. Now you've got the file name and the line number, but not the function name. To get that, you need to also include <portinfo>:
#include <portinfo>
#include "journal/debug.h"
int main( int argc, char **argv)
{
journal::debug_t debug("test");
debug.activate();
debug << journal::at(__HERE__) << "hello journal whirled" << journal::endl;
return 0;
}
which produces this output:
$ ./a.out >> debug(test) >> example2.cc:10:main -- hello journal whirled $
The build system should automatically add the lines that help the compiler find portinfo.
You can access the same journal channel from different functions, and even from different translation units (source code files, essentially):
#include <portinfo>
#include "journal/debug.h"
void function1();
void function2();
int main( int argc, char **argv)
{
journal::debug_t pizza("test");
debug.activate();
function1();
function2();
pizza << journal::at(__HERE__) << "I'm in main" << journal::endl;
return 0;
}
void function1()
{
// It doesn't matter what you call the local journal object: what matters is
// the severity and the name in parantheses.
journal::debug_t antenna("test");
antenna << journal::at(__HERE__) << "I'm in function1" << journal::endl;
return;
}
void function2()
{
journal::debug_t frog("test");
frog << journal::at(__HERE__) << "I'm in function2" << journal::endl;
return;
}
This produces the following output:
$ ./a.out >> debug(test) >> example2.cc:26:function1 -- I'm in function1 >> debug(test) >> example2.cc:34:function2 -- I'm in function2 >> debug(test) >> example2.cc:18:main -- I'm in main $
Notes:
- Journal channels can be used across translation units: function1 and function2 could have been placed in separate source files, and this example would still compile, link, and execute just fine.
- Journal channels can be put into libraries, and activated later from programs that link to that library.
- As long as two journal instantiations have the same severity and name, they'll refer to the same channel.
- The local names given to the journal objects (pizza, antenna, frog) don't matter.
Usage considerations
- Writing things to journals takes a little bit of time, even if the journal channel is not active. A journal call in an inner loop can really kil performance.
- Let the end user decide whether to activate a channel.
Advanced tricks & tips
C++
Using iostream manipulators
- You can use iostream manipulators on journals to adjust how numbers are displayed:
#include <portinfo>
#include "journal/debug.h"
#include <iomanip>
int main( int argc, char **argv)
{
journal::debug_t debug("test");
debug.activate();
debug << journal::at(__HERE__) << "here's a floating point number: "
<< 3.14159
<< journal::newline
<< std::setprecision(10)
<< std::showpoint
<< "Here's the same number with precision 10: "
<< 3.14159
<< journal::endl;
return 0;
}
produces:
$ ./a.out >> debug(test) >> example2.cc:12:main -- here's a floating point number: 3.14159 -- Here's the same number with precision 10: 3.141590000 $
journal should be .so/.dll
journal depends on a Singleton object to work correctly. This requires journal to be compiled into an .so or .dll to work correctly between Python and C++.
