Batch Logging
This article demonstrates some common approaches to batch logging.
Batch jobs are great for processing large amounts of data in the background. They can be scheduled, executed by hand on demand, or via some sort of trigger. The common pain point with these is that there's little visibility when it comes to failures. Hence, many developers turn to logging. There are a number of use cases to cover here: basic error handling, trace logging, and profiling.
Basic Error Handling
Proper error handling is essential to weed out those pesky errors in batch jobs. Often Salesforce will mask failures or provide a one-liner error message with no stack trace. This is where we can make a huge difference by taking matters into our own hands. Below is a code snippet that incorporates error logging in the most troublesome execute() method.
public class LeadsBatch implements Database.Batchable<sObject>, Database.Stateful {
private String query ='';
private Triton logger = Triton.instance;
public LeadsBatch(String leadQuery) {
this.query = leadQuery;
}
public Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<Lead> leads) {
for(Lead l : leads){
try {
LeadProcessingResult result = LeadService.getQualificationScore(l);
if(result.isSuccess) {
l.Score__c = result.qualificationScore;
}
} catch(Exception e){
this.logger.addError(TritonTypes.Area.LeadQualification, e);
}
}
try {
update leads;
} catch(Exception e){
this.logger.addError(TritonTypes.Area.LeadQualification, e);
}
}
public void finish(Database.BatchableContext bc){
this.logger.flush();
}
}
There is a good chance you might find similar batch jobs in your own org. The pattern is quite typical: query records, process them in a loop, then perform a DML, a callout or some other action. Let's now dive into the details of what's happening here.
Note the use of the Database.Stateful interface in the declaration of our LeadsBatch class. In this example we have made our batch stateful, or, in other words, retain its state between the different iterations of the execute() method. For more information on this please refer to Salesforce developer documentation here. The reason we need state preservation is to be able to accumulate our logs in a buffer within the logger class variable declared on line (3). Static context in our Singleton logger instance will not be preserved between different batch iterations.
In this batch class we're utilizing another concept called bulk logging introduced in this section. You'll notice the use of the addError() method instead of the error() method as in the previous example. The addError() method appends to the log buffer that is subsequently flushed in the finish() method on line (33).
The use of the finish() method on line (33). This method runs when a batch is finished executing, regardless of whether those executions resulted in exceptions. This is good news for us as it ensures that our log buffer is persisted properly.
Note the use of the Area enum on line (21) and (28) in the catch() clause. The more accurate you can be with this value, the better your searching and reporting experience will be.
Grouping Logs From Multiple Iterations
Let's now incorporate the idea of uniting logs under a single transaction illustrated in the very first example with the concept of bulk logging. We'd like to see all logs for each of the iteration grouped under the same parent, and we are also going to do in a way that is consistent with the platform best practices by emitting only a single platform event. The example below also adds a few debug() and event() statements to the mix for a more detailed trace of batch execution:
public class LeadsBatch implements Database.Batchable<sObject>, Database.Stateful {
private String query ='';
private Log logger = Triton.instance;
//will need to keep track of the number of iterations
private Integer iteration = 0;
public LeadsBatch(String leadQuery) {
this.query = leadQuery;
logger.startTransaction();
logger.addDebug(TritonTypes.Type.LeadsBatch,
TritonTypes.Area.LeadQualification,
'LeadsBatch initialized',
'LeadsBatch initialized with query: ' + query );
}
public Database.QueryLocator start(Database.BatchableContext bc) {
logger.addEvent(TritonTypes.Level.FINE,
TritonTypes.Type.LeadsBatch,
TritonTypes.Area.LeadQualification,
'LeadsBatch starting',
'Fetching query locator for query: ' + query );
return Database.getQueryLocator(query);
}
public void execute(Database.BatchableContext bc, List<Lead> leads) {
iteration++;
logger.addEvent(TritonTypes.Level.FINE,
TritonTypes.Type.LeadsBatch,
TritonTypes.Area.LeadQualification,
'LeadsBatch executing iteration: '+ iteration,
'Processing ' + leads.size()) + ' in this iteration';
for(Lead l : leads) {
try {
LeadProcessingResult result = LeadService.getQualificationScore(l);
if(result.isSuccess) {
l.Score__c = result.qualificationScore;
} else {
logger.addEvent(TritonTypes.Level.FINE,
TritonTypes.Type.LeadsBatch,
TritonTypes.Area.LeadQualification,
'LeadsBatch failed to obtain successful qualification score',
'Result returned for lead id ' + l.Id +
Triton.SPACE_SEP + 'Result: ' +
Triton.SPACE_SEP + String.valueOf(result);
}
} catch(Exception e){
logger.addError(TritonTypes.Area.LeadQualification, e);
}
}
try {
update leads;
} catch(Exception e){
logger.addError(TritonTypes.Area.LeadQualification, e);
}
}
public void finish(Database.BatchableContext bc){
logger.addDebug(TritonTypes.Type.LeadsBatch,
TritonTypes.Area.LeadQualification,
'LeadsBatch finishing',
'Iterations processed: ' + iteration);
logger.flush();
logger.stopTransaction();
}
}
For the most part this example is similar in spirit to the basic batch logging snippet. A few things to highlight here nonetheless:
Note the more generous logging statements that are incorporated into every method of this batch class. This will provide more details when analyzing the execution and help examine some key data points such as iteration number and the query that was used to construct a query locator.
In this example we're making use of the different log levels (see Level enum). The constructor and the finish() methods are utilizing the DEBUG level, whereas the start() and execute() methods are logging at INFO level.
Last but not least is that all these logs that we're generating here will be grouped under a single parent log. This is quite convenient for future troubleshooting needs as there is now an nice easy to navigate structure available.
Last updated