Skip to content

Instantly share code, notes, and snippets.

@bennadel
Created January 27, 2020 13:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save bennadel/ccf1f6c1670080449f36be9d752c5c29 to your computer and use it in GitHub Desktop.
Save bennadel/ccf1f6c1670080449f36be9d752c5c29 to your computer and use it in GitHub Desktop.
Wrapping The FusionReactor API (FRAPI) For Safe Consumption 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 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 );
}
}
<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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment