Dynamically Instrumenting ColdFusion Component Methods With FusionReactor Tracked Transactions In Lucee CFML 5.2.9.40
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) ); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment