I have spent a lot of time over the course of my career writing ColdFusion code and during that time I have leveraged a lot of tools to monitor the health of my code. Tools for ColdFusion range from complex like Fusion Reactor for realtime application performance monitoring to the mundane with a simple writeDump. One of my favorite ways of debugging is using the old standby, the writeLog function.

Getting to know writeLog

Writelog has been around since ColdFusion 9 and is supported by all the major application engines. The default log file consists of six columns: type, thread, date, time, application, and message. It is super simple to use:

writeLog(type = "error", message = "Oh no! There was an error", file = "my-errors");

This will either append a new log entry to my-errors.log in your servers log file directory if it exists, otherwise it will create the file and then append the message. The log files themselves are straight forward CSV delimited data. The column headers are in the first row and the data follows.

An example log file

The data has been written out, now we just need a way to to make it useful. I will be using the kisdigital-playground repo since a basic application groundwork has already been laid.

component {
 // LogService.cfc
 function readLogfile (required struct criteria) {
  var logfile = criteria.keyExists("logfile") ? criteria.logfile : "";
  var data = {
   'response': {
    'log': []
   },
   'meta_data': {
    'status' = "success",
    'code' = 200,
    'type' = "OK",
    'message' = ""
   },
   'errors': []
  };
  var lines = [];
  try{
   cfhttp(url = logfile);
   if(cfhttp.responseHeader.status_code == "200"){
    var logs = cfhttp.fileContent.listToArray(chr(10));
    // skip the headers
    for(var i = 2; i <= logs.len(); i++){
     // parse the positions for log4j
     var sv = replace(logs[i].listGetAt(1), """", "", "all"); // severity
     var td = replace(logs[i].listGetAt(2), """", "", "all"); // threadid
     var dt = replace(logs[i].listGetAt(3), """", "", "all"); // date
     var tm = replace(logs[i].listGetAt(4), """", "", "all"); // time
     var ap = replace(logs[i].listGetAt(5), """", "", "all"); // application
     var ms = replace(logs[i].listGetAt(6), """", "", "all"); // message
     // strip the message from the log line
     var tmp = logs[i];
     tmp = tmp
      .replace(sv, "")
      .replace(td, "")
      .replace(dt, "")
      .replace(tm, "")
      .replace(ap, "")
      .replace('"","","","","","', "");
     tmp = left(tmp, tmp.len() - 1);
     lines.append({
      'severity': sv,
      'date': dt,
      'time': tm,
      'message': encodeForHTML(tmp),
      'epoch': DateDiff("s", "January 1 1970 00:00", DateConvert("Local2utc", parseDateTime(dt & " " & tm)))
     });
    }
   }
   arraySort(lines, function(e1, e2){
    // descending sort by utc timestamp
    return compare(e2.epoch, e1.epoch);
   });
   // merge in to response.log
   data.response.log.append(lines, true);
  }
  catch(any e) {
   data['errors'].append("Invalid log file format or parsing error");
   data['meta_data']['code'] = 500;
   data['meta_data']['status'] = "error";
   data['meta_data']['type'] = "error";
  }
  return data;
 }

}
LogService.cfc

The readLogfile reads the contents of the requested log file using cfhttp and then  split into lines using listToArray on the fileContent using char 10 (new line) as the delimiter. Next, the log file attributes are appended to the lines array along with the epoch value that is derived from the date and time. Finally the array is sorted by epoch time in descending order and returned.

The next step is to create a simple view to output the data. In the Demo.cfc controller I am creating a new readLog handler to call the LogService and save the output to the logs variable in the private request context.

 function readLog (event, rc, prc) {
  prc['logFile'] = "https://raw.githubusercontent.com/robertz/payloads/master/links.log";
  prc['logs'] = LogService.readLogfile({logfile: prc.logFile}).response.log;
  event.setView('demos/logs');
 }
readLog handler in Demos.cfc

To simplify things I have created an example log file in my payloads repo. There may be an easy way to expose the log directory in CommandBox, but I do not know offhand how to do it.

Logfile output

It is also possible to view the contents of the log file as an API JSON response.

Viewing log data as an API call

I suppose the biggest caveat of leveraging this service, the log files have to be in a location readable from ColdFusion. However, when it does work it works really well.

Wrapping up

If you are only running a single instance of ColdFusion this might not be all that useful, but what if your code base is running on 15 instances behind a load balancer? If you needed to find an item in the log you could open the log file on each instance but that is time consuming. The quicker option would be to scan each instance for your log file, combine all the results, sort it by time, and output the results. The output could be formatted on the screen using ColdFusion or your favorite javascript framework. Personally I use VueJS to allow me to easily search messages for specific values and to easily filter by severity.

I would be interested to hear how you handle debugging applications. Please feel free to share in the comments below!

robertz/kisdigital-playground
Contribute to robertz/kisdigital-playground development by creating an account on GitHub.