|
As we have a strong demand for consistent logging I would like to present this draft describing the logging service. It should fulfill the following requirements:
Java and C++
slim simple interface
fast
small
configurable during runtime
gracefully degrading
We approached this task from the Java side and found 2 APIs mentionable:
JDK 1.4 Logging API (referenced as
JUL further on; derived from java.util.logging,
see
http://java.sun.com/j2se/1.4/docs/api/java/util/logging/package-summary.html
)
Jakarta's log4j API (see http://jakarta.apache.org/log4j/docs/index.html )
The basic concepts used in these two APIs are nearly the same,
although terminology and interfaces differ. log4j is older than JUL
and more evolved, e.g. provides complete support for runtime
configuration. Neither of the two APIs fulfills the above
requirements, such as C++ support. Nevertheless, we decided to adopt
the JUL interfaces because they will become the de-facto standard
over time and because we want to avoid yet another logging API.
First of all I would like to introduce JUL as we will use
it's syntax and terminology.
The most prominent class is Logger
,
where you call the methods for logging. A Logger object can be
created like
static Logger aLogger; aLogger = Logger.getLogger("name.Of.Logger");
and used like
aLogger.logp(Level.INFO, "package.class", "method", "log string");
The Logger objects form a hierarchical tree based on the dotted
notation of the name you assign to the logger with the parameter of
getLogger
(you could use any string
there, but mirroring the package structure is a straight forward
approach). All properties of the various logger objects are inherited
in this tree structure. The most prominent property is the log level.
JUL defines seven levels in the following order: SEVERE, WARNING,
INFO, CONFIG, FINE, FINER, and FINEST. The table below should provide
some guidelines (keeping close to the definitions given in JUL) when
to use what level.
Level |
Description |
Example |
User |
Admin |
Developer |
SEVERE |
serious failure that prevents normal program execution |
cannot find computer |
x |
x |
|
WARNING |
potential problem but can be managed by the system |
connection refused - retrying |
x |
x |
|
INFO |
informational messages |
user logged in |
x |
x |
|
CONFIG |
static configuration messages |
CPU: x86 |
|
x |
x |
FINE |
tracing information (low volume / most important) |
first login of user - initializing defaults |
|
|
x |
FINER |
tracing information (entering, exiting, exceptions) |
entering queryServiceLocation() |
|
|
x |
FINEST |
tracing information (high volume / less important) |
performing insert of x into table y |
|
|
x |
You can configure the logging framework via a configuration file. If this file contains a line like
com.sun.star.foo.level = SEVERE
the above
call will have no effect because you raised the log
threshold for this Logger higher than its log level. The same holds
true if you writeaLogger
.logp(LogLevel::INFO,
...)
com.sun.level = SEVERE or .level = SEVERE
as all Logger properties are inherited and com.sun.star.foo
is a child of com.sun
is a child of
root
.
The logging service contains the XLogger
interface with the following signature:
module com { module sun { module star { module util { module logging { interface XLogger: com::sun::star::uno::XInterface { XLogger getLogger( [in] string name ); long getLevel(); string getName(); boolean isLoggable( [in] long level ); [oneway] void logp( [in] long level, [in] string sourceClass, [in] string sourceMethod, [in] string msg ); }; }; }; }; }; };
A piece of code using XLogger in C++ could look like
Sequence< Any > aNameSequence(1); aNameSequence[0] <<= OUString::createFromAscii("MyNamespace.MyClass"); Reference< XInterface > r = m_rxServiceManager->createInstanceWithArguments( OUString::createFromAscii("com.sun.star.util.logging.Logger"), aNameSequence ); Reference< XLogger > rxLogger( r , UNO_QUERY ); rxLogger->logp( LogLevel::INFO, OUString::createFromAscii("MyNamespace"), OUString::createFromAscii("MyClass::myMethod(myParameterType)"), OUString::createFromAscii("") );
(The actual location of the objects is transparent to the client in UNO. This imposes heavy and (according to the log threshold used) maybe useless net traffic if a XLogger object is hosted on a remote server. So there is a XLoggerRemote interface which provides some additional methods capable of coping with this problem. However there exists no implementation for this interface.)
.watch=10 .level=SEVERE .handler=com.sun.star.util.logging.ConsoleHandler com.sun.star.util.logging.ConsoleHandler.target=stderr com.sun.star.util.logging.ConsoleHandler.layout=%d %-7p %c %M %m %n
As all properties of the configuration can be changed during runtime
the logging framework monitors the configuration file for changes.
The interval in seconds for this monitoring is specified by the
.watch
property. You should
provide this property. If you omit it the watchdog thread is not
started at all and you loose the feature of runtime configuration.
The value should be greater than 0. If the startup value is
less than 1 the watchdog thread is not started. If you change the
interval to a value less than 1 the watchdog thread stops checking
for changes.
.level
specifies the log threshold for a Logger. It can be
prepended by the name of a Logger (or dot-seperated parts of it) to
log more selectively. A line like com.sun.level=FINEST
will overwrite the level SEVERE
of the
root logger for the logger with the name com.sun
and all its children. If you don't prepend a name you configure the
root logger from which all other loggers inherit their properties.
This root .level
must be
provided.
The .handler
property is used to specify the output device to be used. For the
time being there are two handlers available:
com.sun.star.util.logging.ConsoleHandler
prints to the console and com.sun.star.util.logging.FileHandler
prints to a file.
The target
property of a handler is used to specify the output device more
closely. FileHandler.target
accepts any
fully qualified path with a filename denoting the file to which the
log lines should be apended (if the files doesn't exist it is
created). ConsoleHandler.target
accepts
either stdout
or stderr
thus printing to the standard output or standard error stream
respectively.
The layout
property of a handler is
used to specify the actual layout of the log string. The value of
this property is a string very similiar to the format specification
string known from printf:
%[flags][width][.precision][{h | l | I64 |
L}]type
All fields have the same semantics as
known from printf except for the type
field. It does not denote the data type but the content type to be
displayed (data type is always string here). Valid values for type
are:
Type |
Description |
---|---|
c |
name of the logger |
p |
level of the logger |
C |
caller class |
M |
caller method |
m |
log message |
H |
caller host |
P |
caller PID |
T |
caller thread |
d |
current date (ISO8601) |
n |
linefeed |
So the above configuration file configures the logging service to print the following line on standard error on console
2002-03-14 12:20:31,683 INFO com.sun.star.Foo myMethod(int) some message
if
aLogger.logp(Level.INFO, "com.sun.star.Foo" , "myMetod(int)", "some message");
is called.
It is recommended that each class in your code that requires logging should instantiate its own logging object. Despite the fact that you can instanciate any logger with any name you want you should choose <package>.<class> for this (nevertheless there might be some exceptions to this in special cases). It is quite straightforward and gives you some nice configuration possibilities in combination with the inherit-feature of the logging API, e.g. prepending some name before .level.
Using a static CLASS variable proved being a good helper for clarifiying the code. You define it once and provide it as second parameter in every logp call.
You should always pass the complete signature of a method as third parameter. This eases evaluating a log if you use many methods with different signatures but with the same name (overloading).
There are no explicit laws when to use which log levels. The table in some chapters above gives some rough frame for using them. It is directly derived from the official JUL documentation and should be percieved as a general guideline. But in real life you may discover it somewhat difficult to decide which log level to choose. This holds especially true for the INFO level and the FINE/FINER/FINEST group.
You should keep in mind that every log level has a recipient: If you log a message that is directed to a user and you are using the FINE level - something is clearly rotten.
Exceptions should be instrumented with two lines of log code like
catch(Exception &e) { aLogger.logp(Level::SEVERE, CLASS, "myMethod(int)", "EXCEPTION: " + e.getSomeName()); aLogger.logp(Level::FINEST, CLASS, "myMethod(int)", e.getArbitraryInfo());
The level of the latter log call should be in any case FINEST to
allow dumping the complete stack trace only if runtime considerations
are neglectable. The level of the first log call should be choosen
according to the importance of the exception thrown.
The method isLoggable
can be used if
the evaluation of any of the parameters of the logp method will
inflict unproportional computing effort. This will minimize
evaluation costs if the log threshold of the logger is higher than
its log level:
if (aLogger.isLoggable(LogLevel::INFO)) aLogger.logp(LogLevel::INFO, "com.sun.star.foo.Bar", "Bar", veryExpensiveMethod());
If the threshold is lower you will have a loss of performance as
isLoggable
is executed twice: in your
code to avoid unnecessary parameter evaluation and in the log method
itself. But isLoggable
does not consume
too much computing power; it just returns a comparison between two
integers.
Another issue: you decrease the clarity
of code if you use isLoggable very often. It is probably best to use
isLoggable
only if you have a parameter
method that is outstanding costly. Making that kind of decisions
requires some knowledge of the performance implications of the
different code parts.
If you use or code a UNO component you have the possibility to use UNO contexts. I don't want to go in too much detail here (see: http://udk.openoffice.org/common/man/concept/uno_contexts.html ), but these contexts will allow you to save data in this context even across machine boundaries. This provides a solution for the problem when you want to add, let's say, the name of a user in every log message but the username is not available in all classes. You also can add arbitrary XLogger objects to the context for reusing them thoughout the system. The code for using some UNO context could look like:
private Logger m_aLogger; private XComponentContext m_aContex; // UNO component Bar(XComponentContext aContext) { m_aContext = aContext; m_aLogger = m_aContext.getValueByName("some.Logger"); m_aLogger = m_aLogger.getLogger("com.sun.star.foo.Bar"); } // if don't code a UNO component but need the context // you need to init UNO in Java like this // (and are able to init components afterwards): XMyObject bootstrapUno() { XComponentContext context = Bootstrap.createInitialContext(); XMultiServiceFactory smgr = context.getServiceManager(); XMyObject myObject = smgr.createInstance("com.sun.star.myObject"); return myObject; }
(The context will be used to configure the XLogger components themselves as well. But this will be coded when the UNO context can be made persistent which isn't the case just right now.)
Author: Thomas
Pfohe
($Date: 2002/03/26 11:14:25 $) |