Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created February 13, 2020 12:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save bennadel/2260952ce2afc6dd0bf859dc1d403195 to your computer and use it in GitHub Desktop.
Save bennadel/2260952ce2afc6dd0bf859dc1d403195 to your computer and use it in GitHub Desktop.
Dynamically Instrumenting ColdFusion Component Methods With FusionReactor Tracked Transactions In Lucee CFML 5.2.9.40
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 );
}
}
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 ) );
}
}
<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