Skip to content

Commit

Permalink
Slack integration
Browse files Browse the repository at this point in the history
* Added SlackLogPusher batch class to handle pushing logs to Slack
* Added SlackLogPushScheduler schedulable class to schedule pushing logs to Slack
* Added Slack record to LoggerIntegration__mdt custom metadata type
* Added new Slack fields PushToSlack__c and PushedToSlackDate__c on Log__c object
* Added remote site settings for Loggly & Slack
  • Loading branch information
jongpie authored Jul 13, 2018
1 parent ed7d995 commit 2eb887e
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 152 deletions.
1 change: 0 additions & 1 deletion src/classes/LogEntryEventHandler.cls
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,6 @@ public without sharing class LogEntryEventHandler {
this.logEntryToTopics.put(logEntry, LogEntryEvent.Topics__c.split(','));
}
}
//upsert logEntries TransactionEntryId__c;
insert logEntries;
}

Expand Down
1 change: 0 additions & 1 deletion src/classes/LogglyLogPushScheduler_Tests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
@isTest
private class LogglyLogPushScheduler_Tests {


@isTest
static void it_should_schedule_the_batch_job() {
String cronExpression = '0 0 0 15 3 ? 2022';
Expand Down
23 changes: 9 additions & 14 deletions src/classes/LogglyLogPusher.cls
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@
* 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 LogglyLogPusher implements Database.Batchable<Log__c>, Database.AllowsCallouts {
public without sharing class LogglyLogPusher implements Database.AllowsCallouts, Database.Batchable<Log__c> {

private static final LoggerIntegration__mdt SETTINGS = [SELECT BaseUrl__c, ApiToken__c FROM LoggerIntegration__mdt WHERE DeveloperName = 'Loggly'];
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'];

public List<Log__c> start(Database.BatchableContext batchableContext) {
return [
Expand Down Expand Up @@ -38,32 +39,24 @@ public without sharing class LogglyLogPusher implements Database.Batchable<Log__
update logs;
}

public void finish(Database.BatchableContext batchableContext) {
// If new logs have generated while the batch has been running, start a new batch
Integer countOfRemainingLogEntries = [
SELECT COUNT()
FROM Log__c
WHERE PushToLoggly__c = true
AND PushedToLogglyDate__c = null
AND TotalLogEntries__c > 0
];
if(countOfRemainingLogEntries > 0) Database.executeBatch(new LogglyLogPusher());
}
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 Map<Id, List<LogEntry__c>> getLogEntriesMap(List<Log__c> logs) {
Map<Id, List<LogEntry__c>> logEntriesMap = new Map<Id, List<LogEntry__c>>();
// TODO cleanup formatting of code
for(LogEntry__c logEntry : [
SELECT Id, ExceptionStackTrace__c, ExceptionType__c, Log__c, Message__c, OriginLocation__c, OriginType__c,
Timestamp__c, TransactionEntryId__c, Type__c,
(SELECT Id, TopicId, Topic.Name FROM TopicAssignments)
(SELECT Topic.Name FROM TopicAssignments)
FROM LogEntry__c
WHERE Log__c IN :logs
]) {
if(!logEntriesMap.containsKey(logEntry.Log__c)) logEntriesMap.put(logEntry.Log__c, new List<LogEntry__c>());

List<LogEntry__c> logEntries = logEntriesMap.get(logEntry.Log__c);
logEntries.add(logEntry);
logEntriesMap.put(logEntry.Log__c, logEntries);
Expand All @@ -90,6 +83,7 @@ public without sharing class LogglyLogPusher implements Database.Batchable<Log__
LogDto log = new LogDto();
log.exceptionStackTrace = logEntry.ExceptionStackTrace__c;
log.exceptionType = logEntry.ExceptionType__c;
log.originIsProduction = !ORG.IsSandbox;
log.originLocation = logEntry.OriginLocation__c;
log.originType = logEntry.OriginType__c;
log.logEntryId = logEntry.Id;
Expand Down Expand Up @@ -119,6 +113,7 @@ public without sharing class LogglyLogPusher implements Database.Batchable<Log__
public String logType;
public String logUrl;
public String message;
public Boolean originIsProduction;
public String originLocation;
public String originType;
public OrganizationDto organization;
Expand Down
32 changes: 32 additions & 0 deletions src/classes/SlackLogPushScheduler.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
public without sharing class SlackLogPushScheduler 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, 'Slack Log Sync: Every Hour at ' + minuteString);
}

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

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 SlackLogPusher());
}

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/SlackLogPushScheduler.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/SlackLogPushScheduler_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 SlackLogPushScheduler_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('SlackLogPushScheduler', cronExpression, new SlackLogPushScheduler());
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();
SlackLogPushScheduler.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();
SlackLogPushScheduler.scheduleHourly(0);
Test.stopTest();
}

}
5 changes: 5 additions & 0 deletions src/classes/SlackLogPushScheduler_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>
137 changes: 137 additions & 0 deletions src/classes/SlackLogPusher.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*************************************************************************************************
* 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 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'];

public List<Log__c> start(Database.BatchableContext batchableContext) {
return [
SELECT
LoggedBy__c, LoggedBy__r.Name, Name,
TotalDebugLogEntries__c, TotalExceptionLogEntries__c, TransactionId__c,
(SELECT Topic.Name FROM TopicAssignments)
FROM Log__c
WHERE PushToSlack__c = true
AND PushedToSlackDate__c = null
AND TotalLogEntries__c > 0
];
}

public void execute(Database.BatchableContext batchableContext, List<Log__c> logs) {
for(Log__c log : logs) {
NotificationDto notification = new NotificationDto();
notification.text = 'New Salesforce Log Created';
notification.attachments = new List<LogDto>();
notification.attachments.add(this.convertLog(log));

HttpRequest request = new HttpRequest();
request.setEndpoint(SETTINGS.BaseUrl__c + SETTINGS.ApiToken__c);
request.setMethod('POST');
request.setHeader('Content-Type', 'application/json');
String jsonString = Json.serialize(notification);
// 'Short' is a reserved word in Apex, but used in Slack's API, so the conversion happens in JSON
jsonString = jsonString.replace('"isShort"', '"short"');
request.setBody(jsonString);

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

log.PushedToSlackDate__c = System.now();
}

update logs;
}

public void finish(Database.BatchableContext batchableContext) {}

private LogDto convertLog(Log__c log) {
LogDto notification = new LogDto();
notification.author_link = Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.LoggedBy__c;
notification.author_name = log.LoggedBy__r.Name;
notification.color = log.TotalExceptionLogEntries__c >= 1 ? '#FF7373' : '#7CD197'; // Red if there are exceptions, otherwise green
notification.fields = new List<FieldDto>();
notification.text = 'Transaction ID: ' + log.TransactionId__c;
notification.title = log.Name;
notification.title_link = Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.Id;

FieldDto orgNameField = new FieldDto();
orgNameField.isShort = false;
orgNameField.title = 'Org Name';
orgNameField.value = UserInfo.getOrganizationName();
notification.fields.add(orgNameField);

FieldDto orgIdField = new FieldDto();
orgIdField.isShort = true;
orgIdField.title = 'Org ID';
orgIdField.value = '`' + UserInfo.getOrganizationId() + '`';
notification.fields.add(orgIdField);

FieldDto orgIsProductionField = new FieldDto();
orgIsProductionField.isShort = true;
orgIsProductionField.title = 'Production';
orgIsProductionField.value = '`' + !ORG.IsSandbox + '`';
notification.fields.add(orgIsProductionField);

FieldDto totalDebugEntriesField = new FieldDto();
totalDebugEntriesField.isShort = true;
totalDebugEntriesField.title = '# of Debug Entries';
totalDebugEntriesField.value = String.valueOf(log.TotalDebugLogEntries__c);
notification.fields.add(totalDebugEntriesField);

FieldDto totalExceptionEntriesField = new FieldDto();
totalExceptionEntriesField.isShort = true;
totalExceptionEntriesField.title = '# of Exception Entries';
totalExceptionEntriesField.value = String.valueOf(log.TotalExceptionLogEntries__c);
notification.fields.add(totalExceptionEntriesField);

List<String> topicNames = new List<String>();
for(TopicAssignment topicAssignment : log.TopicAssignments) {
topicNames.add(topicAssignment.Topic.Name);
}
topicNames.sort();

if(topicNames.isEmpty()) return notification;

FieldDto topicsField = new FieldDto();
topicsField.isShort = false;
topicsField.title = 'Topics';
topicsField.value = String.join(topicNames, ', ');
notification.fields.add(topicsField);

return notification;
}

private class NotificationDto {
public List<LogDto> attachments;
public String text;
}

private class LogDto {
public List<ActionDto> actions;
public String author_name;
public String author_link;
public String author_icon;
public String color;
public String fallback;
public List<FieldDto> fields;
public String pretext;
public String text;
public String title;
public String title_link;
}

private class ActionDto {
public String text;
public String type;
public String url;
}

private class FieldDto {
public Boolean isShort;
public String title;
public String value;
}

}
5 changes: 5 additions & 0 deletions src/classes/SlackLogPusher.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 2eb887e

Please sign in to comment.