16

We have multiple Apex jobs running, both from Schedulable and Batchable code. While an attentive SA could examine the Apex Jobs list to check that everything is OK, it seems a better idea to have explicit notification of errors (and more detailed error information).

We've tried a number of approaches to this in the past, but are now thinking of creating standard Document objects to log into (as that can be done from both Schedulable and Batchable code with no new dependency introduced) that follow a naming pattern and then having one Schedulable that merges these once a day into a single notification email.

But before making this change I'm interested to know if anyone has a better idea they are willing to share?

3 Answers 3

17

I occasionally use something that I got from the Dev 501 class workbook. They have an Error Log custom object where they store error information. I have implemented this in a few different orgs and it works well to alert me of any issues with scheduled and batch classes. I even used in it a few controller classes for high volume VF pages.

Error Log custom object with just one custom field, Trace__c, although you could break it out into different, more specific fields if you like.

So in my apex I create a new Error Log object in my catch statements to records specific error information.

try{
    //Some DML statement
} catch (Exception e) {         
    Error_Log__c log = new Error_Log__c();
    log.trace__c = 'Type: ' + e.getTypeName() + '\n' + 'Cause: ' + e.getCause() + '\n' + 'Message: ' 
    + e.getMessage() + '\n' + 'Line #: ' + e.getLineNumber() + '\n' + e.getStackTraceString() + '\n'
    + 'Some Custom Variable Information From Class: ' + myClassVariable;
    insert log;         
}

I then set up a workflow on the error log object to email me whenever a new error log is created. As you can see you can add different information based on what class you are using it in. Its fairly flexible and pretty simple.

The one drawback would be that you are using a custom object for this and storing the error data, so you might want to monitor this object and periodically delete old error logs. I have a batch class that runs monthly and deletes anything over 30 days old.

The advantage to this method is you only get alerted when there are issues. I have a lot of scheduled classes, so I originally sent an email on the job completion, but with so many classes, some running every night, I didn't need to get an email just to tell me it worked fine. This will only email me when something goes wrong.

Don't know if this is exactly what you were looking for, but hopefully this helps.

2
  • 1
    Yeah it is this sort of thing. We have multiple managed packages and source code deployed as a company, hence using Document instead of a custom object to log to.
    – Keith C
    Oct 2, 2013 at 14:34
  • 1
    Then instead of creating a custom object, you could add the information to a blob and create a PDF and store it as a document or add it as content nightly. Oct 2, 2013 at 14:35
10

I use a couple different things for monitoring:

(1) Batch Code Finish Email: Creates a Messaging.SingleEmailMessage to the user who launched the batch code (you gotta send the Message in the batch code).

public static Messaging.SingleEmailMessage finishEmail(Database.BatchableContext BC){
    messaging.singleEmailMessage mail = new messaging.singleEmailMessage();
    AsyncApexJob a = [SELECT Id,
                             Status,
                             NumberOfErrors,
                             JobItemsProcessed,
                             TotalJobItems,
                             CompletedDate,
                             ExtendedStatus,
                             ApexClass.name,
                             CreatedBy.Email,
                             CreatedBy.Name
                      FROM AsyncApexJob 
                      WHERE Id =:BC.getJobId()];
    mail.setToAddresses(new string[]{a.CreatedBy.Email});
    mail.setReplyTo('');   // <-----------------------------------Set the ReplyTo---
    mail.setSubject('Batch Code Complete -- '+a.ApexClass.name);
    mail.setUseSignature(false);

    string td1='"border:1px solid green; width=200px;"';
    string td2='"width=200px; border:1px solid green; background-color:red; color:white; font-weight:bold;"';
    string tdHead='"border:1px solid green; width=200px; color:white; background-color:green; font-weight:bold;"';
    string ExtendedStatus='';
    if(a.ExtendedStatus!=null)
        ExtendedStatus=a.ExtendedStatus;
    string tdErrors=td1;
    if(a.NumberOfErrors>0)
        tdErrors=td2;
    string htmlBody = '<div style="border:2px solid green; border-radius:15px;"><p>Hi,</p><p><span style="color:brown; font-weight:bolder;">Salesforce</span> completed running <b>Apex Batch Code<>/b>.</p>'
    +'<p>Results:</p>'
    +'<center><table style="border:3px solid green; border-collapse:collapse;">'
    +'<tr><td style='+tdHead+'>Class Name</td><td style='+tdHead+'>'+a.ApexClass.name+'</td></tr>'
    +'<tr><td style='+td1+'>Completed Date</td><td style='+td1+'>'+a.CompletedDate+'</td></tr>'
    +'<tr><td style='+td1+'>Status</td><td style='+td1+'>'+a.Status+'</td></tr>'
    +'<tr><td style='+td1+'>Job Items Processed</td><td style='+td1+'>'+a.JobItemsProcessed+' / '+a.TotalJobItems+'</td></tr>'
    +'<tr><td style='+td1+'>NumberOfErrors</td><td style='+tdErrors+'>'+a.NumberOfErrors+'</td></tr>'
    +'<tr><td style='+td1+'>Extended Status</td><td style='+td1+'>'+ExtendedStatus+'</td></tr>'
    +'<tr><td style='+tdHead+'>Created By</td><td style='+tdHead+'>'+a.CreatedBy.Name+' ('+a.CreatedBy.Email+')</td></tr>'
    +'</table></center>'
    +'<p>Enjoy the <b>results</b>!</p><p><span style="font-family:"Courier New", Courier, monospace; color:green; font-weight:bold; font-size:larger;">Scott</span></p></div>';

    mail.setHtmlBody(htmlBody);
    return mail;
}

Test Code -- Test_Batch_Code:

@isTest
global class test_Batch implements database.Batchable<sObject>{
    global final string query;

    global test_Batch(){
        query='SELECT id FROM Account';
    }//END test

    global database.QueryLocator start(database.BatchableContext BC){
        return database.getQueryLocator(query);
    }//END start

    global void execute(database.BatchableContext BC,list<sObject> scope){
    }//END execute

    global void finish(database.BatchableContext BC){
        Util.finishEmail(BC);
    }//END finish 

}//END test_Batch

TestMethod:

@isTest private static void testFinishEmail_Batch(){
    list<Account> accts=new list<Account>();
    for(integer i=0;i<(math.round(math.random()*200)+1);i++)
        accts.add(new Account(Name='test'+string.valueof(i)));
    insert accts;
    test.startTest();
    id batchInstance=database.executeBatch(new test_Batch());
    test.stopTest();
}//END testFinishEmail_Batch

(2) Batch Job Status Bar Component: I created a Visualforce Component to visually display the progress of my Batch Jobs:

Controller (w Test Code -- it's version 27.0 SfAPI):

public class batchJobs{

    public List<BatchJob> batchJobs;
    public Integer numberOfJobs {get; set;}

    public List<BatchJob> getBatchJobs(){
    //Create new list of BatchJobs, a wrapper class that includes the job and percent complete.
    batchJobs = new List<BatchJob>();

    //If number of jobs was not defined, default to 20
    if(numberOfJobs== null || numberofJobs <= 0){
        numberofJobs = 20;
    }

    map<string,string> bgColorMap=new map<string,string>();
    bgColorMap.put('Queued','#f8f8f8');
    bgColorMap.put('Processing','#f8f8f8');
    bgColorMap.put('Aborted','#551A8B');
    bgColorMap.put('Completed','#f8f8f8');
    bgColorMap.put('Failed','#9E0508');
    bgColorMap.put('Preparing','#f8f8f8');

    map<string,string> fgColorMap=new map<string,string>();
    fgColorMap.put('Queued','#F7B64B');
    fgColorMap.put('Processing','#F7B64B');
    fgColorMap.put('Aborted','#B23AEE');
    fgColorMap.put('Completed','#20F472');
    fgColorMap.put('Failed','#FFB6C1');
    fgColorMap.put('Preparing','#F7B64B');

    //Query the Batch apex jobs
    for(AsyncApexJob a : [select TotalJobItems, Status, NumberOfErrors, MethodName, JobType, JobItemsProcessed, ExtendedStatus, Id, CreatedDate, CreatedById, CompletedDate, ApexClassId, ApexClass.Name From AsyncApexJob WHERE JobType='BatchApex' order by CreatedDate desc limit :numberOfJobs]){
        Double itemsProcessed = a.JobItemsProcessed;
        Double totalItems = a.TotalJobItems;

        BatchJob j = new BatchJob();
        j.job = a;

        //Determine the pecent complete based on the number of batches complete
        if(totalItems == 0){
            //A little check here as we don't want to divide by 0.
            j.percentComplete = 0;
        }else{
            j.percentComplete = ((itemsProcessed  / totalItems) * 100.0).intValue();
        }
        j.bgStatusColor=bgColorMap.get(a.Status);
        j.fgStatusColor=fgColorMap.get(a.Status);

        batchJobs.add(j);
    }
    return batchJobs;
}

//This is the wrapper class the includes the job itself and a value for the percent complete
public Class BatchJob{
    public AsyncApexJob job {get; set;}
    public Integer percentComplete {get; set;}
    public string bgStatusColor {get;set;}
    public string fgStatusColor {get;set;}

    public BatchJob(){
        this.job=null;
        this.percentComplete=0;
        bgStatusColor='';
        fgStatusColor='';
    }

    /*--------------------TEST METHOD------------------------*/
    static testMethod void batchStatusBarTest(){    
        list<Account> accts=new list<Account>();
        for(integer i=0;i<(math.round(math.random()*200)+1);i++)
            accts.add(new Account(Name='test'+string.valueof(i)));
        insert accts;

        id batchInstance=database.executeBatch(new test_Batch());

        test.startTest();

        batchJobs controller = new batchJobs();
        controller.getBatchJobs();
        id batchInstance2=database.executeBatch(new test_Batch());
        controller.getBatchJobs();

        test.stopTest();
    }//END batchStatusBarTest

}//END class    

VF Component:

<apex:component controller="batchJobs" selfClosing="true">
    <apex:attribute name="cNumberOfJobs" type="Integer" assignTo="{!numberOfJobs}" description="The number of batch jobs to display in the table."/>

<!-- Here is the css styles that will be used for the progress bars -->
<style>
    .progressBar{      
        border:1px solid #DDDDDD;
        height: 19px;
        width: 300px;
        -moz-border-radius: 5px; 
        -webkit-border-radius: 5px;
    }
    .progress{            
        border:1px solid #E78F08;
        height: 100%;
        margin: -1px;
        text-align: center;
        -moz-border-radius: 5px; 
        -webkit-border-radius: 5px;
        line-height: 18px;
    }
    .BadStatus{
        border:1px solid #FF55FF;
        background-color: #CC0000;
        color: white;

    }
</style>
<!--  
    progressBar{background-color: #f8f8f8;}
    progress{background-color: #F7B64B;}
 -->
<!-- This action poller will check the status of the batch jobs every 5 seconds -->
<apex:actionPoller rerender="jobs" interval="5"/>

<apex:pageBlock title="Batch Apex Jobs">
    <apex:pageBlockTable value="{!batchJobs}" var="b" id="jobs">
        <apex:column headerValue="Apex Class" value="{!b.job.ApexClass.Name}"/>
        <apex:column value="{!b.job.CreatedDate}"/>
        <apex:column value="{!b.job.CreatedById}"/>
        <apex:column value="{!b.job.JobItemsProcessed}"/>
        <apex:column value="{!b.job.TotalJobItems}"/>
        <apex:column value="{!b.job.Status}"/>
        <apex:column width="320px" >

            <!-- Here with have two divs that construct our progresses bar. An outter which is the entire bar,
            and and inner that represents the percent complete. We simply pass the percentComplete value to
            the inner div width and this will show how far along the job is. Brilliant! -->

            <div class="progressBar" style="background-color: {!CASE(b.job.Status,'Aborted','#551A8B','Completed','#f8f8f8','Failed','#9E0508','#f8f8f8')};">
                <div class="progress" style="width: {!b.percentComplete}%; background-color: {!CASE(b.job.Status,'Aborted','#B23AEE','Completed','#20F472','Failed','#FFB6C1','#F7B64B')}; color: {!CASE(b.job.Status,'Aborted','white','Failed','white','black')}">
                    {!b.percentComplete}%
                </div>
            </div>

        </apex:column>
        <apex:column value="{!b.job.CompletedDate}"/>
        <apex:column value="{!b.job.ExtendedStatus}" styleClass="{!IF(ISBLANK(b.job.ExtendedStatus),'','BadStatus')}"/>
        <apex:column value="{!b.job.NumberOfErrors}"/>
    </apex:pageBlockTable>
</apex:pageBlock>

3
  • You could also use the new <progress> element (HTML5), which now renders on 75% of globally used browsers (and gaining support daily). Only IE9 users are left out, since <IE9 is no longer supported by salesforce.com as of Winter '14.
    – sfdcfox
    Oct 2, 2013 at 14:31
  • As we have multiple Schedulable and Batchable implementations, I'm trying to consolidate so emailing from each doesn't really suit. The status bar component is cool.
    – Keith C
    Oct 2, 2013 at 14:39
  • The emails I received about IE said "Microsoft has announced that they will end support for IE7 and IE8 in January 2015. Salesforce.com will also stop supporting these older browsers with our Summer ‘15 release, targeted for May 2015.".
    – Keith C
    Oct 2, 2013 at 14:41
0

Jason Venable also has a nice monitor at tehnrd. The problem with the monitors is that they are all just Administrator-oriented since AsyncApexJob object seems to need "View Setup" permissions for the user to view. You can see Jason's monitor code here.

In one of my apps, the END USER initiates the batch process and have to wait for them to complete before they move to the next step. There is no way that I know of to allow the End User to see the "progress" as this and Jason's component do.

Does anyone out there have a solution for this?

1

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .