Create a Debug Logger Apex Class to Triage Flows

Printing to the debug log can be helpful when debugging complex Flows in Salesforce. The steps below explain how to create and deploy a straightforward Apex class to print arbitrary messages to the Debug log. The Apex class below shows up as an “Action” in the Flow Builder.

But, note that printing to the debug log itself consumes resources, particularly CPU time. And, querying the Limits class uses SOQL queries.


4 Main Steps

  1. Create the Flow Debug Logger Class
  2. Create the Flow Debug Logger test Class
  3. Deploy to Production from the Sandbox
  4. Add to Flows to Debug as needed for triage

The main steps above are broken down into substeps, below.

1. Create the Flow Debug Logger Class

  1. Open the Developer Console (Gear Icon > Developer Console)
  2. Create the Class (File > New > Apex Class). Call it FlowDebugLogger.
  3. Replace the contents of the class with the code below.
  4. Save.
public with sharing class FlowDebugLogger {
    @InvocableMethod(label='Log Debug Message' description='Outputs messages to the debug log')
    public static void trace(List<String> debugMessages) {
            for (String debugMessage : debugMessages) {
                system.debug(logginglevel.error, debugMessage);
            }
        }
}

2. Create the Flow Debug Logger test Class

  1. Open the Developer Console (Gear Icon > Developer Console)
  2. Create the Class (File > New > Apex Class). Call it FlowDebugLogger_Test.
  3. Replace the contents of the class with the code below.
  4. Save.
@isTest
private class FlowDebugLogger_Test {
    @isTest static void testCoverage() {
        List<String> testList = new List<String>{'Test'};
        FlowDebugLogger.trace(testList);
    }
}

3. Deploy to Production from the Sandbox

  1. In the Sandbox org, navigate to “Outbound Change Sets” in Setup (Setup > Quick Find > “change” > Outbound Change Sets)
  2. Click “New” button
  3. Call the change set FlowDebugLogger Apex Class or similar. Save.
  4. Click “Add” button in the “Change Set Components” section
  5. Select “Apex Class” from the dropdown
  6. Select the “F” to show just Apex classes starting with “F”
  7. Click the checkboxes next to “FlowDebugLogger” and “FlowDebugLogger_Test,” then click the “Add To Change Set” button
  8. Click the “Upload” button
  9. Choose your Production org using the radio button
  10. In the Production org, navigate to “Inbound Change Sets” in Setup (Setup > Quick Find > “change” > Inbound Change Sets)
  11. Select the “Deploy” link under the “Action” section next to the Change Set you uploaded
  12. Select the option to “Run Specified Tests,” then enter the name of the test class (such as FlowDebugLogger_Test) into the text box, then “Deploy.”

4. Add to Flows to Debug as needed for triage

  1. In the flow, select a “+” icon to add an element.
  2. In the dialog box that pops up, select “Action.”
  3. In the search box, type “debug,” then select Log Debug Message
  4. Populate the “Label,” “API Name,” and click the Toggle
  5. Populate the debug message with whatever is useful, perhaps Entered "Birthday Reminder" Flow


Benchmarking

Running the following Apex test class in a sandbox or Trailhead org allows you to determine the CPU time impact of printing to the debug log. The setup below performs a few operations, the most time consuming of which appears to be the SOQL query. The operations below are included to mimic intended usage of this debug class within a Flow. It:

  1. Queries for a record ID
  2. Converts that record ID to a String
  3. Adds that String to a list of Strings
  4. Calls the FlowLongDebugger trace method which prints to the debug log

Running this test class five times yielded the 4500 microseconds, 4020 microseconds, 4030 microseconds, 4610 microseconds, and 4060 microseconds per operation.

So using this FlowDebugLogger class to print to the debug log consistently takes fewer than 5 milliseconds per call.

@istest(SeeAllData=true)
public class Benchmarking {
    
    private static Integer referenceStartTime;
    private static Integer referenceEndTime;
    private static Integer targetStartTime;
    private static Integer targetEndTime;

    private static void markReferenceStartTime()
    {
        referenceStartTime = Limits.getCpuTime();
    }

    private static void markReferenceEndTime()
    {
        referenceEndTime = Limits.getCpuTime();
    }

    private static void markTargetStartTime()
    {
        targetStartTime = Limits.getCpuTime();
    }

    // Also called by reportResults - so this doesn't have to be called explicitely
    private static void markTargetEndTime()
    {
        targetEndTime = Limits.getCpuTime();
    }

    private static void reportResults(Integer loops)
    {
        if(targetEndTime==null) markTargetEndTime();
        Integer referenceDuration = referenceEndTime - referenceStartTime;
        Integer targetDuration = targetEndTime - targetStartTime;
        Integer benchmarkResults = targetDuration - referenceDuration;
        // Time in microseconds is duration * 1000 / loops
        Decimal eachItem = benchmarkResults * 1000;
        eachItem /= loops;
        eachItem.setScale(2);
        system.debug(LoggingLevel.Error, 
                     'Reference Duration: ' + referenceDuration + 
                     ' Target duration: ' + targetDuration + 
                     ' Benchmark Results: ' + benchmarkResults + 
                     'ms or ' + eachItem + ' us per operation');
    }
    
    // Benchmark a simple integer increment
    @istest
    public static void printToDebugLogBenchmark()
    {
        Integer v = 0;
        markReferenceStartTime();
        for(Integer x = 0; x<100; x++) {}
        markReferenceEndTime();
        markTargetStartTime();
        for(Integer x = 0; x<100; x++) {
            Contact andyYoung = [SELECT Id FROM Contact WHERE Name = 'Andy Young'];
            String strId = Id.valueOf(andyYoung.Id);
            List<String> message = new List<String>{strId};
            FlowDebugLogger.trace(message);
        }
        reportResults(100);
    }
}

Other Options with Debug Classes

Consider creating similar debug classes using lines from the code snippet below, but keep in mind that printing to the debug log itself consumes resources. And, querying the Limits class uses SOQL queries.

System.debug('Limits.getAggregateQueries - '+ Limits.getAggregateQueries());
System.debug('Limits.getLimitAggregateQueries - '+ Limits.getLimitAggregateQueries());
System.debug('Limits.getCallouts - '+ Limits.getCallouts());
System.debug('Limits.getLimitCallouts - '+ Limits.getLimitCallouts());
System.debug('Limits.getCpuTime - '+ Limits.getCpuTime());
System.debug('Limits.getLimitCpuTime - '+ Limits.getLimitCpuTime());
System.debug('Limits.getDMLRows - '+ Limits.getDMLRows());
System.debug('Limits.getLimitDMLRows - '+ Limits.getLimitDMLRows());
System.debug('Limits.getDMLStatements - '+ Limits.getDMLStatements());
System.debug('Limits.getLimitDMLStatements - '+ Limits.getLimitDMLStatements());
System.debug('Limits.getEmailInvocations - '+ Limits.getEmailInvocations());
System.debug('Limits.getLimitEmailInvocations - '+ Limits.getLimitEmailInvocations());
System.debug('Limits.getFutureCalls - '+ Limits.getFutureCalls());
System.debug('Limits.getLimitFutureCalls - '+ Limits.getLimitFutureCalls());
System.debug('Limits.getHeapSize - '+ Limits.getHeapSize());
System.debug('Limits.getLimitHeapSize - '+ Limits.getLimitHeapSize());
System.debug('Limits.getMobilePushApexCalls - '+ Limits.getMobilePushApexCalls());
System.debug('Limits.getLimitMobilePushApexCalls - '+ Limits.getLimitMobilePushApexCalls());
System.debug('Limits.getQueries - '+ Limits.getQueries());
System.debug('Limits.getLimitQueries - '+ Limits.getLimitQueries());
System.debug('Limits.getQueryLocatorRows - '+ Limits.getQueryLocatorRows());
System.debug('Limits.getLimitQueryLocatorRows - '+ Limits.getLimitQueryLocatorRows());
System.debug('Limits.getQueryRows - '+ Limits.getQueryRows());
System.debug('Limits.getLimitQueryRows - '+ Limits.getLimitQueryRows());
System.debug('Limits.getQueueableJobs - '+ Limits.getQueueableJobs());
System.debug('Limits.getLimitQueueableJobs - '+ Limits.getLimitQueueableJobs());
System.debug('Limits.getSoslQueries - '+ Limits.getSoslQueries());
System.debug('Limits.getLimitSoslQueries - '+ Limits.getLimitSoslQueries());