Implementing a Custom Logger in Mach-II, Part 3

This is the last of a three-part series. For the first and second parts, click here and here.

When we left off last time we had a nearly functioning logger; we were just missing the bits that actually output the logging information to IRC. Let’s jump right in and fill out the onRequestEnd callback and get those logs dumped correctly:

  <cffunction name="onRequestEnd" access="public" returntype="void" output="false"
	      hint="Sends an email for this logger.">
 
    <cfset var data = ArrayNew(1) />
    <cfset var messageString = "" />
    <cfset var i = 0/>
 
    <!--- Only display output if logging is enabled --->
    <cfif getLogAdapter().getLoggingEnabled() AND getLogAdapter().isLoggingDataDefined()>
 
      <cfset data = getLogAdapter().getLoggingData().data />
 
      <cfif ArrayLen(data)>
	<!--- Send to the channel --->
	<cfloop index="i" from="1" to="#ArrayLen(data)#">
	  <cfset messageString = javaCast("string", data[i].channel & ": " & data[i].message)/>
	  <cfset sendMessage(messageString)/>
	</cfloop>
      </cfif>
    </cfif>
  </cffunction>
 
  <cffunction name="sendMessage" access="private" returntype="void" output="false">
    <cfargument name="message" type="string" required="true"/>
    <cfset var messageCommand = createObject("java", "f00f.net.irc.martyr.commands.MessageCommand").init(getChannel(), arguments.message)/>
    <cfset getBot().sendCommand(messageCommand)/>
  </cffunction>

A very straightforward callback. A vast majority of the time your onRequestEnd callbacks will look exactly like this one, with the only difference being what’s within the cfloop. The method simply checks to see whether logging is enabled, and if so it loops over all the logging data available, sending each line to the IRC connection through sendMessage. If you want to see what values are available to play around with in the data array, check out the MachIILog’s output template in the source code. It’s a simple page that’s pretty easy to understand.

And with that, we have a functioning logger. Let’s look at the configuration in mach-ii.xml:

    <property name="Logging" type="MachII.logging.LoggingProperty">
      <parameters>
	<parameter name="IrcLog">
	  <struct>
	    <key name="type" value="project.loggers.IrcLogger" />
	    <key name="loggingEnabled" value="true" />
	    <key name="loggingLevel" value="info" />
	    <key name="server" value="irc.server.net"/>
	    <key name="channel" value="#debugging"/>
	    <key name="nick" value="project[bot]"/>
	    <key name="filter" value="*,!MachII*"/>
	  </struct>
	</parameter>
      </parameters>
    </property>

Notice server, channel, and nick? Those were the parameters we configured in Part 2. This is where we give those parameters values. Type points to the logger CFC we’ve been writing, and filter sets up some of the filtering rules mentioned in Part 2.

For more on filtering, read how to use the GenericChannelFilter on the wiki; if you want to set up multiple loggers, check out the multiple logger section on the same page.

Okay, we’re set up and running, but we’re not handling redirects yet. Redirects simply require that the logging data accrued prior to the redirect be persisted and then restored into the new logging data once the redirect is finished. This requires we fill out preRedirect, postRedirect, and a convenience method called arrayConcat:

  <cffunction name="preRedirect" access="public" returntype="void" output="false"
	      hint="Pre-redirect logic for this logger.">
    <cfargument name="data" type="struct" required="true"
		hint="Redirect persist data struct." />
 
    <cfif getLogAdapter().getLoggingEnabled() AND getLogAdapter().isLoggingDataDefined()>
      <cfset arguments.data[getLoggerId()] = getLogAdapter().getLoggingData() />
    </cfif>
  </cffunction>
 
  <cffunction name="postRedirect" access="public" returntype="void" output="false"
	      hint="Post-redirect logic for this logger.">
    <cfargument name="data" type="struct" required="true"
		hint="Redirect persist data struct." />
 
    <cfset var loggingData = StructNew() />
 
    <cfif getLogAdapter().getLoggingEnabled() AND getLogAdapter().isLoggingDataDefined()>
      <cftry>
	<cfset loggingData = getLogAdapter().getLoggingData() />
	<cfset loggingData.data = arrayConcat(arguments.data[getLoggerId()].data, loggingData.data) />
	<cfcatch type="any">
	  <!--- Do nothing as the configuration may have changed between start of
	      the redirect and now --->
	</cfcatch>
      </cftry>
    </cfif>
  </cffunction>
 
  <cffunction name="arrayConcat" access="private" returntype="array" output="false"
	      hint="Concats two arrays together.">
    <cfargument name="array1" type="array" required="true" />
    <cfargument name="array2" type="array" required="true" />
 
    <cfset var result = arguments.array1 />
    <cfset var i = 0 />
 
    <cfloop from="1" to="#ArrayLen(arguments.array2)#" index="i">
      <cfset ArrayAppend(result, arguments.array2[i]) />
    </cfloop>
 
    <cfreturn result />
  </cffunction>

There’s a lot going on here, but don’t panic! Most of the time you’ll be able to copy/paste these methods directly into your logger and it will just work. The only time that might not be true is if you implement your own custom Adapter. Functionally, both methods check to make sure logging is enabled, and then either persist or restore the logging data through the registered logging adapter by assigning or saving the data from a data array passed in as an argument.

There’s one more method before we wrap up this little tutorial: getConfigurationData. As far as I know, this method is just a way to let the Dashboard module (which if you’re not using, you should really look into) know some information about your logger. For this logger, I implemented it like so:

  <cffunction name="getConfigurationData" access="public" returntype="struct" output="false"
	      hint="Gets the configuration data for this logger including adapter and filter.">
 
    <cfset var data = StructNew() />
 
    <cfset data["Server"] = getServer() />
    <cfset data["Channel"] = getChannel() />
    <cfset data["Nick"] = getNick() />
    <cfset data["Logging Enabled"] = YesNoFormat(isLoggingEnabled()) />
 
    <cfreturn data />
  </cffunction>

And that’s it! We now have a perfectly working IRC-based logger for the Mach-II framework. Finally I can just lounge in my favorite IRC channels and know instantly when a problem happens with my web applications. It’s also pretty handy to include a call to getLog().info() when some interesting function I wrote goes off, just to watch it run.

If you’d like to see the whole file, you can download it here.

As always, if you have questions or concerns, leave a comment. As always, I’m going to enjoy a post-entry glass of bourbon.

Leave a Reply

Powered by WP Hashcash