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>