Wrapping The FusionReactor API (FRAPI) For Safe Consumption In Lucee CFML 5.2.9.40
Over the weekend, I looked at how to use the FusionReactor API (FRAPI) to instrument ColdFusion code. In that post, I was referencing the FRAPI
Java class directly; which, only works if you have the FusionReactor Java Agent installed. In production code, I tend to wrap these kinds of classes / APIs in my own ColdFusion component so that I can safely - and simply - consume them even when the code is running in an environment that doesn't include the FusionReactor Java Agent. In the past, I've done this with New Relic's Java Agent; and now, I wanted to look at how I would do this with the FusionReactor Java Agent in Lucee CFML 5.2.9.40.
The goal of the ColdFusion component wrapper is to keep the calling code safe and consistent. This means that the calling code doesn't have to worry about whether or not the FusionReactor API is actually available. If the FRAPI
class can't be loaded, the methods on this wrapper, essentially, become No-Ops (No Operation); and, the calling code continues executing, blissfully unaware of the underlying implementation details.
In order to keep the calling code simple, I have to limit the amount of functionality that's actually exposed by the FRAPI
wrapper. Primarily, I focus on the ability to segment portions of the code so that I can identify bottlenecks; and demonstrate that code optimizations actually lead to better performance.
To this end, I have methods that create "segments" within the CFML code:
segmentStart( name )
segmentEnd( token )
segmentWrap( name, callback )
Under the hood, these methods are creating tracked Transactions off of the parent transaction. These show up in the Relations and Traces tabs in the Standalone and Cloud dashboards, respectively. The segmentStart()
and segmentEnd()
methods are executed in pairs, whereas the segmentWrap()
takes a ColdFusion closure and implicitly calls segmentStart()
and segmentEnd()
under the hood.
As companion functionality, I also have methods that can record aggregate numeric metrics. Typically, I'll use these methods to record the execution time of an algorithm (often one that is segmented using the aforementioned methods):
metricAdd( name, value [, enableCloudMetric] )
metricAddFloat( name, value [, enableCloudMetric] )
Under the hood, these are, by default, enabled for the Cloud dashboard, which means that they must be aggregate, numeric values (the only type supported in the Cloud dashboard and the custom Metrics graphs).
I also have a few other methods that can add meta-data to the running request:
transactionSetName( name )
propertyAdd( name, value )
propertyAddAll( struct )
traceAdd( value )
To see these in action, I put together a small CFML script that consumed the JavaAgentHelper.cfc
, which is my wrapper around the FRAPI
Java class. As with my previous FusionReactor demos, this one uses a mock Feature Flag to segment and time an experimental algorithm:
<cfscript>
// The JavaAgentHelper is a SIMPLIFIED wrapper around the FRAPI class that can safely
// be consumed even if the FRAPI class isn't installed. This allows the same code
// to run "consistently" across environments, even when the FusionReactor Java Agent
// isn't installed.
javaAgentHelper = new JavaAgentHelper();
javaAgentHelper.transactionSetName( "Testing-Wrapper" );
javaAgentHelper.propertyAddAll({
userID: 4,
teamID: 99
});
javaAgentHelper.traceAdd( "Testing the wrapper." );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
startedAt = getTickCount();
// For this test, we're going to mock the usage of a feature flag.
shouldUseExperiment = randRange( 0, 1 );
// Experimental case.
if ( shouldUseExperiment ) {
// Use the callback approach to segment this code. Under the hood, this is
// creating a sub-transaction that can be seen under the Relations / Traces tabs
// within the various FusionReactor dashboards.
javaAgentHelper.segmentWrap(
"MyExperimentalSubTransaction",
() => {
sleep( randRange( 100, 500 ) );
}
);
// Record an Aggregate Numeric Long metric for this experimental case.
javaAgentHelper.metricAdd( "/wrapper/experimental/duration", ( getTickCount() - startedAt ) );
// Base case.
} else {
// Use the token approach to segment this code. Under the hood, this is creating
// a sub-transaction that can be seen under the Relations / Traces tabs within
// the various FusionReactor dashboards.
token = javaAgentHelper.segmentStart( "MyBaseSubTransaction" );
try {
sleep( randRange( 500, 1500 ) );
} finally {
javaAgentHelper.segmentEnd( token );
}
// Record an Aggregate Numeric Long metric for this base case.
javaAgentHelper.metricAdd( "/wrapper/base/duration", ( getTickCount() - startedAt ) );
}
</cfscript>
<!--- ------------------------------------------------------------------------------ --->
<!--- ------------------------------------------------------------------------------ --->
<script>
// Simulate regular throughput / traffic to this endpoint by refreshing.
setTimeout(
function() {
window.location.reload();
},
1000
);
</script>
As you can see, this code only uses the JavaAgentWrapper.cfc
- there are no direct references to the FRAPI
Java class. As such, this code will work perfectly well regardless of whether or not the FusionReactor Java agent is installed.
Within this code, we're adding some meta-data; then, using the two different segmentation techniques to compare the "base" algorithm to the performance of an "experimental" algorithm. Each branch of the code also records a duration (in milliseconds), which can be seen in the FusionReactor dashboards.
If we look in the FusionReactor dashboard (the Standalone dashboard in this case), we can see the experimental segmentation showing up under "Relations":
We can all see the numeric aggregate metrics that we recorded showing up against the JVM's CPU and Heap graphs:
In FusionReactor's Standalone dashboard, we can easily see the performance of the feature-flag code in relation to the JVM state. However, it's not that easy to see the performance of the experimental code vs. the original code. For that, we can jump into the Cloud dashboard and create a custom graph that overlays the experimental duration on top of the base duration:
By using a custom graph, we can show the relative performance of our two aggregate numeric metrics. Clearly, my code optimization experiment and feature flag are worth while!
Now that we see how the JavaAgentHelper.cfc
can be used to safely consume the FRAPI
regardless of whether or not FusionReactor's Java Agent has been installed, let's look at how it's implemented under the hood:
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 post the given metric as an AGGREGATE NUMERIC LONG metric; and, optionally,
* stream the metric to the Cloud dashboard.
*
* NOTE: In the FusionReactor documentation, metrics are all named using slash-
* notation. As in, "/this/is/my/metric". I recommend you follow this same pattern
* for consistency.
*
* @name I am the name of the metric.
* @value I am the LONG value of the metric.
* @output false
*/
public void function metricAdd(
required string name,
required numeric value,
boolean enableCloudMetric = true
) {
if ( shouldUseFusionReactorApi() ) {
FRAPIClass.getInstance().postNumericAggregateMetric(
javaCast( "string", name ),
javaCast( "long", value )
);
if ( enableCloudMetric ) {
FRAPIClass.getInstance().enableCloudMetric( javaCast( "string", name ) );
}
}
}
/**
* I post the given metric as an AGGREGATE NUMERIC FLOAT metric; and, optionally,
* stream the metric to the Cloud dashboard.
*
* @name I am the name of the metric.
* @value I am the FLOAT value of the metric.
* @output false
*/
public void function metricAddFloat(
required string name,
required numeric value,
boolean enableCloudMetric = true
) {
if ( shouldUseFusionReactorApi() ) {
FRAPIClass.getInstance().postNumericAggregateMetric(
javaCast( "string", name ),
javaCast( "float", value )
);
if ( enableCloudMetric ) {
FRAPIClass.getInstance().enableCloudMetric( javaCast( "string", name ) );
}
}
}
/**
* I associate the given property with the currently active transaction.
*
* @name I am the name of the custom property.
* @value I am the value of the custom property.
* @output false
*/
public void function propertyAdd(
required string name,
required string value
) {
if ( shouldUseFusionReactorApi() ) {
FRAPIClass.getInstance().getActiveTransaction().setProperty(
javaCast( "string", name ),
javaCast( "string", value )
);
}
}
/**
* I associate the given set of properties with the currently active transaction.
*
* NOTE: The values are expected to be Strings.
*
* @payload I am the set of key-value pairs to record as properties.
* @output false
*/
public void function propertyAddAll( required struct payload ) {
for ( var key in payload ) {
propertyAdd( key, payload[ 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.
* @output false
*/
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.
* @output false
*/
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( "" );
}
/**
* I wrap a new Segment with the given name around the execution of the given
* callback. 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. I pass-through the return value of the callback invocation.
*
* @name I am the name of the segment being started.
* @callback I am the callback being executed and timed.
* @output false
*/
public any function segmentWrap(
required string name,
required function callback
) {
var segmentToken = segmentStart( name );
try {
return( callback() );
} finally {
segmentEnd( segmentToken );
}
}
/**
* I set the name of the request's MASTER transaction (which is used to separate
* requests within the FusionReactor dashboard).
*
* CAUTION: Transaction names should container alpha-numeric characters. Including
* slashes or dots within the name appears to create some unexpected behaviors in the
* Standalone dashboard.
*
* @name I am the name of the transaction. It will be used as-is.
* @output false
*/
public void function transactionSetName( required string name ) {
if ( shouldUseFusionReactorApi() ) {
FRAPIClass.getInstance().setTransactionName( javaCast( "string", name ) );
}
}
/**
* I record the given value along with a time-stamp under the Traces tab of the master
* transaction. This information appears to only be available in the Standalone
* dashboard.
*
* NOTE: Struct, Array, and Simple values have been tested to work.
*
* @value I am the value being recorded.
* @output false
*/
public void function traceAdd( required any value ) {
if ( shouldUseFusionReactorApi() ) {
FRAPIClass.getInstance().trace( value );
}
}
// ---
// 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).
*
* @output false
*/
private boolean function shouldUseFusionReactorApi() {
// If we were UNABLE TO LOAD THE FRAPI CLASS, there's no API to consume.
if ( isSimpleValue( FRAPIClass ) ) {
return( false );
}
return( true );
}
}
As you can see, this ColdFusion component starts by attempting to get a reference the core Java class that is exposed by FusionReactor's Java Agent, com.intergral.fusionreactor.api.FRAPI
. If this class definition can be created, it gets cached in the component variables; and, if it can't be found (because FusionReactor's Java Agent isn't installed), then an empty-string is cached. Each of the wrapper methods then subsequently checks to see if the FRAPI
value exists before attempting to consume it.
The goal of all of this is to help articulate portions of the Lucee CFML code within the overall execution of the ColdFusion request; and then, to start applying optimizations to the code, using metrics and graphs to prove that the changes are actually leading to a positive outcome. There's nothing more frustrating than trying to change code without data-driven results. And, by wrapping FusionReactor's Java Agent in a thin ColdFusion component wrapper, I can start to identify performance bottlenecks safely across all environments to which my code is deployed.
Want to use code from this post? Check out the license.
Reader Comments
Have you tried any of these integrations with Adobe CF?
@John,
I'm 95% sure that my New Relic one used to run under Adobe ColdFusion 10. But, the FusionReactor one - I never tested that outside of Lucee CFML.
So, are you going to package these and post them on ForgeBox?
@John,
To be honest, I'm not yet familiar with ForgeBox. I've only just recently begun to play with CommandBox. I'm still not really effective with all this new stuff. That said, I am not sure this rises to the level of "module". Really, this is just informational, providing code that would likely have to be tailored to one's own needs. But, I'm open to learning about this stuff, that's for sure.
Nice post, the FRAPI does a heck of a lot of stuff and most people don't know about it. I've found the actual Java API to be pretty well built and I don't even feel like it needs a wrapper to be honest. That said, my FRAPI integration I did with CommandBox uses it and I did create a separate helper CFC, but really only for abstraction of the logic and easier re-use
https://github.com/Ortus-Solutions/commandbox/blob/development/src/cfml/system/services/FRTransService.cfc
The commercial ColdBox Module ProfileBox also makes use of the FRAPI to report things like view renderings, event executions, and cache stats in a ColdBox app directly to FR.
@Brad,
Agreed. Looking at your ColdBox module, it looks like your wrapper is more or less doing what I am doing as well -- mostly just protecting against situations in which the
FRAPI
isn't available. I know that in our app, we have various Testing and Staging environments where we don't have the-javaagent
in our_JAVA_OPTIONS
ENV variable. So, it's not for the calling code to not have to care about this.@Brad,
Also, I see you're caching the results of
.getInstance()
. I wasn't sure if that was doable or not. The documentation on that is unclear for me. It says:So, I wasn't sure if this was referring to anything under the
FRAPI
class? Or, specifically on instances of the.getInstance()
return value. Maybe you can add clarity?@All,
So, we just had an incident on one of our servers. For about 20-minutes (until I evicted the pod from the load-balancer), it was throwing this error:
I just talked to the FusionReactor support and they let me know that if the underlying FusionReactor instances wasn't running yet, then
.getInstance()
will returnnull
. As such, I've gone into my production code and updated theshouldUseFusionReactorApi()
method to look more like this:The Support team also told me that you can force the FusionReactor initialization to block the Lucee CFML initialization by adding this
_JVM_OPTIONS
flag:However, for now, I think I'll silently ignore the in-app calls until the
.getInstance()
returns non-null values.@All,
As a fun follow-up experiment with instrumentation using the
JavaAgentHelper
, I wanted to see if I could dynamically wrap all methods of a ColdFusion component in "tracked transactions":www.bennadel.com/blog/3769-dynamically-instrumenting-coldfusion-component-methods-with-fusionreactor-tracked-transactions-in-lucee-cfml-5-2-9-40.htm
Because ColdFusion is so intensely dynamic, it turns out this is kind of simple (in a complex sort of way). I am not saying this is something I would do a lot -- but, the fact that it is possible is just so delicious!
@All,
One of the things I think I want to add to this
FRAPI
wrapper is the ability to provide a description when wrapping a segment of code in a tracked transaction. Transaction descriptions show up in the Tracing and Relations tab of the Cloud and Standalone dashboards, respectively:www.bennadel.com/blog/3772-adding-a-description-to-fusionreactor-tracked-transactions-in-lucee-cfml-5-2-9-40.htm
This would allow me to get a better understanding of why a particular segment might take longer on some requests (based on variations of its inputs, which I can outline in the Description).
@All,
I think I might add another method to this
JavaAgentHelper.cfc
to aid in the instrumentation ofCFThread
tags:www.bennadel.com/blog/3773-experiment-wrapping-cfthread-execution-in-a-fusionreactor-tracked-transaction-in-lucee-cfml-5-2-9-40.htm
This experiment (linked here) proxies the
CFThread
tag, using a callback to invoke asynchronous logic within a "Tracked Transaction."