Skip to content

Commit

Permalink
Trello integration
Browse files Browse the repository at this point in the history
* Added TrelloLogPusher batch class to handle pushing logs to Slack
* Added TrelloLogPushScheduler schedulable class to schedule pushing logs to Slack
* Added Trello record to LoggerIntegration__mdt custom metadata type
* Added new fields ApiKey__c & UrlParameters__c to LoggerIntegration__mdt.object
* Added new Trello fields PushToTrello__c and PushedToTrelloDate__c on Log__c object
* Added remote site setting for Trello API
* Added batch size of 100 for Loggly & Slack schedulers to avoid hitting callout limits & limit JSON size
* Added exceptions in Slack & Loggly when status code >= 400
  • Loading branch information
jongpie committed Jul 16, 2018
1 parent 2eb887e commit 6dbcdbc
Show file tree
Hide file tree
Showing 20 changed files with 394 additions and 10 deletions.
2 changes: 1 addition & 1 deletion src/classes/LogglyLogPushScheduler.cls
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public without sharing class LogglyLogPushScheduler implements System.Schedulabl
// Any records that need to be processed will be processed the next time the job executes
if(this.getNumberOfRunningBatchJobs() >= 5) return;

Database.executebatch(new LogglyLogPusher());
Database.executebatch(new LogglyLogPusher(), 100);
}

private Integer getNumberOfRunningBatchJobs() {
Expand Down
10 changes: 8 additions & 2 deletions src/classes/LogglyLogPusher.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
public without sharing class LogglyLogPusher implements Database.AllowsCallouts, Database.Batchable<Log__c> {

private static final Organization ORG = [SELECT Id, IsSandbox FROM Organization LIMIT 1];
private static final LoggerIntegration__mdt SETTINGS = [SELECT ApiToken__c, BaseUrl__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Loggly'];
private static final LoggerIntegration__mdt SETTINGS = [SELECT ApiToken__c, BaseUrl__c, UrlParameters__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Loggly'];

public List<Log__c> start(Database.BatchableContext batchableContext) {
return [
Expand All @@ -29,13 +29,17 @@ public without sharing class LogglyLogPusher implements Database.AllowsCallouts,
log.PushedToLogglyDate__c = System.now();
}

String urlParameters = SETTINGS.UrlParameters__c == null ? '' : '?' + SETTINGS.UrlParameters__c.replace('\n', '&');
HttpRequest request = new HttpRequest();
request.setEndpoint(SETTINGS.BaseUrl__c + '/bulk/' + SETTINGS.ApiToken__c + '/tag/salesforce/');
request.setEndpoint(SETTINGS.BaseUrl__c + '/bulk/' + SETTINGS.ApiToken__c + '/tag/salesforce/' + urlParameters);
request.setMethod('POST');
request.setHeader('Content-Type', 'text/plain');
request.setBody(String.join(logEntryStrings, '\n'));

HttpResponse response = new Http().send(request);

if(response.getStatusCode() >= 400) throw new LogglyApiException(response.getBody());

update logs;
}

Expand Down Expand Up @@ -103,6 +107,8 @@ public without sharing class LogglyLogPusher implements Database.AllowsCallouts,
return log;
}

private class LogglyApiException extends Exception {}

private class LogDto {
public String className;
public String exceptionStackTrace;
Expand Down
6 changes: 5 additions & 1 deletion src/classes/SlackLogPushScheduler.cls
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
/*************************************************************************************************
* This file is part of the Nebula Logger project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. *
*************************************************************************************************/
public without sharing class SlackLogPushScheduler implements System.Schedulable {

public static void scheduleEveryXMinutes(Integer x) {
Expand All @@ -22,7 +26,7 @@ public without sharing class SlackLogPushScheduler implements System.Schedulable
// Any records that need to be processed will be processed the next time the job executes
if(this.getNumberOfRunningBatchJobs() >= 5) return;

Database.executebatch(new SlackLogPusher());
Database.executebatch(new SlackLogPusher(), 100);
}

private Integer getNumberOfRunningBatchJobs() {
Expand Down
12 changes: 8 additions & 4 deletions src/classes/SlackLogPusher.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
public without sharing class SlackLogPusher implements Database.AllowsCallouts, Database.Batchable<Log__c> {

private static final Organization ORG = [SELECT Id, IsSandbox FROM Organization LIMIT 1];
private static final LoggerIntegration__mdt SETTINGS = [SELECT ApiToken__c, BaseUrl__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Slack'];
private static final LoggerIntegration__mdt SETTINGS = [SELECT ApiToken__c, BaseUrl__c, UrlParameters__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Slack'];

public List<Log__c> start(Database.BatchableContext batchableContext) {
return [
Expand All @@ -27,8 +27,9 @@ public without sharing class SlackLogPusher implements Database.AllowsCallouts,
notification.attachments = new List<LogDto>();
notification.attachments.add(this.convertLog(log));

String urlParameters = SETTINGS.UrlParameters__c == null ? '' : '?' + SETTINGS.UrlParameters__c.replace('\n', '&');
HttpRequest request = new HttpRequest();
request.setEndpoint(SETTINGS.BaseUrl__c + SETTINGS.ApiToken__c);
request.setEndpoint(SETTINGS.BaseUrl__c + '/services/' + SETTINGS.ApiToken__c + urlParameters);
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
String jsonString = Json.serialize(notification);
Expand All @@ -38,6 +39,8 @@ public without sharing class SlackLogPusher implements Database.AllowsCallouts,

HttpResponse response = new Http().send(request);

if(response.getStatusCode() >= 400) throw new SlackApiException(response.getBody());

log.PushedToSlackDate__c = System.now();
}

Expand Down Expand Up @@ -90,7 +93,6 @@ public without sharing class SlackLogPusher implements Database.AllowsCallouts,
for(TopicAssignment topicAssignment : log.TopicAssignments) {
topicNames.add(topicAssignment.Topic.Name);
}
topicNames.sort();

if(topicNames.isEmpty()) return notification;

Expand All @@ -103,6 +105,8 @@ public without sharing class SlackLogPusher implements Database.AllowsCallouts,
return notification;
}

private class SlackApiException extends Exception {}

private class NotificationDto {
public List<LogDto> attachments;
public String text;
Expand All @@ -111,8 +115,8 @@ public without sharing class SlackLogPusher implements Database.AllowsCallouts,
private class LogDto {
public List<ActionDto> actions;
public String author_name;
public String author_link;
public String author_icon;
public String author_link;
public String color;
public String fallback;
public List<FieldDto> fields;
Expand Down
36 changes: 36 additions & 0 deletions src/classes/TrelloLogPushScheduler.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*************************************************************************************************
* This file is part of the Nebula Logger project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. *
*************************************************************************************************/
public without sharing class TrelloLogPushScheduler implements System.Schedulable {

public static void scheduleEveryXMinutes(Integer x) {
for(Integer i = 0; i < 60; i += x) {
scheduleHourly(i);
}
}

public static void scheduleHourly(Integer startingMinuteInHour) {
String minuteString = String.valueOf(startingMinuteInHour);
minuteString = minuteString.leftPad(2, '0');
scheduleHourly(startingMinuteInHour, 'Trello Log Sync: Every Hour at ' + minuteString);
}

public static void scheduleHourly(Integer startingMinuteInHour, String jobName) {
System.schedule(jobName, '0 ' + startingMinuteInHour + ' * * * ?', new TrelloLogPushScheduler());
}

public void execute(SchedulableContext sc) {
// Salesforce has a limit of 5 running batch jobs
// If there are already 5 jobs running, then don't run this job
// Any records that need to be processed will be processed the next time the job executes
if(this.getNumberOfRunningBatchJobs() >= 5) return;

Database.executebatch(new TrelloLogPusher(), 100);
}

private Integer getNumberOfRunningBatchJobs() {
return [SELECT COUNT() FROM AsyncApexJob WHERE JobType='BatchApex' AND Status IN ('Processing', 'Preparing', 'Queued')];
}

}
5 changes: 5 additions & 0 deletions src/classes/TrelloLogPushScheduler.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>43.0</apiVersion>
<status>Active</status>
</ApexClass>
42 changes: 42 additions & 0 deletions src/classes/TrelloLogPushScheduler_Tests.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*************************************************************************************************
* This file is part of the Nebula Logger project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. *
*************************************************************************************************/
@isTest
private class TrelloLogPushScheduler_Tests {

@isTest
static void it_should_schedule_the_batch_job() {
String cronExpression = '0 0 0 15 3 ? 2022';
Integer numberOfScheduledJobs = [SELECT COUNT() FROM CronTrigger];
System.assertEquals(0, numberOfScheduledJobs);

Test.startTest();
String jobId = System.schedule('TrelloLogPushScheduler', cronExpression, new TrelloLogPushScheduler());
Test.stopTest();

CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE Id = :jobId];
System.assertEquals(cronExpression, ct.CronExpression);
}

@isTest
static void it_should_schedule_the_batch_job_schedule_every_5_minutes() {
Integer numberOfScheduledJobs = [SELECT COUNT() FROM CronTrigger];
System.assertEquals(0, numberOfScheduledJobs);

Test.startTest();
TrelloLogPushScheduler.scheduleEveryXMinutes(5);
Test.stopTest();
}

@isTest
static void it_should_schedule_the_batch_job_schedule_hourly() {
Integer numberOfScheduledJobs = [SELECT COUNT() FROM CronTrigger];
System.assertEquals(0, numberOfScheduledJobs);

Test.startTest();
TrelloLogPushScheduler.scheduleHourly(0);
Test.stopTest();
}

}
5 changes: 5 additions & 0 deletions src/classes/TrelloLogPushScheduler_Tests.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>43.0</apiVersion>
<status>Active</status>
</ApexClass>
78 changes: 78 additions & 0 deletions src/classes/TrelloLogPusher.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*************************************************************************************************
* This file is part of the Nebula Logger project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. *
*************************************************************************************************/
public without sharing class TrelloLogPusher implements Database.AllowsCallouts, Database.Batchable<Log__c> {

private static final LoggerIntegration__mdt SETTINGS = [SELECT ApiKey__c, ApiToken__c, BaseUrl__c, UrlParameters__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Trello'];

public List<Log__c> start(Database.BatchableContext batchableContext) {
return [
SELECT
LoggedBy__r.Name, LoggedBy__c, Name, TransactionId__c,
// Get the most recent exception entry - this will be used to created the Trello card
(
SELECT ExceptionStackTrace__c, ExceptionType__c, Message__c, Timestamp__c
FROM LogEntries__r
WHERE Type__c = 'Exception'
ORDER BY Timestamp__c
DESC LIMIT 1
),
(SELECT Topic.Name FROM TopicAssignments)
FROM Log__c
WHERE PushToTrello__c = true
AND PushedToTrelloDate__c = null
AND TotalExceptionLogEntries__c > 0
];
}

public void execute(Database.BatchableContext batchableContext, List<Log__c> logs) {
for(Log__c log : logs) {
LogEntry__c lastException = log.LogEntries__r[0];

String cardName = log.Name + ' logged by ' + log.LoggedBy__r.Name;

List<String> topicNames = new List<String>();
for(TopicAssignment topicAssignment : log.TopicAssignments) {
topicNames.add('#' + topicAssignment.Topic.Name);
}
if(!topicNames.isEmpty()) cardName += '\n\n' + String.join(topicNames, ' ');

String cardDescription =
'**Transaction ID:** ' + log.TransactionId__c
+ '\n\n**Timestamp:** ' + this.getFormattedTimestamp(lastException.Timestamp__c)
+ '\n\n**Exception Type:** ' + lastException.ExceptionType__c
+ '\n\n**Message:** ' + lastException.Message__c
+ '\n\n**Stack Trace:** ' + lastException.ExceptionStackTrace__c;

String urlParameters = SETTINGS.UrlParameters__c == null ? '' : '&' + SETTINGS.UrlParameters__c.replace('\n', '&').replace('\r', '').replace(' ', '');
String newTrelloCardEndpoint = SETTINGS.BaseUrl__c + '/1/cards'
+ '?name=' + EncodingUtil.urlEncode(cardName, 'UTF-8').replace('+', '%20')
+ '&desc=' + EncodingUtil.urlEncode(cardDescription, 'UTF-8').replace('+', '%20')
+ '&urlSource=' + EncodingUtil.urlEncode(Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.Id, 'UTF-8')
+ '&key=' + SETTINGS.ApiKey__c
+ '&token=' + SETTINGS.ApiToken__c
+ urlParameters;

HttpRequest request = new HttpRequest();
request.setEndpoint(newTrelloCardEndpoint);
request.setMethod('POST');
request.setHeader('Content-Type', 'text/plain');
HttpResponse response = new Http().send(request);

if(response.getStatusCode() >= 400) throw new TrelloApiException(response.getBody());

log.PushedToTrelloDate__c = System.now();
}
update logs;
}

public void finish(Database.BatchableContext batchableContext) {}

private String getFormattedTimestamp(Datetime timestamp) {
return timestamp.format('yyyy-MM-dd\'T\'HH:mm:ss\'Z\'', 'Greenwich Mean Time');
}

private class TrelloApiException extends Exception {}

}
5 changes: 5 additions & 0 deletions src/classes/TrelloLogPusher.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>43.0</apiVersion>
<status>Active</status>
</ApexClass>
89 changes: 89 additions & 0 deletions src/classes/TrelloLogPusher_Tests.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*************************************************************************************************
* This file is part of the Nebula Logger project, released under the MIT License. *
* See LICENSE file or go to https://github.com/jongpie/NebulaLogger for full license details. *
*************************************************************************************************/
@isTest
private class TrelloLogPusher_Tests {

public class SuccessCalloutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest request) {
HttpResponse response = new HttpResponse();
response.setBody(request.getBody());
response.setStatusCode(200);
return response;
}
}

public class FailureCalloutMock implements HttpCalloutMock {
public HttpResponse respond(HttpRequest request) {
HttpResponse response = new HttpResponse();
response.setBody(request.getBody());
response.setStatusCode(400);
return response;
}
}

static void verifyLogEntryCountEquals(Integer expectedCount) {
List<LogEntry__c> existingLogEntries = [SELECT Id FROM LogEntry__c];
System.assertEquals(expectedCount, existingLogEntries.size());
}

@isTest
static void it_should_push_a_debug_log_entry_and_update_push_date() {
verifyLogEntryCountEquals(0);
Logger.addDebugEntry(LoggingLevel.DEBUG, 'testing', 'MyClass.myMethodName()');
Logger.saveLog();
Test.getEventBus().deliver();
verifyLogEntryCountEquals(1);

// To trigger Trello push, we need to update a field on the parent Log__c
Log__c log = [SELECT Id, PushToTrello__c FROM Log__c];
log.PushToTrello__c = true;
update log;

LogEntry__c logEntry = [SELECT Id, Log__r.PushToTrello__c, Log__r.PushedToTrelloDate__c FROM LogEntry__c];
System.assertEquals(true, logEntry.Log__r.PushToTrello__c);
System.assertEquals(null, logEntry.Log__r.PushedToTrelloDate__c);

Test.startTest();
Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock());

Database.executeBatch(new TrelloLogPusher());

Test.stopTest();

logEntry = [SELECT Id, Log__r.PushToTrello__c, Log__r.PushedToTrelloDate__c FROM LogEntry__c];
System.assertEquals(true, logEntry.Log__r.PushToTrello__c);
System.assertEquals(System.today(), logEntry.Log__r.PushedToTrelloDate__c.date());
}

@isTest
static void it_should_not_push_a_debug_log_entry_when_push_field_is_false() {
verifyLogEntryCountEquals(0);
Logger.addDebugEntry(LoggingLevel.DEBUG, 'testing', 'MyClass.myMethodName()');
Logger.saveLog();
Test.getEventBus().deliver();
verifyLogEntryCountEquals(1);

// To make sure that Trello push is not triggered, we need to update a field on the parent Log__c
Log__c log = [SELECT Id, PushToTrello__c FROM Log__c];
log.PushToTrello__c = false;
update log;

LogEntry__c logEntry = [SELECT Id, Log__r.PushToTrello__c, Log__r.PushedToTrelloDate__c FROM LogEntry__c];
System.assertEquals(false, logEntry.Log__r.PushToTrello__c);
System.assertEquals(null, logEntry.Log__r.PushedToTrelloDate__c);

Test.startTest();
Test.setMock(HttpCalloutMock.class, new SuccessCalloutMock());

Database.executeBatch(new TrelloLogPusher());

Test.stopTest();

logEntry = [SELECT Id, Log__r.PushToTrello__c, Log__r.PushedToTrelloDate__c FROM LogEntry__c];
System.assertEquals(false, logEntry.Log__r.PushToTrello__c);
System.assertEquals(null, logEntry.Log__r.PushedToTrelloDate__c);
}

}
5 changes: 5 additions & 0 deletions src/classes/TrelloLogPusher_Tests.cls-meta.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>43.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading

0 comments on commit 6dbcdbc

Please sign in to comment.