Without A (Debug) Trace: Easier Logging in Apex

As a Salesforce developer working with my client’s CRM development team to find and fix bugs in our Apex code, it didn’t take long for us to get ever-so-slightly frustrated with the constant need to start debug traces, dig through logs, and clean everything up afterwards. In addition, because traces are almost never running in higher lifecycles, if (heaven forbid) issues occur in production, you can find yourself left in the dark when it comes to things that aren’t easily reproduced. To avoid this, we started looking for a way to log errors that would be relatively hassle-free, lightweight and reliable.

EventLogs

The first incarnation of this idea came about when we were working with Platform Events. Included in the Summer ’17 release, Platform Events (PEs) are a message/event-driven architecture native to the Force.com platform that can be very helpful in integrating Salesforce with other enterprise systems. Although PEs are structured more or less the same way as custom SObjects, one key difference is that they are not persisted in the database. As a result, if your code is processing an after insert trigger on your PE and an exception occurs, you might have a tough time getting to the root cause if you didn’t store that PE somewhere.

I created a custom object called an “EventLog”, and added logic similar to the below at relevant points in the trigger:

List<EventLog__c> eventLogs = new List<EventLog__c>();
Database.SaveResult[] results = Database.update( contacts, false );
for ( Integer i = 0; i < results.size(); i++ ) {
// for each Contact that failed to update, create an EventLog
if ( !results[i].isSuccess() ) {
List<String> errorMessages = new List<String>();
        // collect all problem fields from the record
for ( Database.Error err : results[i].getErrors() ) {
errorMessages.add(
err.getMessage() + '[' + err.getFields() + ']'
);
}
Contact c = contacts[i];
eventLogs.add( new EventLog__c(
Event_Type__c = 'Sale Line: Contact',
Event_ID__c = c.Ext_ID__c,
Event_Details__c = JSON.serialize( c ),
Event_Error_Message__c = String.join(
errorMessages, '\n'
)
) );
}
}
// ...
insert eventLogs;

This is a pretty standard process — having updated some Contacts related to the incoming events, we’re calling Database.update with the allOrNone parameter set to false, then looping through the results of the update to see what failed. If there were any failures, the EventLog allows me to store the type of event being processed, the ID (external ID in this case) of the record that failed, the actual value of that record, and all of the error messages that occurred as a result. The EventLogs can then be easily surfaced in the UI for review.

But what if there’s an exception outside of a DML operation? Just create an EventLog in your catch block! Which brings us to the next section…

Log All The Things

Hopefully, if you’re writing Apex triggers, you’re following some kind of Trigger Handler design pattern. Trigger Handlers can range from bare-bones to extremely complex, but the core principles needed here are the following:

  • your trigger contains no logic, but instead delegates to a TriggerHandler
  • your TriggerHandler defines methods to handle the various combinations of before/after/insert/update/etc., most likely grouped into some kind of “run()” method
  • your TriggerHandler contains methods to prepare for and clean up after the actual trigger logic, “setup” and “teardown”, if you will

By integrating the EventLog with a TriggerHandler-type framework, we can almost completely avoid the possibility of uncaught exceptions. Let’s take a look:

public virtual class TriggerHandler {
// SObject type of trigger; set this in the constructor
// of the implementing class
protected SObjectType otype = null;
protected List<EventLog__c> errorList = new List<EventLog__c>();
    public void run() {
setup();
try {
// trigger logic goes in here, via calls to
//beforeInsert, afterUpdate and so forth
        } catch ( Exception e ) {
log(
String.valueOf( otype ) + ' : uncaught',
'0',
JSON.serialize( trigger.new ),
Event_Error_Message__c = e.getMessage() + '\n'
+ e.getStackTraceString()
) );
} finally {
finish();
}
}
    protected void log( String eventType, String eventRecordId,
String eventMessage, String eventDetails
) {
errorList.add( new EventLog__c(
Event_Type__c = eventType,
Event_ID__c = eventRecordId,
Event_Details__c = eventDetails,
Event_Error_Message__c = eventMessage
) );
}
    protected void finish() {
if ( !errorList.isEmpty() ) {
Database.insert( errorList, false );
}
}
}

This way, if the unthinkable happens and our code throws an otherwise uncaught exception, we make sure to snag it and log whatever might have happened. Furthermore, as long as we’re running within the TriggerHandler, we can leverage that log method for more granular exception handling elsewhere, and know that whatever we captured in-flight will land in the database before we wrap up. And all of this takes place without the overhead of creating traces and digging through logs!

The Future

In its current state, the integrated EventLog pattern is… satisfactory. We’ve got most of what we want out of it, but there is definitely room for some enhancements! Here are a couple things I’m working toward:

  • Allow users to enable/disable logging through the UI:
    Use a Custom Metadata Type to store an “Enabled” Boolean for various SObject types, and have the TriggerHandler check whether to insert EventLogs as it’s wrapping up.
  • Cleaner logging on DML operations:
    Implement some sort of “insertWithLogging”/“updateWithLogging” method to abstract away the loop through SaveResult and improve readability.
  • Improving search functionality:
    At the moment, the Event Type field is a bit too flexible; it would be helpful to break it into separate fields, storing the actual SObject type separately from info about where in the process the exception occurred.
  • Integration with enterprise logging applications:
    Set up a workflow rule to send outbound messages to Splunk, ELK, etc.

If you pursue this sort of pattern, make sure to take steps to maintain the health of your org. For instance, consider scheduling a job to delete EventLogs older than 2 weeks, or whatever timeframe suits you. Also keep in mind who should or should not have access to the object; EventLogs likely won’t be of concern to your business users.