Monitor Salesforce Limits on a Dashboard

Over the last few months, we have been doing more and more in Salesforce. Adding new integrations, new features, new emails etc. I have been worried that we are not really watching the right things and soon, we will come into work one day and have bricked our org for a day because we have hit a hard limit.

In our offices, we display a dashboard that monitors some of the things that would give us indications that some of the features we have built are not working for one reason or another. But this just tells us if its working or not. I would also like to proactively monitor Salesforce limits that are most important to us. Luckily, there is a REST resource for Limits that allows us to monitor this.

My plan was to use this on our dashboards site so the monitor that displays our existing dashboard can cycle through this. However, according to the documentation, the user who is executing the Limits resource needs to have View Setup and Configuration and some of the Limits will require Manage User permissions. Unfortunately, for me, the user who will be executing the page can’t get these permissions. No matter how I access the resource, even when providing OAuth authentication with another user, I still am not able to execute and receive the limits appropriately. Pretty big bummer actually.

There is also a Limits class that has some cool stuff, but I don’t think this has most of what I’m looking to do. My options are to log into the dashboard monitor as a real user, and possibly allow someone to find the keyboard and get access to our org, or think of a different way to get the Limits resource data and display it on this monitor.

After kicking this around for a few weeks, going back and forth on if this was even valuable, we almost hit a limit and my decision was made for me. This not only had to be done, but needed to be displayed for all to see to help report a problem.

I decided that the easiest way to do this was to run a scheduled job at a regular interval and populate data into a custom setting. Then the page which would be available on a “hidden” public site would just render the data from the custom setting.

Quick note: some of this code probably sucks. There are most likely other ways of doing this, but I wanted to slap something together quickly so we could get this up and running. If you want to grab this code, make sure you understand what it does and think about optimizing it.

Ok, so let’s start with the REST resource. I have a class called ‘populateLimitsScheduler’ which is called as a scheduled job. I am running it every 15 minutes. The class uses ‘implements Schedulable’ so that it can be scheduled. It has an Execute method to trigger the API call, a method called populateLimits() to organize the data and save it, a method called runAPI() to access the REST resource and a wrapper for the incoming Limits. Lets start with the Wrapper:

public class sfLimit{
public String name;
public Integer max;
public Integer remaining;
public sfLimit(String name, Integer max, Integer remaining){
this.name = name;
this.max = max;
this.remaining = remaining;
}
}

view raw
sfLimit
hosted with ❤ by GitHub

If you execute the Limits resource by itself, you will notice that the limits will come with a name, a max and a remaining value. The Execute method will be used by the scheduled job to run the populateLimits method:

 

public void execute (SchedulableContext SC){
populateLimits();
}

view raw
execute
hosted with ❤ by GitHub

The ‘populateLimits’ method is where we are going to setup all of our data, assign variables and upsert into our Custom Setting:

@future(callout=true)
public static void populateLimits(){
Datetime now = DateTime.Now();
List<sfLimit> lim = runAPI();
Map<String,sfLimit> lMap = new Map<String,sfLimit>();
for(sfLimit l : lim){
system.debug(l);
lMap.put(l.name, l);
}
List<Limits__c> limitList = new List<Limits__c>();
for(sfLimit l : lMap.values()){
Limits__c limitInstance = new Limits__c();
limitInstance.Name = l.name != null ? l.name : 'Missing Name';
limitInstance.Max__c = l.max != null ? l.max : 0;
limitInstance.Remaining__c = l.remaining != null ? l.remaining : 0;
limitInstance.Used__c = l.max != null && l.remaining != null ? l.max – l.remaining : 0;
limitList.add(limitInstance);
}
try{
upsert limitList Name;
} catch(DMLException e){
system.debug('Error: ' + e);
}
}

view raw
populateLimits()
hosted with ❤ by GitHub

You’ll notice that we call runAPI() and assign the values to a List of sfLimits. This method will access the Limits resource, parse the response and create a bunch of sfLimits:

public static List<sfLimit> runAPI(){
//Get a token
String clientId = Label.Limits_Connected_App_Client;
String clientSecret = Label.Limits_Connected_App_Key;
String username= Label.Limits_Username;
String password= Label.Limits_PW;
//using this request body we'll make API call
String reqbody = 'grant_type=password&client_id='+clientId+'&client_secret='+clientSecret+'&username='+username+'&password='+password;
String sfdcURL = URL.getSalesforceBaseUrl().toExternalForm();
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setBody(reqbody);
req.setMethod('POST');
String endpoint = Utilities.isProduction() ? 'https://login.salesforce.com/services/oauth2/token&#39; : 'https://test.salesforce.com/services/oauth2/token&#39;;
system.debug(endpoint);
req.setEndpoint(endpoint);
HttpResponse res = h.send(req);
OAuth2 objAuthInfo =(OAuth2)JSON.deserialize(res.getbody(), OAuth2.class);
system.debug(objAuthInfo);
String restAPIURL = sfdcURL + '/services/data/v41.0/limits';
HttpRequest httpRequest = new HttpRequest();
httpRequest.setMethod('GET');
httpRequest.setHeader('Authorization', 'Bearer ' + objAuthInfo.access_token);
httpRequest.setEndpoint(restAPIURL);
List<sfLimit> l = new List<sfLimit>();
String response = '';
try {
Http http = new Http();
HttpResponse httpResponse = http.send(httpRequest);
if (httpResponse.getStatusCode() == 200 ) {
String JSONContent = httpResponse.getBody();
System.debug(JSONContent);
JSONParser parserJira = JSON.createParser(JSONContent);
String lName;
String innerName;
Integer Max;
Integer Remaining;
String text;
Integer innerMax;
Integer innerRemaining;
while (parserJira.nextToken() != null) {
if (parserJira.getCurrentToken() == JSONToken.FIELD_NAME) {
Max = null;
Remaining = null;
lName = null;
text = parserJira.getText();
lName = text;
if (parserJira.nextToken() != JSONToken.VALUE_NULL) {
if (text == 'Max') {
Max = parserJira.getIntegerValue();
} else if (text == 'Remaining') {
Remaining = parserJira.getIntegerValue();
} else {
//inner Limits
if(parserJira.nextToken() != JSONToken.END_OBJECT){
if(parserJira.getText() == 'Max'){
parserJira.nextToken();
innerMax = parserJira.getIntegerValue();
parserJira.nextToken();
}
if(parserJira.getText() == 'Remaining'){
parserJira.nextToken();
innerRemaining = parserJira.getIntegerValue();
}
}
Max = innerMax;
Remaining = innerRemaining;
consumeObject(parserJira);
}
}
}
sfLimit sfl = new sfLimit(text,Max,Remaining);
system.debug(sfl);
l.add(sfl);
}
system.debug(l);
return l;
} else {
System.debug(' httpResponse ' + httpResponse.getBody() );
throw new CalloutException( httpResponse.getBody() );
return null;
}
} catch( System.Exception e) {
System.debug('ERROR: '+ e);
throw e;
return null;
}
}

view raw
runAPI
hosted with ❤ by GitHub

This thing is REALLY complicated. I attempted to use JSON2Apex by pasting the response from Limits, but it was taking forever and the structure of the Limits response kind of makes it a pain to use. So, instead of using JSON2Apex, I created my own JSON parser for the structure of the Limits Resource. What makes it problematic is that limits are imbedded within other limits. There is probably an easy way to parse the response with JSON2Apex, but I was just having all kinds of issues and just did it on my own. This is probably the part that could use a rewrite.

ANYWAY, this method attempts to login using OAuth with a system user and then once a valid token is returned, access the Limits resource. We then parse the response from the Limits resource from the format we get and then make sfLimits out of each one. One problem that happens with this resource, is there are some Limits that are duplicated in the response under different parent Limits. This causes some funkiness but we try to resolve this later.

Once we return all of the sfLimits in runAPI to populateLimits, we put them into a Map to attempt to de-dupe based on the Limit name since we should never have more than 1 Limit with the same name anyway. After we created the Map, we create individual records in our custom setting called Limits__c and populate with objects with the variable name limitInstance. Then we just upsert a list of these based on the Name to replace the existing Limits in the custom setting. This data will just keep being replaced over and over again to get the newest numbers. Maybe some day we will redesign this to store data over time to do comparisons, but right now, I just want to show the current data. Complete code at the end of the blog post.

So after we have this code, we can schedule it every 15 minutes by running this in dev console:

System.schedule('Limits Data Pull 00', '0 0 * * * ?', new populateLimitsScheduler());
System.schedule('Limits Data Pull 15', '0 15 * * * ?', new populateLimitsScheduler());
System.schedule('Limits Data Pull 30', '0 30 * * * ?', new populateLimitsScheduler());
System.schedule('Limits Data Pull 45', '0 45 * * * ?', new populateLimitsScheduler());

So now every 15 minutes, the Limits API will be accessed, limits pulled down and saved to our custom setting.

We have Data!

Screen Shot 2017-12-17 at 4.10.52 PM.png

Now we need a page though that will display the data we want. There are 6 limits that I want to monitor based on some of our previous experience and things that most companies will probably have trouble with:

  1. Daily API Limit
  2. Daily Asynchronous Apex
  3. Hourly oData Callout Limit (External Objects)
  4. Daily Emails sent from Apex
  5. Daily Workflow Emails
  6. Hourly Scheduled Workflow

We can then just create a Page and a Controller. The controller is pretty simple:

public without sharing class dailyLimitsController {
public String nowTime {get;set;}
public Integer dailyAPIMax {get;set;}
public Integer dailyAPIRem {get;set;}
public Integer oDataMax {get;set;}
public Integer oDataRem {get;set;}
public Integer wfMax {get;set;}
public Integer wfRem {get;set;}
public Integer wfeMax {get;set;}
public Integer wfeRem {get;set;}
public Integer singleMax {get;set;}
public Integer singleRem {get;set;}
public Integer asyncMax {get;set;}
public Integer asyncRem {get;set;}
public dailyLimitsController(){
Limits__c wfemail = Limits__c.getInstance('DailyWorkflowEmails');
wfeMax = Integer.valueOf(wfemail.Max__c);
wfeRem = Integer.valueOf(wfemail.Remaining__c);
Limits__c dailyAPI = Limits__c.getInstance('DailyApiRequests');
dailyAPIMax = Integer.valueOf(dailyAPI.Max__c);
dailyAPIRem = Integer.valueOf(dailyAPI.Remaining__c);
Limits__c oData = Limits__c.getInstance('HourlyODataCallout');
oDataMax = Integer.valueOf(oData.Max__c);
oDataRem = Integer.valueOf(oData.Remaining__c);
Limits__c wfHourly = Limits__c.getInstance('HourlyTimeBasedWorkflow');
wfMax = Integer.valueOf(wfHourly.Max__c);
wfRem = Integer.valueOf(wfHourly.Remaining__c);
Limits__c singleEmail = Limits__c.getInstance('SingleEmail');
singleMax = Integer.valueOf(singleEmail.Max__c);
singleRem = Integer.valueOf(singleEmail.Remaining__c);
Limits__c async = Limits__c.getInstance('DailyAsyncApexExecutions');
asyncMax = Integer.valueOf(async.Max__c);
asyncRem = Integer.valueOf(async.Remaining__c);
nowTime = async.LastModifiedDate.format('MM/dd/yyyy HH:mm:ss a');
}
}

And the page:

<apex:page showHeader="false" sidebar="false" controller="dailyLimitsController" standardStylesheets="false">
<head>
<title>Salesforce Limits Manager</title>
</head>
<apex:includeScript value="https://ajax.googleapis.com/ajax/libs/jquery/1.12.4/jquery.min.js"/&gt;
<apex:includeScript value="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js"/&gt;
<apex:stylesheet value="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css"/&gt;
<script src = "https://code.highcharts.com/highcharts.js"></script&gt;
<script src = "https://code.highcharts.com/highcharts-more.js"></script&gt;
<script src = "https://code.highcharts.com/modules/solid-gauge.js"></script&gt;
<style>
.highcharts-yaxis-grid .highcharts-grid-line {
display: none;
}
</style>
<script type="text/javascript">
$(function () {
var chartype = {
type: 'solidgauge'
}
var chartitle = null
var chartpane = {
center: ['50%', '85%'],
size: '140%',
startAngle: -90,
endAngle: 90,
background: {
backgroundColor: (Highcharts.theme && Highcharts.theme.background2) || '#EEE',
innerRadius: '60%',
outerRadius: '100%',
shape: 'arc'
}
}
var chartooltip = {
enabled: false
}
var chartyaxis = {
stops: [
[0.1, '#55BF3B'], // green
[0.5, '#DDDF0D'], // yellow
[0.9, '#DF5353'] // red
],
lineWidth: 0,
minorTickInterval: null,
//tickPixelInterval: 400,
tickWidth: 0,
title: {
y: -70
},
labels: {
y: 16
}
}
var chartplotoptions = {
solidgauge: {
dataLabels: {
y: 5,
borderWidth: 0,
useHTML: true
}
}
}
var gaugeOptions = {
chart:chartype,
title: chartitle,
pane: chartpane,
tooltip:chartooltip,
yAxis: chartyaxis,
plotOptions: chartplotoptions
};
$('#container-dailyAPI').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Daily API Limit'
},
subtitle: {
text: 'Rolling 24 hours'
},
yAxis: {
min: 0,
max: {!dailyAPIMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Daily API',
data: [{!dailyAPIMax}-{!dailyAPIRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!dailyAPIMax} max</span></div>'+
'<span style="font-size:12px;">{!dailyAPIRem} remaining</span></div>'
},
}]
}));
$('#container-oData').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Hourly oData Callout Limit'
},
subtitle: {
text: 'Salesforce \"Lightning Connect\" or External Object'
},
yAxis: {
min: 0,
max: {!oDataMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Hourly oData',
data: [{!oDataMax}-{!oDataRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!oDataMax} max</span></div>'+
'<span style="font-size:12px;">{!oDataRem} remaining</span></div>'
},
}]
}));
$('#container-async').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Daily Asynchronous Apex'
},
subtitle: {
text: 'Rolling 24 hours'
},
yAxis: {
min: 0,
max: {!asyncMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Daily Async Apex',
data: [{!asyncMax}-{!asyncRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!asyncMax} max</span></div>'+
'<span style="font-size:12px;">{!asyncRem} remaining</span></div>'
},
}]
}));
$('#container-hourlyWF').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Hourly Scheduled Workflow'
},
yAxis: {
min: 0,
max: {!wfMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Hourly Scheduled Workflow',
data: [{!wfMax}-{!wfRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!wfMax} max</span></div>'+
'<span style="font-size:12px;">{!wfRem} remaining</span></div>'
},
}]
}));
$('#container-dailyEmail').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Daily Workflow Emails'
},
subtitle: {
text: 'Rolling 24 hours'
},
yAxis: {
min: 0,
max: {!wfeMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Daily WF Email',
data: [{!wfeMax}-{!wfeRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!wfeMax} max</span></div>'+
'<span style="font-size:12px;">{!wfeRem} remaining</span></div>'
},
}]
}));
$('#container-singleEmail').highcharts(Highcharts.merge(gaugeOptions, {
title: {
text: 'Daily Emails sent from Apex'
},
subtitle: {
text: 'Rolling 24 hours'
},
yAxis: {
min: 0,
max: {!singleMax},
endOnTick:false
},
credits: {
enabled: false
},
series: [{
name: 'Daily Single Email',
data: [{!singleMax}-{!singleRem}],
dataLabels: {
format: '<div style="text-align:center"><span style="font-size:25px;color:' +
((Highcharts.theme && Highcharts.theme.contrastTextColor) || 'black') + '">{y}</span><br/>'+
'<span style="font-size:12px;">{!singleMax} max</span></div>'+
'<span style="font-size:12px;">{!singleRem} remaining</span></div>'
},
}]
}));
});
</script>
<apex:form >
<div class="container-fluid" style="padding-top: 10px;">
<div class="row" style="padding-left: 10px;">
<div class="col-md-3">
<img src="https://watchguard–c.na42.content.force.com/servlet/servlet.ImageServer?id=015F0000005rYrw&oid=00DA0000000KLks&lastMod=1465939806000"/&gt;
</div>
<div class="col-md-6">
<center><h2>Salesforce Daily Limits</h2></center>
</div>
<div class="col-md-3" style="text-align: right;">
<apex:outputText value="Data Last Refreshed: {!nowTime}" />
</div>
</div>
</div>
<br/>
<div class="container-fluid">
<div class="row">
<apex:outputPanel id="dashboard">
<div class="col-md-4 col-md-6">
<div id="container-dailyAPI"></div>
<div id="container-singleEmail"></div>
</div>
<div class="col-md-4 col-md-6">
<div id="container-async"></div>
<div id="container-dailyEmail"></div>
</div>
<div class="col-lg-4 col-md-6">
<div id="container-oData"></div>
<div id="container-hourlyWF"></div>
</div>
</apex:outputPanel>
</div>
</div>
</apex:form>
</apex:page>

view raw
limits
hosted with ❤ by GitHub

We are using a couple of libraries such as jQuery and Bootstrap and then also my favorite charting library, Highcharts.

Then, we get a page that looks like this:

Screen Shot 2017-12-17 at 3.54.12 PM

It is responsive with the Bootstrap and Highcharts, so looks pretty great on mobile too!

Last, we make this page available on our public dashboards site so it is accessible, but the underlying data, API access and credentials for the OAuth callout is not which is pretty cool. So now, we have our two dashboards that help our Salesforce team make sure they can easily monitor some of the things that we are important to us daily. When all is said and done, we come up with this for all to see in our IT department:

2017-12-18 10.28.18
Limits Dashboard on a wall with idea paint (note the smiley face)

populateLimitsScheduler (shoutout to  for their Superfell & Metadaddy for their consumeObject concept from JSON2Apex):

public class populateLimitsScheduler implements Schedulable{
public void execute (SchedulableContext SC){
populateLimits();
}
@future(callout=true)
public static void populateLimits(){
Datetime now = DateTime.Now();
List<sfLimit> lim = runAPI();
Map<String,sfLimit> lMap = new Map<String,sfLimit>();
for(sfLimit l : lim){
system.debug(l);
lMap.put(l.name, l);
}
List<Limits__c> limitList = new List<Limits__c>();
for(sfLimit l : lMap.values()){
Limits__c limitInstance = new Limits__c();
limitInstance.Name = l.name != null ? l.name : 'Missing Name';
limitInstance.Max__c = l.max != null ? l.max : 0;
limitInstance.Remaining__c = l.remaining != null ? l.remaining : 0;
limitInstance.Used__c = l.max != null && l.remaining != null ? l.max – l.remaining : 0;
limitList.add(limitInstance);
}
try{
upsert limitList Name;
} catch(DMLException e){
system.debug('Error: ' + e);
}
}
public static List<sfLimit> runAPI(){
//Get a token
String clientId = Label.Limits_Connected_App_Client;
String clientSecret = Label.Limits_Connected_App_Key;
String username= Label.Limits_Username;
String password= Label.Limits_PW;
//using this request body we'll make API call
String reqbody = 'grant_type=password&client_id='+clientId+'&client_secret='+clientSecret+'&username='+username+'&password='+password;
String sfdcURL = URL.getSalesforceBaseUrl().toExternalForm();
Http h = new Http();
HttpRequest req = new HttpRequest();
req.setBody(reqbody);
req.setMethod('POST');
String endpoint = Utilities.isProduction() ? 'https://login.salesforce.com/services/oauth2/token&#39; : 'https://test.salesforce.com/services/oauth2/token&#39;;
system.debug(endpoint);
req.setEndpoint(endpoint);
HttpResponse res = h.send(req);
OAuth2 objAuthInfo =(OAuth2)JSON.deserialize(res.getbody(), OAuth2.class);
system.debug(objAuthInfo);
String restAPIURL = sfdcURL + '/services/data/v41.0/limits';
HttpRequest httpRequest = new HttpRequest();
httpRequest.setMethod('GET');
httpRequest.setHeader('Authorization', 'Bearer ' + objAuthInfo.access_token);
httpRequest.setEndpoint(restAPIURL);
List<sfLimit> l = new List<sfLimit>();
String response = '';
try {
Http http = new Http();
HttpResponse httpResponse = http.send(httpRequest);
if (httpResponse.getStatusCode() == 200 ) {
String JSONContent = httpResponse.getBody();
System.debug(JSONContent);
JSONParser parserJira = JSON.createParser(JSONContent);
String lName;
String innerName;
Integer Max;
Integer Remaining;
String text;
Integer innerMax;
Integer innerRemaining;
while (parserJira.nextToken() != null) {
if (parserJira.getCurrentToken() == JSONToken.FIELD_NAME) {
Max = null;
Remaining = null;
lName = null;
text = parserJira.getText();
lName = text;
if (parserJira.nextToken() != JSONToken.VALUE_NULL) {
if (text == 'Max') {
Max = parserJira.getIntegerValue();
} else if (text == 'Remaining') {
Remaining = parserJira.getIntegerValue();
} else {
//inner Limits
if(parserJira.nextToken() != JSONToken.END_OBJECT){
if(parserJira.getText() == 'Max'){
parserJira.nextToken();
innerMax = parserJira.getIntegerValue();
parserJira.nextToken();
}
if(parserJira.getText() == 'Remaining'){
parserJira.nextToken();
innerRemaining = parserJira.getIntegerValue();
}
}
Max = innerMax;
Remaining = innerRemaining;
consumeObject(parserJira);
}
}
}
sfLimit sfl = new sfLimit(text,Max,Remaining);
system.debug(sfl);
l.add(sfl);
}
system.debug(l);
return l;
} else {
System.debug(' httpResponse ' + httpResponse.getBody() );
throw new CalloutException( httpResponse.getBody() );
return null;
}
} catch( System.Exception e) {
System.debug('ERROR: '+ e);
throw e;
return null;
}
}
public static void consumeObject(JSONParser parser) {
Integer depth = 0;
do {
JSONToken curr = parser.getCurrentToken();
if (curr == JSONToken.START_OBJECT ||
curr == JSONToken.START_ARRAY) {
depth++;
} else if (curr == JSONToken.END_OBJECT ||
curr == JSONToken.END_ARRAY) {
depth–;
}
} while (depth > 0 && parser.nextToken() != null);
}
public class sfLimit{
public String name;
public Integer max;
public Integer remaining;
public sfLimit(String name, Integer max, Integer remaining){
this.name = name;
this.max = max;
this.remaining = remaining;
}
}
public class OAuth2{
public String issued_at{get;set;}
public String instance_url{get;set;}
public String signature{get;set;}
public String access_token{get;set;}
}
}

Test for our code:

@isTest
public class dailyLimitsController_TEST {
static testmethod void testAdminLimitsManager(){
Limits__c l = new Limits__c(Name='DailyWorkflowEmails', Max__c = 100, Remaining__c = 50);
insert l;
Limits__c l1 = new Limits__c(Name='DailyApiRequests', Max__c = 100, Remaining__c = 50);
insert l1;
Limits__c l2 = new Limits__c(Name='HourlyODataCallout', Max__c = 100, Remaining__c = 50);
insert l2;
Limits__c l3 = new Limits__c(Name='HourlyTimeBasedWorkflow', Max__c = 100, Remaining__c = 50);
insert l3;
Limits__c l4 = new Limits__c(Name='SingleEmail', Max__c = 100, Remaining__c = 50);
insert l4;
Limits__c l5 = new Limits__c(Name='DailyAsyncApexExecutions', Max__c = 100, Remaining__c = 50);
insert l5;
Test.startTest();
PageReference pageRef = Page.adminLimitsManager;
Test.setCurrentPage(pageRef);
dailyLimitsController controller = new dailyLimitsController();
Test.stopTest();
}
public static testmethod void testPopulateLimitsScheduler(){
String hour = String.valueOf(Datetime.now().hour());
String min = String.valueOf(Datetime.now().minute());
String ss = String.valueOf(Datetime.now().second());
//parse to cron expression
String nextFireTime = ss + ' ' + min + ' ' + hour + ' * * ?';
Test.startTest();
Test.setMock(HttpCalloutMock.class, new limitsMock());
populateLimitsScheduler p = new populateLimitsScheduler();
system.schedule('testPopulateLimitsScheduler',nextFireTime, p);
Test.stopTest();
}
}

Mock for our Limits callout for the test:

@isTest
global with sharing class limitsMock implements HTTPCalloutMock{
global HTTPResponse respond(HTTPRequest req){
HttpResponse res = new HTTPResponse();
res.setHeader('Content-Type', 'application/JSON');
res.setBody('{ "ConcurrentAsyncGetReportInstances" : { "Max" : 200, "Remaining" : 200 }, "ConcurrentSyncReportRuns" : { "Max" : 20, "Remaining" : 20 }, "DailyApiRequests" : { "Max" : 6450000, "Remaining" : 4988570, "Ant Migration Tool" : { "Max" : 0, "Remaining" : 0 }, "Chatter Desktop" : { "Max" : 0, "Remaining" : 0 }, "Chatter Mobile for BlackBerry" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Bulk" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Partner" : { "Max" : 0, "Remaining" : 0 }, "Force.com IDE" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Chatter" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Files" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Mobile Dashboards" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Touch" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Android" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Outlook" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for iOS" : { "Max" : 0, "Remaining" : 0 }, "SalesforceA" : { "Max" : 0, "Remaining" : 0 }, "Workbench" : { "Max" : 0, "Remaining" : 0 }, "inContact Integration" : { "Max" : 0, "Remaining" : 0 } }, "DailyAsyncApexExecutions" : { "Max" : 350000, "Remaining" : 342356 }, "DailyBulkApiRequests" : { "Max" : 10000, "Remaining" : 9954, "Ant Migration Tool" : { "Max" : 0, "Remaining" : 0 }, "Chatter Desktop" : { "Max" : 0, "Remaining" : 0 }, "Chatter Mobile for BlackBerry" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Bulk" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Partner" : { "Max" : 0, "Remaining" : 0 }, "Force.com IDE" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Chatter" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Files" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Mobile Dashboards" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Touch" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Android" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Outlook" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for iOS" : { "Max" : 0, "Remaining" : 0 }, "SalesforceA" : { "Max" : 0, "Remaining" : 0 }, "Workbench" : { "Max" : 0, "Remaining" : 0 }, "inContact Integration" : { "Max" : 0, "Remaining" : 0 } }, "DailyDurableGenericStreamingApiEvents" : { "Max" : 1000000, "Remaining" : 1000000 }, "DailyDurableStreamingApiEvents" : { "Max" : 1000000, "Remaining" : 1000000 }, "DailyGenericStreamingApiEvents" : { "Max" : 10000, "Remaining" : 10000, "Ant Migration Tool" : { "Max" : 0, "Remaining" : 0 }, "Chatter Desktop" : { "Max" : 0, "Remaining" : 0 }, "Chatter Mobile for BlackBerry" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Bulk" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Partner" : { "Max" : 0, "Remaining" : 0 }, "Force.com IDE" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Chatter" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Files" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Mobile Dashboards" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Touch" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Android" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Outlook" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for iOS" : { "Max" : 0, "Remaining" : 0 }, "SalesforceA" : { "Max" : 0, "Remaining" : 0 }, "Workbench" : { "Max" : 0, "Remaining" : 0 }, "inContact Integration" : { "Max" : 0, "Remaining" : 0 } }, "DailyStreamingApiEvents" : { "Max" : 1000000, "Remaining" : 999747, "Ant Migration Tool" : { "Max" : 0, "Remaining" : 0 }, "Chatter Desktop" : { "Max" : 0, "Remaining" : 0 }, "Chatter Mobile for BlackBerry" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Bulk" : { "Max" : 0, "Remaining" : 0 }, "Dataloader Partner" : { "Max" : 0, "Remaining" : 0 }, "Force.com IDE" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Chatter" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Files" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Mobile Dashboards" : { "Max" : 0, "Remaining" : 0 }, "Salesforce Touch" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Android" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for Outlook" : { "Max" : 0, "Remaining" : 0 }, "Salesforce for iOS" : { "Max" : 0, "Remaining" : 0 }, "SalesforceA" : { "Max" : 0, "Remaining" : 0 }, "Workbench" : { "Max" : 0, "Remaining" : 0 }, "inContact Integration" : { "Max" : 0, "Remaining" : 0 } }, "DailyWorkflowEmails" : { "Max" : 487000, "Remaining" : 473334 }, "DataStorageMB" : { "Max" : 323422, "Remaining" : 253722 }, "DurableStreamingApiConcurrentClients" : { "Max" : 2000, "Remaining" : 1999 }, "FileStorageMB" : { "Max" : 1107627, "Remaining" : 677213 }, "HourlyAsyncReportRuns" : { "Max" : 1200, "Remaining" : 1200 }, "HourlyDashboardRefreshes" : { "Max" : 200, "Remaining" : 200 }, "HourlyDashboardResults" : { "Max" : 5000, "Remaining" : 5000 }, "HourlyDashboardStatuses" : { "Max" : 999999999, "Remaining" : 999999999 }, "HourlyODataCallout" : { "Max" : 20000, "Remaining" : 18181 }, "HourlySyncReportRuns" : { "Max" : 500, "Remaining" : 500 }, "HourlyTimeBasedWorkflow" : { "Max" : 3000, "Remaining" : 3000 }, "MassEmail" : { "Max" : 5000, "Remaining" : 5000 }, "PermissionSets" : { "Max" : 1500, "Remaining" : 1443, "CreateCustom" : { "Max" : 1000, "Remaining" : 954 } }, "SingleEmail" : { "Max" : 5000, "Remaining" : 4988 }, "StreamingApiConcurrentClients" : { "Max" : 2000, "Remaining" : 2000 } }');
res.setStatusCode(200);
return res;
}
}

view raw
limitsMock
hosted with ❤ by GitHub

 


5 thoughts on “Monitor Salesforce Limits on a Dashboard

  1. Hi,

    Very knowlegeable article. I am working on a close requirement as mentioned above.

    My requirement is as mentioned below:

    Notification alert for Async Apex Execution Limits
    I want to send an alert notification when overall organisation Async Apex Execution limit reached 70%of total limit(24hrs API Limit). Say if limit is 2500 and if system already exhausted 1750 of daily Async Apex Limit out of 2500 then any alert should be send to few persons notifying that your organisation limit of Async Apex Executions have reached 70% threshold.

    Kindly help me.

    Any help will be greatly appreciated.

    Many thanks in advance

    Thanks & Regards,
    Harjeet

    Like

    1. I think you could easily do this without the batch job stuff, although it might be helpful so you’re not constantly monitoring the API usage for every transaction.

      I think if you followed a similar pattern, with either an internal page (where you didn’t need a batch) or use a batch to store the most recent limits, you could write automation based on the saved data (remaining/total) and depending on the value returned for remaining/total, send an email or something. You would need to worry about sending duplicate emails of course.

      I think all of the components are here if you wanted to do this yourself, except for the automation for the email. In my case, you may need to change the structure to a standard object instead of a custom setting and then looking for the ‘DailyApiRequests’ limit in your object and compare the two returned values like above.

      Like

    2. If you follow the pattern exactly or similarly, you should be able to do a comparison of used/available for a specific limit and take action either within the batch or after by sending an email or creating a chatter post, etc. If you do it at the time of comparison in your batch that seems easy enough but you’re going to have to write your own code for that 😉

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s