Home Articles OSGi Matters Simple OSGi Logging
Logging OSGi Applications - The Simple and Robust way Print E-mail
Written by Valery Abu-Eid   
Tuesday, 23 June 2009 22:14

Observing different approaches for logging OSGi-based applications for few months, I came across different articles on the topic. Although they explain how to use the OSGi Log Service pretty well, all in all, I consider the general approach for logging OSGi-based applications using OSGi Log Service is too complex. The complexity is driven by the fact that OSGi is a dynamic environment, and OSGi Log Service, like any other service, could become available or unavailable at runtime. Sure, these concerns need to be addressed, but my point here is that this should not be the responsibility of the developer.

The rest of this article explains what do you need to know about logging in OSGi and how you can use the OSGi Log service properly without adding any complexity to your code at all. Working on tens of OSGi-based applications I kept evolving my approach, so it's important to mention that the approach presented here is not only simple, but also has proved to work in different situations (even in cases where Log Service API packages are installed later than the bundles which consume them).

How is logging done for OSGi-based applications?

The OSGi Services Compendium contains Log Service specification which provides OSGi developers with a standard API for logging OSGi-based applications. The implementation of the OSGi Log Service is provided by different vendors/communites like Eclipse, Apache Felix and Knopflerfish. So what you will have usually is two bundles, one which contains the API of the Log Service, the other contains the implementation.

Why the OSGi Log Service should be your default option?

Mainly two reasons:

  • OSGi Log Service is designed to be used dynamically: Since OSGi is a dynamic environment, it's only natural to give both vendors and developers the opportunity to provide and update the logger at runtime. OSGi Log Service benefits from the capabilities provided by the OSGi Environment to allow the dynamic use of logging utilities.
  • Intended for both embedded and desktop applications: OSGi is designed for both embedded and desktop applications, and all of the implementations of OSGi Log Service take into account this aspect. It might not be important for you if you develop a client or server-side application. But if you develop a bundle that will be provided to other developers then you want to make sure that the logging utility used by your bundle can be run anywhere, otherwise you limit the scope of use of your bundle.

What is the main difficulty of using the OSGi Log Service?

The OSGi Log Service is provided as an OSGi Service. As such, it can't be simply instantiated in a single line of code like the loggers of any other logging utility. OSGi Services need to be watched for availability, unavailability and updates. Someone might say that the binding of the service can be done using Spring-DM or any other OSGi Service Binding solution. But the idea of having to bind an OSGi service for every class which uses a logger brings me the creep.

Using the OSGi Log Service - the simple and robust way

Instead of logging through the OSGi Log Service directly, you can initialize a log forwarder at the beginning of the application (from the activator of a configurator bundle for example) that will forward logs from the standard JDK logger to the OSGi Log Service (if such is available), then use the JDK Logging API instead of the OSGi Log Service. The benefits of this approach are:

  • Simplicity: You can always use the the JDK loggers within a single line of code without having to watch for services or taking care of binding them, and you don't have to import any packages or use any additional logging utility to your application.
  • Robustness: Having the code that uses the OSGi Log Service centralized in a one place, you can easily increase its quality by dedicating a plenty of time for it since it you have to write it once (actually it's already written for you).
  • Reusability: Since the code which is responsible for forwarding logs is centralized in one independent class (Log Forwarder), you can always reuse this solution by adding the Log Forwarder to your application and activate it from the Bundle Activator.

Here is an example code which uses the JDK Log Forwarder from the Bundle Activator:

import java.util.logging.ConsoleHandler;
import java.util.logging.SimpleFormatter;
import org.osgi.framework.*;

public class Activator implements BundleActivator {
	
	//@Override
	public void start(BundleContext bundleContext) throws Exception {
		/// The JDK Log Forwarder takes a default log handler as an argument which
		/// will be used in the case the OSGi Log Service was not available.
		/// The handle argument is optional.
		ConsoleHandler defaultHandler = new ConsoleHandler();
		defaultHandler.setFormatter(new SimpleFormatter());
		
		/// Other than the default handler we pass the package name of the
		/// classes whose JDK loggers must forward logs to the OSGi Service.
		jdkLogForwarder = new JdkLogForwarder(bundleContext,
				new String[] { "org.dynamicjava.examples.jdk_log_delegate" },
				defaultHandler);
		
		/// We start the Log Forwarder
		jdkLogForwarder.start();
		
		/// Now you can use the JDK Loggers and have the logs forwarded
		/// to the OSGi Log Service
		Logger logger = Logger.getLogger(getClass().getName());
		logger.info("This log will be forwarded to the OSGi Log Service.");
	}
	
	//@Override
	public void stop(BundleContext bundleContext) throws Exception {
		jdkLogForwarder.stop();
	}
	
	private JdkLogForwarder jdkLogForwarder;
}
	

Below is the code of the JDK Log Forwarder. Please note that the code is written in a way which makes the code work even when the "org.osgi.service.log" package is not available, and if the package becomes available later, the forwarding is done. Using the forwarder in your bundle and importing the OSGi Log Service packages dynamically (using the DynamicPackage-Import header) allows you to provide a bundle which doesn't force its users to use OSGi Log Service.

import java.util.logging.*;

import org.osgi.framework.*;
import org.osgi.service.log.LogService;

public class JdkLogForwarder {
	
	public void start() {
		for (String loggerName : getLoggerNames()) {
			Logger logger = Logger.getLogger(loggerName);
			logger.setUseParentHandlers(false);
		}
		
		updateLogHandler();
		
		addLogServiceListener();
	}
	
	public void stop() {
		getBundleContext().removeServiceListener(getLogServiceServiceListener());
		
		for (String loggerName : getLoggerNames()) {
			Logger.getLogger(loggerName).removeHandler(getLastUsedHandler());
		}
		
		if (getLastUsedLogServiceRef() != null) {
			getBundleContext().ungetService(getLastUsedLogServiceRef());
		}
	}
	
	
	protected void updateLogHandler() {
		ServiceReference logServiceRef = getBundleContext().getServiceReference(
				LOG_SERVICE_CLASS_NAME);
		Handler logHandler = null;
		
		if (logServiceRef != null && logServiceRef == getLastUsedLogServiceRef()) {
			return;
		} if (logServiceRef != null) {
			logHandler = new OsgiLogDelegateHandler(
				(LogService)getBundleContext().getService(logServiceRef));
		} else {
			logHandler = getDefaultHandler();
		}
		
		Handler lastUsedHandler = getLastUsedHandler();
		
		for (String loggerName : getLoggerNames()) {
			Logger logger = Logger.getLogger(loggerName);
			logger.removeHandler(lastUsedHandler);
			logger.addHandler(logHandler);
		}
		
		if (getLastUsedLogServiceRef() != null) {
			getBundleContext().ungetService(getLastUsedLogServiceRef());
		}
		
		setLastUsedLogServiceRef(logServiceRef);
		setLastUsedHandler(logHandler);
	}
	
	protected void addLogServiceListener() {
		try {
			getBundleContext().addServiceListener(getLogServiceServiceListener(),
					String.format("(%s=%s)", Constants.OBJECTCLASS,
					LOG_SERVICE_CLASS_NAME));
		} catch (InvalidSyntaxException ex) {
			/// This exception should not occur in the first place
			throw new RuntimeException(ex.getMessage());
		}
	}
	
	
	public JdkLogForwarder(BundleContext bundleContext, String[] loggerNames) {
		this(bundleContext, loggerNames, null);
	}
	
	public JdkLogForwarder(BundleContext bundleContext,
			String[] loggerNames, Handler defaultHandler) {
		this.bundleContext = bundleContext;
		this.loggerNames = loggerNames;
		
		this.defaultHandler = defaultHandler != null
				? defaultHandler : new DummyLogHandler();
	}
	
	private final BundleContext bundleContext;
	protected BundleContext getBundleContext() {
		return bundleContext;
	}
	
	private final String[] loggerNames;
	protected String[] getLoggerNames() {
		return loggerNames;
	}
	
	private final Handler defaultHandler;
	protected Handler getDefaultHandler() {
		return defaultHandler;
	}
	
	private final ServiceListener logServiceServiceListener =
			new LogServiceServiceListener();
	protected ServiceListener getLogServiceServiceListener() {
		return logServiceServiceListener;
	}
	
	private ServiceReference lastUsedLogServiceRef;
	protected ServiceReference getLastUsedLogServiceRef() {
		return lastUsedLogServiceRef;
	}
	protected void setLastUsedLogServiceRef(
			ServiceReference lastUsedLogServiceRef) {
		this.lastUsedLogServiceRef = lastUsedLogServiceRef;
	}
	
	private Handler lastUsedHandler;
	protected Handler getLastUsedHandler() {
		return lastUsedHandler;
	}
	protected void setLastUsedHandler(Handler lastUsedHandler) {
		this.lastUsedHandler = lastUsedHandler;
	}
	
	
	protected class LogServiceServiceListener implements ServiceListener {
		//@Override
		public void serviceChanged(ServiceEvent serviceEvent) {
			try {
				updateLogHandler();
			} catch (Throwable ex) {
				System.out.println("Error: " + ex.getMessage());
				ex.printStackTrace();
			}
		}
	}
	
	protected class OsgiLogDelegateHandler extends Handler {
		@Override
		public void publish(LogRecord record) {
			if (record.getLevel() == Level.OFF) {
				return;
			}
			
			if (record.getThrown() != null) {
				getLogService().log(getToOsgiLogLevel(record.getLevel()),
						record.getMessage(), record.getThrown());
			} else {
				getLogService().log(getToOsgiLogLevel(
						record.getLevel()), record.getMessage());
			}
		}
		
		@Override
		public void close() throws SecurityException {
		}
		
		@Override
		public void flush() {
		}
		
		
		protected int getToOsgiLogLevel(Level level) {
			if (level == Level.SEVERE) {
				return LogService.LOG_ERROR;
			} else if (level == Level.WARNING) {
				return LogService.LOG_WARNING;
			} else if (level == Level.INFO
					|| level == Level.CONFIG || level == Level.FINE) {
				return LogService.LOG_INFO;
			} else {
				return LogService.LOG_DEBUG;
			}
		}
		
		
		public OsgiLogDelegateHandler(LogService logService) {
			this.logService = logService;
		}
		
		private final LogService logService;
		protected LogService getLogService() {
			return logService;
		}
	}
	
	protected class DummyLogHandler extends Handler {
		@Override
		public void publish(LogRecord record) {
		}
		
		@Override
		public void close() throws SecurityException {
		}
		
		@Override
		public void flush() {
		}
	}
	
	protected static final String LOG_SERVICE_CLASS_NAME =
			"org.osgi.service.log.LogService";
	
}
	
The example application which publishes logs with source code can be downloaded from jdklog-delegate-example.zip. Please note that the delegate bundle requires OSGi Log Service packages because of the example code which publishes the logs. The separate java file for the Log Forwarder can be downloaded from JdkLogForwarder.java.