|Use tracing during development as well as after your application is deployed|
Andrei Malacinski (email@example.com)
WebSphere software engineer, IBM
September 1, 2000
In today's world of tight schedules and the constant demand for more function, developers often don't have the time, or don't take the time, to think about a debugging (or tracing) strategy. There are always more important things to code than debug statements. This article provides developers that strategy for free -- Java source code and all -- relieving them of the need to design and implement one of their own tracing strategies and allowing them to concentrate on their application's main logic. Two techniques are presented in this article, one for run-time tracing and one for development-time tracing.
Development-time tracing configures your application's code so that the output of trace statements are turned on, as a debugging aid, while the application is being developed, but turn them off when the application is deployed. Customers will never see your development trace messages.
A common development-time trace mechanism allows Java
System.out.println() calls to be triggered by a build flag when you build your Java source code. But, you can easily expand this basic model. During code development, embed calls to the same trace functions you created for your run-time tracing. Further, set it up so that when you turn off the build flag, all development trace calls are optimized out of the compiled code. An inspection of the
.class file byte code shows that the development trace calls do not exist in the file. See "Development-time trace mechanism" for the Java source code needed to employ this technique as well as an explanation on how to do it.
Run-time tracing sets up your application to allow customers to turn on the output of debug (or trace) messages. If your customers encounter problems running your application, they can turn on tracing, run the application again, and send the trace messages to you, to help you debug their problems.
A simple run-time trace mechanism consists of basic Java
System.out.println() calls. But, with a little more thought this can be expanded. For example, you can employ several levels of tracing that can be turned on dynamically at run time by the customer. Additionally, you can give the customer the option of where to send the output of the trace, such as to the console or a file. In implementing these traces, it makes sense to create a set of reusable trace functions you can share with your fellow developers. These functions encapsulate trace details, such as where to send the trace output, whether to include date/time stamps in the trace statements, and so on. See "Run-time trace mechanism" for a set of trace functions and sample Java source code showing how to use these functions.
Development-time trace mechanism
Development-time tracing involves inserting trace statements in your code as a debugging aid. These statements are turned on during application development and testing, but turned off when your application is built for deployment. This technique makes use of the same set of reusable
printMessage methods in the run-time trace mechanism. Additionally, build flags are declared in such a way that when they are turned off, all development trace calls are optimized out of the compiled code.
First, declare a set of build flags to implement tracing. See this Java source code which uses
final keywords. Declaring a variable as
final indicates (and enforces) that the value of the variable will not change from its initial value.
During development, set the DEBUG variable to the desired output trace level. In this example, TRACE_MEDIUM encompasses both low- and medium-level tracing, TRACE_HIGH encompasses low, medium and high tracing, and TRACE_ALL encompasses all trace levels. TRACE_ALL is equivalent to TRACE_HIGH. At application deployment time, set the DEBUG variable to TRACE_OFF. When applying this development-time tracing technique, you can define the tracing scope of each level according to your needs.
Note: When making changes to any of the variables in the code above, such as during final development stages to turn off the trace, you must recompile all Java source code that makes use of these variables, or code that makes use of variables that in turn make use of these variables, and so on. This step is necessary because the Java compiler optimizes your code by directly inserting the variables' values into code that makes use of them. Hence, the compiler will not automatically detect the need to compile all affected files through its prerequisite checking.
Next, expand the
printMessage class to include development-time trace capabilities by adding declarations, such as:
public static final boolean traceMedium =
SharedConstants.TRACE_MEDIUM) != 0)
? true : false;
public static final boolean traceAll =
SharedConstants.TRACE_ALL) != 0)
? true : false;
Note: The variables are declared as
Here is the new
, with development-time trace capabilities. This sample code uses the
PrintMessage's trace functions to effect development-time debugging.
You as the developer decide the output trace level category for each message based on the context in which it is coded.
Note: To maintain Java code optimization goals, the example above directly checks the values of the
static final variables.
To verify that the development trace messages are optimized out of the Java byte code by the compiler, use a program to reverse assemble
The Java Development Kit (JDK) provides
javap. Execute it as follows:
javap -c packageName.MyApplication
Note: When invoking
javap, you must supply the package name of
Run-time trace mechanism
Let's start simple
The customer turns on debugging by supplying a command-line option variable to the Java application upon invocation of your application. Within the context of the running application, that variable and its value are available as a Java system property. During application initialization, your code reads the value of this system property to determine if the customer has turned on run-time tracing.
See this sample code that implements the technique.This sample illustrates a utility class called
PrintMessage. This class encapsulates all the code needed to support run-time debugging. Including a class such as
PrintMessage among your Java classes allows each developer writing application code to make use of its functions. A public method
isRunTimeDebugOn(), returns the value of a static variable
debugOn, which is initialized upon class loading. Subsequent method calls to
isRunTimeDebugOn() do not impart much of a performance overhead because the method merely returns the value of a variable. Trace output functions such as
printMessageWithDateTime(String msg) and
printMessage(String msg) are called to output each trace message. See this sample code that makes use of
PrintMessage's trace functions to effect run-time debugging.
For flexibility, you can implement multiple print message method variations in your
printMessage class. In the above example:
printMessage(String msg), simply outputs the trace message exactly as the method caller intended.
printMessageWithDateTime(String msg) adds a date/time stamp to the front of the trace message before outputting it. This could be useful if you are tracing an application that involves database access, network communication, system I/O, or other involved transactions, where time stamps would help debugging.
printMessageNoNewLine(String msg) outputs the trace line without an ending line break.
Numerous other print message method variations are also possible.
To start the Java application, the customer would enter:
jre -Ddebug=1 MainApplication
java -Ddebug=1 MainApplication
If your application hides the call to the Java Virtual Machine (JVM) (for example, jre, jrew, java, and so on) by launching the JVM from a front-end executable (such as C/C++ code) or from a script, you could externalize the debug variable to your customers in a slightly different manner. Within the logic of your executable or script, you would launch the JVM with the debug variable set as above.
Add more flexibility
You can enhance this basic design in the following ways:
- The value of the customer-supplied debug variable could be the trace output location (such as the system console or a fully qualified file name). This would give the customer flexibility in collecting the trace output, which could then be sent to you and your application development team as a debugging aid.
- Alternatively, the variable could indicate output trace level (such as low, medium, or high). Low and medium tracing could include tracing at specific points in your application, whereas high tracing could also include method entry and exit notifications or loop iteration notification. The definitions and meaning of the levels are up to you.
- If your application was logically divided into components, the debug variable could be used to identify which of your application's components to trace.
Note: You could use the value of a single variable to encompass all of the above information, or you could use separate variables. For example, use one for trace output location, one for output trace level, and one for trace component.
See this sample code that implements this technique by outputting the trace statements to a file. In this sample, if the debug variable is specified, its value is interpreted as the fully qualified file name.
In practice, more error checking and error recovery should be added to the file I/O section. Additionally, if you anticipate a large volume of trace statements, implement a file limit size. One strategy is to purge a block of the oldest statements in the file, making room for newer ones to be appended, once the file size limit is reached. Taking the technique one step further, you can externalize your trace capabilities to your customer through your application's user interface. Your interface could allow the user to choose trace granularity, trace output location, file name, file limit, file truncation mechanism, and so on.
Using a development-time trace technique can be an effective debugging aid during the application development process. Once the application is deployed, using a run-time trace technique, which allows customers to send trace output back to the development team, can also be an effective debugging aid. The techniques presented here relieve developers of the need to design and implement their own tracing strategies, allowing them to concentrate on their application's main logic.
|About the author|
Andrei Malacinski is a software engineer in the WebSphere Tools Development area. He works at the IBM Application & Integration Middleware Lab in Research Triangle Park, NC. Andrei received a B.S. degree in Computer Science from Indiana University in 1990. Since then, he has been both a developer and team leader on a number of IBM application development products for Windows, OS/2, and UNIX platforms. Andrei can be reached at firstname.lastname@example.org.