Dynamically Instrumenting ColdFusion Component Methods With FusionReactor Tracked Transactions In Lucee CFML 5.2.9.40
One of the really fun features of ColdFusion is its highly dynamic nature. Whether you're using onMissingMethod()
or using getFunctionCalledName()
or injecting methods, you can basically make your ColdFusion code do anything that you want it to. In celebration of this flexibility, I wanted to have some fun with my FusionReactor helper component, and see if I could dynamically add FusionReactor instrumentation (in the form of "tracked transactions") to a ColdFusion component at runtime in Lucee CFML 5.2.9.40.
DISCLAIMER: Just because ColdFusion is a highly dynamic language, it doesn't necessarily mean that you should be using all of these language features. Often times, the most clever code becomes the code that is hardest to maintain in the long-run. In reality, you should strive for boring code that everyone can understand.
To explore this idea, I created a very silly ColdFusion component that has a variety of public and private methods. Within these various methods, I am including nested method calls that are executing both with and without explicit scoping. I added all of this complexity to make sure that my "proxy" logic handles the various ways in which a developer may have wired things together:
component
output = false
hint = "I provide a sample component on which to try annotating methods."
{
public any function init( required any javaAgentHelper ) {
// This component is going to ask the JavaAgentHelper to add instrumentation to
// all of the Public and Private methods. This will wrap them in "tracked
// transactions", which I'm calling "Segments" (a hold-over from New Relic).
javaAgentHelper.annotateMethods( variables );
}
// ---
// PUBLIC METHODS.
// ---
public numeric function test() {
sleep( randRange( 10, 50 ) );
// Testing with and without scoping.
this.publicMethodA();
variables.privateMethodA();
publicMethodB();
privateMethodB();
return( getTickCount() );
}
public void function publicMethodA() {
sleep( randRange( 10, 50 ) );
publicMethodC();
}
public void function publicMethodB() {
sleep( randRange( 10, 50 ) );
this.publicMethodD();
}
public void function publicMethodC() {
sleep( randRange( 10, 50 ) );
}
public void function publicMethodD() {
sleep( randRange( 10, 50 ) );
variables.privateMethodE();
privateMethodF();
}
// ---
// PRIVATE METHODS.
// ---
private void function privateMethodA() {
sleep( randRange( 10, 50 ) );
// Testing with scoping.
variables.privateMethodC();
}
private void function privateMethodB() {
sleep( randRange( 10, 50 ) );
// Testing without scoping.
privateMethodD();
}
private void function privateMethodC() {
sleep( randRange( 10, 50 ) );
}
private void function privateMethodD() {
sleep( randRange( 10, 50 ) );
}
private void function privateMethodE() {
sleep( randRange( 10, 50 ) );
}
private void function privateMethodF() {
sleep( randRange( 10, 50 ) );
}
}
As you can see, this ColdFusion component is nothing more than a set of stubbed-out method calls that demonstrate simulated latency. The only point of interest to note is that the component is receiving an instance of JavaAgentHelper.cfc
when it is instantiated. It is then asking the JavaAgentHelper.cfc
component to add instrumentation to its own instance:
javaAgentHelper.annotateMethods( variables );
Now, before we dive into the details of what JavaAgentHelper.cfc
is doing, let's try to instantiate and consume the MyService.cfc
ColdFusion component to see what happens in FusionReactor:
<cfscript>
// MyService is going to use the JavaAgentHelper to "wrap" each method call so that
// all methods calls on MyService, whether PUBLIC or PRIVATE, will be instrumented
// with a FusionReactor "Tracked Transaction".
service = new MyService( new JavaAgentHelper() );
dump( service.test() );
</cfscript>
If we run this page and then look in FusionReactor's dashboard, we get the following data:
As you can see, under the Relations tab, we get the full breakdown of all the public and private method calls made within MyService.cfc
. The Gantt chart only shows a few levels; but, if you look at the full Transaction History, you can see all the nested method calls.
We can also see the same data in FusionReactor's Cloud dashboard under the Tracing tab:
The Cloud dashboard shows all the same Transactions; but, is a bit more colorful.
Ok, now that we see what the automatic method instrumentation is doing for us, let's look at the JavaAgentHelper.cfc
to see how it works. Internally, the .annotateMethods()
call is iterating over each method in the target component and is swapping out every given method with a proxy method that calls the original method, wrapped in a "Tracked Transaction":
component
output = false
hint = "I help interoperate with the Java Agent that is instrumenting the ColdFusion application (which is provided by FusionReactor)."
{
// I initialize the java agent helper.
public any function init() {
// The FusionReactor Agent is not available in all contexts. As such, we have to
// be careful about trying to load the Java Class; and then, be cautious of its
// existence when we try to consume it. The TYPE OF THIS VARIABLE will be used
// when determining whether or not the FusionReactor API should be consumed. This
// approach allows us to use the same code in the calling context without having
// to worry if the FusionReactor agent is installed.
try {
// NOTE: The FRAPI was on Version 8.2.3 at the time of this writing.
variables.FRAPIClass = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" );
} catch ( any error ) {
variables.FRAPIClass = "";
}
}
// ---
// PUBLIC METHODS.
// ---
/**
* I wrap all of the methods defined in the given Component Scope (VARIABLES) with
* PROXY methods that will automatically create a FusionReactor "tracked transaction"
* that records the timing of each invocation.
*
* @privateScope I am the VARIABLES scope of the component being instrumented
* @annotatePrivateMethods I determine if private methods should be instrumented.
*/
public void function annotateMethods(
required struct privateScope,
boolean annotatePrivateMethods = true
) {
// In order to make sure the proxy methods can create FusionReactor segments,
// let's store a reference to the JavaAgentHelper in the private scope. This will
// then be accessible on the VARIABLES scope.
privateScope.__javaAgentHelper__ = this;
// -- START: Proxy method. -- //
// Every relevant method in the given Component Scope is going to be replaced
// with this PROXY method, which wraps the underlying call to the original method
// in a FusionReactor Segment.
// --
// CAUTION: We need to use a FUNCTION DECLARATION here, not a CLOSURE, because
// this Function needs to execute in the CONTEXT of the ORIGINAL component (ie,
// it has to have all the correct Public and Private scope bindings).
function instrumentedProxy() {
var key = getFunctionCalledName();
var proxiedKey = ( "__" & key & "__" );
var segment = variables.__javaAgentHelper__.segmentStart( key );
try {
// NOTE: In a Lucee CFML component, both PUBLIC and PRIVATE methods can
// be accessed on the VARIABLES scope. As such, we are able to invoke the
// given method on the private component scope regardless of whether or
// not the proxied method is public or private.
return( invoke( variables, proxiedKey, arguments ) );
} finally {
variables.__javaAgentHelper__.segmentEnd( segment );
}
}
// -- END: Proxy method. -- //
// Replace each Function in the target component with a PROXY function.
// --
// NOTE: Both Public and Private methods show up in the private scope of the
// component. As such, we only need to iterate over the private scope when
// looking for methods to instrument.
for ( var key in structKeyArray( privateScope ) ) {
// Skip if not a defined, custom method.
if (
( key == "init" ) ||
! structKeyExists( privateScope, key ) ||
! isCustomFunction( privateScope[ key ] )
) {
continue;
}
// Skip if we're only annotating PUBLIC methods, and this key isn't aliased
// in the PUBLIC scope.
if (
! annotatePrivateMethods &&
! structKeyExists( privateScope.this, key )
) {
continue;
}
var proxiedKey = ( "__" & key & "__" );
// Regardless of whether or not we're dealing with a PUBLIC method, we always
// want to create a proxy in the PRIVATE scope - remember, all methods, both
// PUBLIC and PRIVATE, are accessible on the private Component scope.
privateScope[ proxiedKey ] = privateScope[ key ];
privateScope[ key ] = instrumentedProxy;
// However, if the original method is PUBLIC, we ALSO want to alias the given
// method on the PUBLIC scope so that we can allow for explicitly-scope calls
// (ie, this.method).
if ( structKeyExists( privateScope.this, key ) ) {
privateScope.this[ key ] = privateScope[ key ];
}
}
}
/**
* I end the segment and associate the resultant sub-transaction with the current
* parent transaction.
*
* @segment I am the OPAQUE TOKEN of the segment being ended and timed.
*/
public void function segmentEnd( required any segment ) {
if ( shouldUseFusionReactorApi() ) {
// In the case where the segment is not available (because the FusionReactor
// agent has not been installed), it will be represented as an empty string.
// In such cases, just ignore the request.
if ( isSimpleValue( segment ) ) {
return;
}
segment.close();
}
}
/**
* I start and return a new Segment to be associated with the current request
* transaction. The returned Segment should be considered an OPAQUE TOKEN and should
* not be consumed directly. Instead, it should be passed to the .segmentEnd() method.
* Segments will show up in the Transaction Breakdown table, as well as in the
* "Relations" tab in the Standalone dashboard and the "Traces" tab in the Cloud
* dashboard.
*
* @name I am the name of the segment being started.
*/
public any function segmentStart( required string name ) {
if ( shouldUseFusionReactorApi() ) {
return( FRAPIClass.getInstance().createTrackedTransaction( javaCast( "string", name ) ) );
}
// If the FusionReactor API feature is not enabled, we still need to return
// something as the OPAQUE SEGMENT TOKEN so that the calling logic can be handled
// uniformly within the application code.
return( "" );
}
// ---
// PRIVATE METHODS.
// ---
/**
* I check to see if this machine should consume the FusionReactor static API as part
* of the Java Agent Helper class (this is to allow the methods to exist in the
* calling context without a lot of conditional consumption logic).
*/
private boolean function shouldUseFusionReactorApi() {
// If we were UNABLE TO LOAD THE FRAPI CLASS, there's no API to consume.
if ( isSimpleValue( FRAPIClass ) ) {
return( false );
}
// Even if the FRAPI class is loaded, the underlying FusionReactor instance may
// not yet be ready for interaction. We have to wait until .getInstance() returns
// a non-null value.
if ( isNull( FRAPIClass.getInstance() ) ) {
return( false );
}
return( true );
}
}
There's a lot of fun, dynamic stuff going on in this code: we're declaring a function inside of another function (that is not a closure), we're injecting methods into a component, we're dynamically checking the name of an invoked method, we're messing with Public and Private scopes.
It's just hella exciting!
The techniques that I'm using in ColdFusion component could probably warrant a blog post all on their own; so, I won't dive deep into what is going on - I'll leave it to the reader to look at the code-comments.
This was a fun experiment. I am not sure that I would actually do this in a production setting. Especially since this particular approach doesn't lend itself well to my personal best practices regarding feature flag usage. However, in a pinch, I might try it. If nothing else, it's just fun to see how flexible Lucee CFML is. And, how we can sprinkle FusionReactor into the mix.
Want to use code from this post? Check out the license.
Reader Comments