Adding FusionReactor Sub-Transaction Breakdowns To My ColdFusion Blog
A couple of years ago, I wrote about how we're using the FusionReactor API (FRAPI
) to instrument our Lucee CFML apps at work. And, now that I have FusionReactor installed on my ColdFusion 2021 blog, I've been translating some of that logic over to this site. I recently demonstrated that FusionReactor gave me critical insights into my SQL queries and my in-memory caching techniques. And, this morning, I added some "Tracked Transactions" to help me understand how long certain portions of my ColdFusion request was taking to execute.
A "Transaction", in FusionReactor parlance, is just a cohesive set of operations that are being rolled-up into a discrete concept. Every incoming ColdFusion request, for example, is implicitly tracked as a "root transaction". Then, portions of the request, such a JDBC query, can be tracked as a sub-transaction under the root transaction. Some operations, like JDBC and HTTP request are automatically tracked by the FusionReactor Java Agent. However, by using the FusionReactor API (FRAPI
), we can progammatically track any segment of the request processing as a sub-transaction:
<cfscript>
FRAPI = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" )
.getInstance()
;
try {
segment = FRAPI.createTrackedTransaction( javaCast( "string", "my-segment" ) );
// ... Perform operations that we want to track ...
} finally {
segment.close();
}
</cfscript>
Here, we're starting a transaction that we're calling "my-segment"
. Then, we're performing some operations within it before calling the .close()
method to demarcate the end of the tracked transaction.
On my blog, most requests flow through a main index.cfm
file, which is just a series of switch
and include
statements that execute and capture the requested View and then render that View content in the desired Layout. For the initial transaction-based instrumentation of my ColdFusion blog, I'd like to carve-out the following segments:
onRequestStart()
- this initializes the incoming request scope and translates the SEO (Search Engine Optimization) URLs into application events.View Rendering - this is the main action associated with the request.
Layout Rendering - this is the wrapping of the generated View content in a Layout.
Error Rendering - when an error is thrown within the View / Layout processing, this takes over the request and renders an error response.
Here is my abbreviated Application.cfc
ColdFusion application component file that instruments the onRequestStart()
event-handler. Note that I have encapsulated the low-level FRAPI
calls inside a JavaAgentApi.cfc
that is discussed later in this post:
component {
public void function onRequestStart( required string scriptName ) {
try {
var onRequestStartSegment = application.javaAgentApi
.segmentStart( "on-request-start" )
;
// .... request initialization ....
// .... seo url parsing ....
// .... backwards-compatibility URL handling ....
// .... security operations ....
} finally {
application.javaAgentApi.segmentEnd( onRequestStartSegment );
}
}
}
Notice that I am using a try/finally
pattern of programming. This ensures that the tracked transaction / segment is closed even if something within the transaction throws an error or short-circuits out of the method execution.
Then, within my main index.cfm
controller file, I'm doing the same thing; only, instead of a single tracked-transaction, I have multiple transactions within the same template:
<cfscript>
try {
indexSegment = application.javaAgentApi
.segmentStart( "processing-index" )
;
viewSegment = application.javaAgentApi
.segmentStart( "processing-view" )
;
param name="request.event[ 1 ]" type="string" default="";
switch ( request.event[ 1 ] ) {
case "about":
include "./content/about/_index.cfm";
break;
// .... all the route subsystems ....
default:
throw(
type = "BenNadel.Routing.InvalidEvent",
message = "Unknown routing event: root."
);
break;
}
application.javaAgentApi.segmentEnd( viewSegment );
// If we made it this far without error, we know that we're dealing with a valid
// route. Let's use the event value to name the request.
application.javaAgentApi.transactionSetName( request.event.toList( "-" ) );
layoutSegment = application.javaAgentApi
.segmentStart( "processing-layout" )
;
// Now that we have executed the page, let's include the appropriate rendering
// template.
switch ( request.template.type ) {
case "blank":
include "./content/layouts/_blank_query.cfm";
include "./content/layouts/_blank.cfm";
break;
// .... all the layout engines ....
}
application.javaAgentApi.segmentEnd( layoutSegment );
// NOTE: Since this try/catch is happening in the onRequest() event handler, we know
// that the application has, at the very least, successfully bootstrapped and that we
// have access to all the application-scoped services.
} catch ( any error ) {
errorSegment = application.javaAgentApi
.segmentStart( "processing-error" )
;
application.logger.logException( error );
errorResponse = application.services.errorService.getResponse( error );
request.template.type = "error";
request.template.statusCode = errorResponse.statusCode;
request.template.statusText = errorResponse.statusText;
request.template.title = errorResponse.title;
request.template.message = errorResponse.message;
include "./content/layouts/_error_query.cfm";
include "./content/layouts/_error.cfm";
application.javaAgentApi.segmentEnd( errorSegment );
} finally {
// Clean-up any potentially un-closed tracked-transactions.
application.javaAgentApi.segmentEnd( indexSegment );
application.javaAgentApi.segmentEnd( viewSegment );
// Depending on where the error happened, these may not exist. Calling the
// .segmentEnd() with an empty-string is safe.
application.javaAgentApi.segmentEnd( layoutSegment ?: "" );
application.javaAgentApi.segmentEnd( errorSegment ?: "" );
}
</cfscript>
As you can see, I'm using the same try/finally
pattern that I have in the Application.cfc
. This is to make sure that I always call .close()
on the tracked transaction. In some cases, I may even call .close()
multiple times on the same transaction in an attempt to clean it up (which is safe to do).
With this sub-transaction instrumentation in place, if I look at my incoming ColdFusion requests in the FusionReactor Cloud dashboard, I can clearly see how long each portion of the request is taking to process:
As you can see, in the Tracing tab of the FusionReactor Cloud dashboard, I can now see the individually instrumented segments; and, how they contribute to the overall request time. In theory, this will help me identify bottlenecks in the request. Though, since my blog doesn't really do much processing of any kind, this is more educational than anything else.
Here's my currently implementation of the JavaAgentApi.cfc
which is a ColdFusion component that simplifies the API calls to the FRAPI
instance:
component
output = false
hint = "I provide a SAFE API to the underlying FusionReactor JavaAgent to help instrument the ColdFusion application."
{
/**
* I initialize the java agent API helper.
*/
public void 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.x at the time of this writing.
variables.FRAPIClass = createObject( "java", "com.intergral.fusionreactor.api.FRAPI" );
} catch ( any error ) {
variables.FRAPIClass = "";
}
}
// ---
// PUBLIC METHODS.
// ---
/**
* I set the application name for the request's MASTER transaction.
*/
public void function applicationSetName( required string name ) {
if ( shouldUseFusionReactorApi() ) {
getApi().setTransactionApplicationName( javaCast( "string", name ) );
}
}
/**
* I end the segment and associate the resultant sub-transaction with the current
* parent transaction.
*/
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.
*/
public any function segmentStart( required string name ) {
if ( shouldUseFusionReactorApi() ) {
return( getApi().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 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.
*/
public void function transactionSetName( required string name ) {
if ( shouldUseFusionReactorApi() ) {
getApi().setTransactionName( javaCast( "string", name ) );
}
}
// ---
// PRIVATE METHODS.
// ---
/**
* I get the instance of the running FusionReactor API. If FusionReactor is not
* running, or is not initialized yet, this returns NULL.
*/
private any function getApi() {
return( FRAPIClass.getInstance() );
}
/**
* 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 );
}
// If the underlying FusionReactor instance isn't running yet, the API is null. We
// have to wait for it to be ready.
if ( isNull( getApi() ) ) {
return( false );
}
return( true );
}
}
It's always fun to get a glimpse into the breakdown of a request. My blog doesn't really "do" anything; so, there's not that much here to see. But the same principles that I am applying to this ColdFusion blog should be applicable to any application.
Want to use code from this post? Check out the license.
Reader Comments
Post A Comment — ❤️ I'd Love To Hear From You! ❤️
Post a Comment →