Skip to main content
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jake Scott
Ben Nadel at cf.Objective() 2013 (Bloomington, MN) with: Jake Scott

Porting Lucee CFML's SystemOutput() Function Over To Adobe ColdFusion

By on
Tags:

In my previous post on dynamically extending a distributed lock in Redis, Paolo Olocco asked about some Lucee CFML / Adobe ColdFusion compatibility issues; one of which was how to implement the SystemOutput() function. At InVision, writing to the standard out and error streams is a very important action: we use SystemOutput() to write all of our logs to the these streams where they are then aggregated and shipped off to Loggly. As such, I thought it would be a fun code kata to try and port the SystemOutput() function over to Adobe ColdFusion.

The SystemOutput() function is fairly light-weight: it takes a value and writes it to either the standard out or the standard err stream. Then, it optionally adds a newline character. The inclusiong of a newline character is critical as log aggregation translates one entire line of output to one log-entry.

In Lucee CFML, complex objects get serialized automatically. For this, I am going to use the serializeJson() function. Lucee CFML will also replace a few special tokens into the serialized value: <print-stack-trace> and <hash-code>. I am not porting this "magic string" functionality over as I've never used it and I don't fully understand what the point of it is.

With that said, here's my attempt at an Adobe ColdFusion compatible SystemOutput() function:

<cfscript>

	ADD_NEW_LINE = true;
	USE_ERROR_STREAM = true;

	// Simple values can be logged.
	SystemOutput( "Hello world", ADD_NEW_LINE );
	SystemOutput( true, ADD_NEW_LINE );
	SystemOutput( 1234, ADD_NEW_LINE );

	// Complex values can be logged and will be serialized automatically as JSON.
	SystemOutput(
		{
			id: 4,
			name: "Sarah",
			phone: "212-555-1234"
		},
		ADD_NEW_LINE
	);

	try {

		throw( type = "OhNoYouDidnt!" );

	} catch ( any error ) {

		// Values can be logged to the error stream.
		SystemOutput( "Ooops: #error.type#", ADD_NEW_LINE, USE_ERROR_STREAM );

	}

	// ------------------------------------------------------------------------------- //
	// ------------------------------------------------------------------------------- //

	/**
	* I port (most of) the SystemOutput() functionality over to Adobe ColdFusion. If the
	* passed-in object is a complex value, it will be serialized as JSON.
	* 
	* CAUTION: Does not support "<print-stack-trace>" replacement.
	* 
	* @obj I am the value being logged.
	* @addNewLine I determine if a newline character will be appended.
	* @doErrorStream I determine if the error output should be used.
	*/
	public void function SystemOutput(
		required any obj,
		boolean addNewLine = false,
		boolean doErrorStream = false
		) {

		var SystemClass = createObject( "java", "java.lang.System" );

		var stream = ( doErrorStream )
			? SystemClass.err
			: SystemClass.out
		;
		var streamMethod = ( addNewLine )
			? "println"
			: "print"
		;

		var serializedValue = ( isSimpleValue( obj ) )
			? toString( obj )
			: serializeJson( obj )
		;

		invoke( stream, streamMethod, [ serializedValue ] );

	}

</cfscript>

Here, we're ultimately dipping down into the Java layer and grabbing either the standard out or standard err streams from the java.lang.System Class. Then, we're either invoking the print() function by default; or, the println() function when we need to include a new line.

ASIDE: I love that we can use invoke() to dynamically execute methods on Java objects - ColdFusion is so freaking flexible!

And, when we run this CFML code with Adobe ColdFusion 2018 in CommandBox, we get the following server log --follow output:

SystemOutput() function writing values to the server output streams.

Given the fact that more applications are moving to a containerized architecture; and, that many containerized applications are aggregating logs using the server's output stream, I'm be shocked if Adobe ColdFusion didn't add its own native SystemOutput() function in a future release. But, until that happens, hopefully this port might help.

Epilogue: Linking Log Files to Output Streams

Back when InVision was running on Adobe ColdFusion 10, we used to have our Dockerfile create "symbolic links" (aka, symlinks) between the server's output stream(s) and ColdFusion's log files. This way, anything that was logged either implicitly by the runtime or explicitly using writeLog() would automatically be piped to the output stream (where it was then consumed by our log aggregation).

This approach worked, but it was always kind of janky. For one, it felt a bit too "magical". And, you could never open the logs up in the Adobe ColdFusion Administrator's log-viewer since the files weren't really there - they were just symlinked to streams. As such, even before we switched to Lucee CFML we ended up replacing the symlinks approach with a SystemOutput() port similar to what I have in this post.

Want to use code from this post? Check out the license.

Reader Comments

426 Comments

Ben

It would be great if we could remove the need for:

doErrorStream

Argument

Is there anyway we can detect an error automatically, inside your customised SystemOutput?

I have found an interesting Java class:

java.util.Thread.UncaughtExceptionHandler

Apparently, this can detect whether an error has been thrown globally!

15,674 Comments

@Charles,

It's an interesting idea. I can tell you that from my standpoint, where I am using the output streams for log aggregation, the type of stream - out vs err - don't actually make a difference. This is because we are using "structured logging", where we're outputting JSON object that contain the "level" of the log entry. So, if we output something like:

{"level": "warning", ....}

... it doesn't matter if it is written to the out stream or the err stream - it just gets slurped-up and sent off to Loggly where it gets indexed based on level.

I say all this only to say that trying to be "too clever" inside the SystemOutput() function may be overkill for how people end up using it. Of course, I can only speak from my own usage perspective.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel