JMRI Code: How to Use the Logging Facilities
This page provides a little information on how JMRI logs error, status and debugging information.
For other details on JMRI internals, please see the technical pages.
JMRI uses the Jakarta Log4J package to handle logging from individual classes. Specifically, as of Summer 2015, we use version 1.2.15; note that's not the most recent one! We use that via SLF4J framework. (For libraries that use the java.util.logging framework, e.g. jmDNS, we also use the jul-to-slf4j adapter)
Logging Levels
This combination provides several levels of messages:Level | Code Fragment | Use |
---|---|---|
ERROR | log.error(..) | Indicates the desired operation is not going to happen, and should explain why. ERROR is meant to be used only for serious problems which should get some attention every time they happen, typically indicating a possible fault in JMRI itself. |
WARN | log.warn(..) | The program is still operating, sort of, but something has gone wrong; often used to say "This operation may not have done all you wanted". Use WARN when the cause of the problem is incorrect user input. |
INFO | log.info(..) | Routine messages you want to see in normal operation. Keep these to a minimum please, there shouldn't be any after the program has started up. |
DEBUG | log.debug(..) | Detailed messages, only used in debugging. There is a lot of this, and turning it all on can slow the program down significantly. |
TRACE | log.trace(..) | Very detailed messages, even more than DEBUG, used for voluminous debugging (e.g. every character in a transmission) that would normally be off even when debugging. Typically only turned on for one class at a time due to volume and performance issues. |
Configuration
By convention, JMRI applications will attempt to initialize Log4J using a "default.lcf" logging control file. JMRI contains a version of the default.lcf file with extensive comments. (When running JUnit tests, a similar "tests.lcf" file is used.) Although JMRI distributes the default.lcf file in the "Program Directory", it's better if you put your own version in the "Preferences Directory, which can be found by selecting the "Locations" item in the main help menu) The rest of this section describes the contents of a logging control file using the contents of default.lcf as an example. AsThe line:
log4j.rootCategory= INFO, A1, T, Rcontrols where logging output goes. Later in the file, there are "appenders" A1, T and R defined that write log messages to:
- A1 - the console on the local computer, really stdout/stderr, which is also available via the Console item on the JMRI Help menu
- R - a single file. By default, this is called "session.log". This starts anew each time JMRI is restarted.
- T - a set of rolling files. The current file is called "messages.log". When it reaches 2MB, it rolls to a new file. Two older versions are kept. Note that this continues through program restarts, so you can see any messages logged at the very beginning or end of the program.
The "default.lcf" file also determines the format of the output by setting the "layout" parameter(s).
log4j.appender.R.layout=org.apache.log4j.PatternLayout
log4j.appender.R.layout.ConversionPattern=%d{ISO8601} %-37.37c{2} %-5p - %m [%t]%n
An example of the default format:
2015-10-28 20:31:52,307 jmri.jmrit.powerpanel.PowerPane WARN - No power manager instance found, panel not active [AWT-EventQueue-0]The columns are:
2015-10-28 20:31:52,307
- local time the message was loggedjmri.jmrit.powerpanel.PowerPane
- the place in the code (class name) that emitted the messageWARN
- the severity of the messageNo power manager instance found, panel not active
- the message itself[AWT-EventQueue-0]
- name of the thread that emitted the message
Coding
To log messages from a class named MyClass, add this to the bottom of the class's .java file:private static final Logger log = LoggerFactory.getLogger(MyClass.class);
and add imports for org.slf4j.Logger and org.slf4j.LoggerFactory in your imports section:
import org.slf4j.Logger;It's also OK to combine those into the fully-qualified form:
import org.slf4j.LoggerFactory;
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(MyClass.class);
(If logging is removed (e.g. commented out), it's OK to comment out the log definition so that it can be easily added back later on.)
Then for each message to log insert a line like:
log.debug("message");
Messages that are not just an explicit string should use this form instead for variables:
log.debug("Found {}", numberEntries);
The string operations to build the actual error message (in this case, combining "Found" and the numberEntries argument) are only done if the message is going to be stored in the logs, which saves a lot of time during normal (non-debug) execution.
Note It slows down logging to add .toString()
to any fields or method results, so do not add that. SLF4J takes care
of ensuring that toString()
is called correctly.
If it would be computationally expensive to pass a parameter into the log, use following form so the program does not waste time calculating parameters (in this case, calling numberEntries() to get a value to pass to the function call):
if (log.isDebugEnabled()) { log.debug("Found {}", numberEntries()); }
If you want to emit a warning only the first time it happens, there's a service method that will handle that for you:
Log4JUtil.warnOnce(log, "The warning with arguments {} {}", "A", "B");Note: The warnOnce method requires a bit of special handling in the unit and CI tests, see testing documentation.
Exceptions should be logged as:
log.error("my local text"+exception.getLocalizedMessage(), exception);to include the user readable description from the exception itself, plus all its traceback information.