devxlogo

Build a Distributed Logging Framework Using Java RMI

Build a Distributed Logging Framework Using Java RMI

or years, “System.out.println” seemed to be the only choice Java developers had for logging; there was no standard logging API in the Java SDK and no widely accepted third-party logging API. However, Java developers looking for a better way to do logging will be cheered by the recent release of the J2SE 1.4 logging API.

J2SE 1.4 offers a simple yet flexible logging framework that Java developers can customize for their application logging needs. Figure 1 shows the basic logging process and the J2SE classes involved.

Logger
Logger is the primary interface for a Java logging application and in most cases it will be the only class an application developer has to deal with. Every log request is made through a Logger object, which maintains the same namespace hierarchy as a regular Java object. A logger of the child namespace will inherit all the logging properties from a logger of its parent namespace. For instance, a logger of namespace “com.foo.bar” will have the same logging level, handler, and formatter as the logger of namespace “com.foo.”

LogRecord
LogRecord is the internal representation of a log request that is used within the logging framework. Most application developers will not deal with this class directly. LogRecord implements the Java Seriliazable interface.

Handler
The Handler object takes a LogRecord from Logger and publishes it to the external medium. The Handler implementations that come with J2SE can publish LogRecord to System.err, OutputStream, disk file, etc.

Some classes of the J2SE logging API are not shown in Figure 1, such as Formatter, which is used to format each LogRecord in the application-specific form or locale. In addition, Level is used to specify the log level at which a log request should be made. It also implements the Java Serializable interface.

Figure 1: This architectural diagram shows the basic logging process and the J2SE classes involved.

The default implementation of the J2SE logging API may be sufficient for the logging needs of most Java applications. But what if you are logging under a distributed and multithreaded environment? In these situations, you’ll have to answer the following questions:

  • Where should I store the logs?locally or on a centralized network server?
  • How should my application access logs from different machines in the network?
  • How do I control log levels remotely at runtime to increase or decrease the number of log requests?
  • Should I log by package/class like the J2SE logging API does, or should I make logs based on a disparate Java thread?

In this article, I’ll show you how to build a logging framework for a distributed and multithreaded Java application.Log Locally vs. Remotely
Whether to log locally or remotely is a question that has no pat answer. The decision varies with each project. You need to consider a number of factors, such as network environment, hardware configuration, and software set up. For example, if you were developing an application for an embedded system with very limited system resources, it would make sense to have log requests come out of a network connection and be handled by a host with more capability.

The rule of thumb is to use the method that will have the least impact on the overall system performance. This is vitally important in a time-critical system; the last thing you want is for logging to become a bottleneck.

You may also want to design your application to avoid having a single point of failure. I have seen systems designed to have one centralized log server, where all the satellite hosts dump log requests to it. In this configuration, when network traffic becomes overloaded, the log server becomes a single point of failure for the entire system.

The logging framework in this article uses an in-memory buffer local to each network host as well as a runtime utility to access and control logs remotely, thus neutralizing these performance and reliability issues.

The In-Memory Log Handler
The in-memory log handler implements the Handler interface defined in the J2SE logging API and uses a ring buffer to hold log records. Whenever the buffer is full, the oldest log record is discarded. Listing 1 shows the code for MemLogHandler.

The log handler invokes the publish() function only if a new log needs to be recorded. Because log filtering is done by the Logger object, the log handler doesn’t need to decide if a request should be logged or not. The inherited close() and flush() functions are mainly used by a log handler that uses buffered streams. It needs an opportunity to flush the remaining log records in the stream before the handler itself is torn down, say, by garbage collection when the application is terminated. The getRecords() and removeAllRecords() functions are called when log records in the buffer need to be reported or deleted.

Remote Log Access and Runtime Log-level Control
Because logs reside on each host, I need a way to remotely access these logs and, in addition, a method to control the logging levels for a host application. Java RMI is a natural choice under this circumstance, as it hides the low-level network communication details from the application logic.

The following code gives the definition of the LogServer RMI interface that each host application needs to implement for remote logging access and control.

   public interface LogServer extends Remote {     public LogRecord[] read()       throws java.rmi.RemoteException;        public void clear()       throws java.rmi.RemoteException;        public void setClassLogLevel(String className, Level logLevel)       throws java.rmi.RemoteException;        public Level getClassLogLevel(String className)       throws java.rmi.RemoteException;        public void setThreadLogLevel(String threadName, Level logLevel)       throws java.rmi.RemoteException;        public Level getThreadLogLevel(String threadName)       throws java.rmi.RemoteException;   }

The read() method returns all the existing log records. The clear() method removes all the log records. The setClassLogLevel()/getClassLogLevel() methods configure/query a class or package’s current logging level. The setThreadLogLevel()/getThreadLogLevel() methods configure/query a thread’s current logging level (I will address logging by thread in the next section).

As mentioned previously, LogRecord and Level can be used in this remote interface because they are both serializable.

LogServerImpl implements the LogServer interface; most of its methods are straightforward. Here is the source code for all the methods except those that are thread-logging related:

   public class LogServerImpl extends UnicastRemoteObject    implements LogServer {   ...     public LogRecord[] read() throws RemoteException {       return memHandler.getRecords();     }        public void clear() throws RemoteException {       memHandler.removeAllRecords();     }        public void setClassLogLevel(String className,          Level logLevel) {       Logger.getLogger(className).setLevel(logLevel);     }        public Level getClassLogLevel(String className) {       Level level =           Logger.getLogger(className).getLevel();       return (level == null) ? Level.OFF : level;     }     public void logByClass(Class cls,        Level level, String msg) {       Logger logger = Logger.getLogger(cls.getName());       logger.log(level, msg);     }   ...   }

What I need to do next is expose the interface through the RMI registry. The following code shows the start() function of the LogServerImpl class, which exposes the interface in RMI.

     public void start() {       // Create and install a security manager       if (System.getSecurityManager() == null) {         System.setSecurityManager(            new RMISecurityManager());       }          try {         // Bind this object to the name          // LogServer.SERVICE_NAME         Naming.rebind(LogServer.SERVICE_NAME, instance);       } catch (Exception e) {         e.printStackTrace();       }     }

LogShell is a command-line utility for log access and control on a remote host. LogShell can talk to the host application only after the LogServer interface is published through RMI, so for it to work properly, each host application must invoke the start() function before making any logging requests. Figures 2 and 3 show LogShell being used to view log records and change log levels, respectively.

Figure 2: Here, LogShell is used to view and clear log records from a remote host.
Figure 3: LogShell changes and checks logging level for a class on a remote host.

Logging by Thread
Another feature you can add to this logging framework is logging by thread. It will be useful when you are writing a multi-threaded Java application and need to track the call flow for a particular thread. This feature is very handy compared to J2SE class logging, especially if the classes used by the thread are not from the same namespace hierarchy.

The following code block creates a logging-by-thread implementation through the setThreadLogLevel() and logByThread() methods of LogServerImpl. The idea is pretty simple: First, you need to assign each thread a namespace based on its own thread name. Then you associate each thread namespace with a logger of the same namespace. By intentionally separating a thread namespace with regular class namespace you get a logger for that particular thread only.

     public void setThreadLogLevel(String threadName,        Level logLevel) {       String tName = THREAD_PREFIX + threadName;       Logger logger = (Logger)loggerHash.get(tName);       if (logger == null) {         logger = Logger.getLogger(tName);         loggerHash.put(tName, logger);       }          if (logLevel.equals(Level.OFF)) {         loggerHash.remove(tName);       } else {         logger.setLevel(logLevel);       }     }        public void logByThread(Level level, String msg) {       String tName = THREAD_PREFIX +           Thread.currentThread().getName();       Logger logger = (Logger)loggerHash.get(tName);       if (logger != null) {         logger.log(level, msg);       }     }

Taken as a whole, the logging framework demonstrated in this article could be useful in debugging your next distributed and multithreaded Java application. There are improvements you can make on this framework. For instance, you could enhance the in-memory log handler to send log records over SOAP to a web server, where an administrator can monitor the system remotely.

With the new J2SE logging API, the outlook for Java application logging is far more positive than it ever has been before.

devxblackblue

About Our Editorial Process

At DevX, we’re dedicated to tech entrepreneurship. Our team closely follows industry shifts, new products, AI breakthroughs, technology trends, and funding announcements. Articles undergo thorough editing to ensure accuracy and clarity, reflecting DevX’s style and supporting entrepreneurs in the tech sphere.

See our full editorial policy.

About Our Journalist