Brian Love
Angular + TypeScript Developer in Denver, CO

Logger Service for ColdFusion

Reading time ~6 minutes

Simple implementation using cfscript and object-oriented ColdFusion to create a logger service layer. The logger service will be logging to the ColdFusion log files. But, you can extend this example to implement a new Logger class (component) that logs to a database or sends an email when an error occurs, or whatever your project requires. This simple example will compose several classes:

  • LoggerService that implements IService
  • Logger that implements ILogger
  • LogLevel
  • ColdFusionLogLogger that extends Logger

Service Layer

Logger Service class diagram

To get started, lets look at the service layer. The service layer is the public interface that will be used by the rest of your application to log out statements. The logging service has 4 methods that differentiate the level of logging:

  • debug()
  • info()
  • warn()
  • error()

These are very common in all logging service layers (for example, the Java Log4j library). Our application can “listen” for logging statements at the required level. For example, when developing locally, you will want all levels of logging to occur; including the debug level so that all debug statement are logged. Conversely, when your code is in production, you do not want to stuff you log files full of debug output, so you would likely set the level to “warn” or “error”, depending on your preferences and project requirements.

OK. Let’s look at the LoggerService. Wait – before we do that we should real quickly look at the IService interface. This simply states what public methods all service classes (components) must implement. You may not even need this, but my code depends on each service having a getServiceName() method, that returns a friendly string that describes the service. This is useful for throwing exceptions and logging in any service.

interface singleton {
  variablename function getServiceName();
}

Notice I also have an attribute called singleton, which is there to indicate that all service layers must be singleton classes. I am using WireBox for dependency injection, so each service layer will only be instantiated by WireBox once, and then any future injections of the service layer will come from the cached instance.

OK, now let’s start looking at the LoggerService. Here is an outline of what the LoggerService does.

  • Stores an array of Logger instances (that implement the ILogger interface)
  • Calls the debug(), info(), warn() and error() methods in each logger
  • Having multiple loggers allows me to log out to the CF file log, log to a database, and also send emails (if necessary)
  • The addLogger(required com.brianflove.services.loggers.Logger logger) method will be called when my application is initialized, and will add each of the loggers necessary. In my application, the loggers that are added depend on the configuration of the application (development/staging/production).
  • Uses CF property and accessors to store the instance variables (the array of loggers).

The LoggerService will have the following public methods:

  • public void addLogger(required com.acuity.services.loggers.Logger logger)
  • public voiddebug(required string message)
  • public voidinfo(required string message)
  • public voidwarn(required string message)
  • public voiderror(required string message, any exception={})

Logger and ILogger

Logger class diagram

The Logger class will implement the ILogger interface, which mandates that all Loggers have the necessary public methods to be a logger (and to be used by the LoggerService). All loggers will extend the parent Logger class, which will determine the logging level and whether or not the debug/info/warn/error statement should be logged.

Again, before we dive into the Logger class, let’s look at the simple ILogger interface.

interface {
  void function debug(required string message);
  void function info(required string message);
  void function warn(required string message);
  void function error(required string message, any exception);
}

The interface simply states that all of loggers must implement the various levels of logging.

The Logger class is going to store a numeric representation of our logging level, and will have the ability to set the logging level, as well as determining if the various levels are enabled:

  • public void setLogLevel(required com.brianflove.services.loggers.LogLevel logLevel);
  • private boolean isErrorLevelEnabled();
  • private boolean isWarnLevelEnabled();
  • private boolean isInfoLevelEnabled();
  • private boolean isDebugLevelEnabled();

The “is” methods return a boolean value, and are used by the individual loggers to determine if the appropriate level is enabled. The setLogLevel() is called when our application is initialized. Now, let’s quickly look at the LogLevels, which are constant classes.

LogLevel

The LogLevel class holds instances of each log level, represented by an integer value between 1 and 4.

  • 1 = ERROR
  • 2 = WARN
  • 3 = INFO
  • 4 = DEBUG

If we have set the log level to DEBUG, then all levels are enabled (1-4). If we set the log level to INFO, then all error, warn and info statements are logged (1-3). The same continues for WARN. If we set the log level to ERROR, then only error statements are logged.

component accessors="true" {

	property type="numeric" name="level";
	property type="LogLevel" name="ERROR";
	property type="LogLevel" name="WARN";
	property type="LogLevel" name="INFO";
	property type="LogLevel" name="DEBUG";

	/**
	* @hint Constructor
	*/
	public LogLevel function init(numeric level=0) {
		if (arguments.level) {
			setLevel(arguments.level);
		}
		return this;
	}

	/**
	* @hint Return the ERROR LogLevel
	*/
	public LogLevel function getError() {
		if (IsNull("ERROR")) {
			variables.ERROR = new LogLevel(1);
		}
		return variables.ERROR;
	}

	/**
	* @hint Return the WARN LogLevel
	*/
	public LogLevel function getWarn() {
		if (IsNull("WARN")) {
			variables.WARN = new LogLevel(2);
		}
		return variables.WARN;
	}

	/**
	* @hint Return the INFO LogLevel
	*/
	public LogLevel function getInfo() {
		if (IsNull("INFO")) {
			variables.ERROR = new LogLevel(3);
		}
		return variables.INFO;
	}

	/**
	* @hint Return the DEBUG LogLevel
	*/
	public LogLevel function getDebug() {
		if (IsNull("DEBUG")) {
			variables.DEBUG = new LogLevel(4);
		}
		return variables.DEBUG;
	}

}

The LogLevel component stores the level for each logging level, as well as each logging level itself. To get a better understanding of how the LogLevel is used, let’s first look at how we will set the logging level for our application. When the application is initialized, you will want to configure your logging service

/**
* @hint Configure the logger service
*/
package void function configureLoggerService() {
	var coldFusionLogLogger = application.wirebox.getInstance("com.brianflove.services.logging.loggers.ColdFusionLogLogger");
	var logLevel = application.wirebox.getInstance("com.brianflove.services.logging.LogLevel");
	logLevel.setLevel(logLevel.getDebug().getLevel());
	coldFusionLogLogger.setLogLevel(logLevel);
	getLoggerService().addLogger(coldFusionLogLogger);
	getLoggerService().debug("LoggerService started.");
}

After getting an instance of the LogLevel class, I set the log level to DEBUG, and then set the LogLevel instance into my logger. To get the debug logging level, I need to first get the debug LogLevel, via logLevel.getDebug(). Then, I need to get the numeric level, stored within the debug LogLevel via getLevel().

That covers the architecture of the logging service I use, but now let’s actually implement a logger that will be used by the application. In this example, I am showing a logger that will write out log statements to the ColdFusion application log using the WriteLog() method in ColdFusion. You can implement more Loggers that meet your projects needs.

ColdFusionLogLogger

First of all, our logger will extend the Logger class. It will also store the name of the log file that we are logging to. Lastly, in order to meet the ILogger interface requirements, it will implement the debug, info, warn and error methods using the signatures described previous (in the ILogger section above). Let’s just look at the debug method, as the rest are all pretty much the same.

public void function debug(required string message) {
	if (!isDebugLevelEnabled()) {
		return;
	}
	writeLog(arguments.message, "debug", true, variables.fileName);
}

It’s pretty simple. First, we check if the level is enabled, and then we do the actual logging. In this logger, we are logging out the message using the WriteLog() method available in ColdFusion, and we are specifying the log level and log file name.

Source Code

Download

Brian Love

Hi, I'm Brian. I am interested in TypeScript, Angular and Node.js. I'm married to my best friend Bonnie, I live in Denver and I ski (a lot).