-
-
Notifications
You must be signed in to change notification settings - Fork 171
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding new plugin to add additional logging options for async failures (
#309) * Adding new plugin for BatchApexErrorEvent and default Finalizer implementation for logging failures * Code review feedback - updated test class to make use of User metadata to avoid having to perform DML/the extra logs associated with Account, and updated example image in README to correctly describe the process for enabling unexpected batch error logging
- Loading branch information
1 parent
b81ea20
commit 520a385
Showing
19 changed files
with
401 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
15 changes: 15 additions & 0 deletions
15
...figuration/objects/LoggerSObjectHandler__mdt/fields/SObjectTypeOverride__c.field-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
<?xml version="1.0" encoding="UTF-8" ?> | ||
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata"> | ||
<fullName>SObjectTypeOverride__c</fullName> | ||
<description | ||
>Not all base platform types can be selected using the SObjectType picklist. If your object is not supported, supply the API name for the object here instead.</description> | ||
<externalId>false</externalId> | ||
<fieldManageability>SubscriberControlled</fieldManageability> | ||
<inlineHelpText | ||
>Not all base platform types can be selected using the SObjectType picklist. If your object is not supported, supply the API name for the object here instead.</inlineHelpText> | ||
<label>SObjectType Override</label> | ||
<length>255</length> | ||
<required>false</required> | ||
<type>Text</type> | ||
<unique>false</unique> | ||
</CustomField> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file added
BIN
+26.3 KB
...sync-failure-additions/.images/opt-into-batch-logging-with-logger-parameter.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
# Async Failure Additions plugin for Nebula Logger | ||
|
||
> :information_source: This plugin requires `v4.7.1` or newer of Nebula Logger's unlocked package | ||
[![Install Unlocked Package](../.images/btn-install-unlocked-package-plugin-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=TODO) | ||
|
||
## What's Included | ||
|
||
### Unexpected Batch Error Logging | ||
|
||
By default, this plugin adds support for logging unexpected Batch class failures in Apex. All a batch class needs to do is implement the marker `Database.RaisesPlatformEvents` interface _and_ create a `LoggerParameter__mdt` record where the `Value` field matches the name of the batch class you are looking to add logging for, and the DeveloperName (the "Name" field) starts with `BatchError`: | ||
|
||
```java | ||
// the class MUST implement Database.RaisesPlatformEvents for this to work correctly! | ||
public class MyExampleBatchable implements Database.Batchable<SObject>, Database.RaisesPlatformEvents { | ||
// etc ... | ||
} | ||
``` | ||
|
||
And the CMDT record: | ||
|
||
![Setting up the Logger Parameter record to opt into unexpected Batch failures](.images/opt-into-batch-logging-with-logger-parameter.png) | ||
|
||
Once you've correctly configured those two things (the marker interface `Database.RaisesPlatformEvents` on the Apex batchable class, and the Logger Parameter CMDT record), your class will now log any uncaught exceptions that cause that batch class to fail unexpectedly. | ||
|
||
--- | ||
|
||
If you want to customize additional behavior off of the trigger that subscribes to `BatchApexErrorEvent`, you can do so by creating a new Trigger SObject Handler CMDT record using the `TriggerSObjectHandler__mdt.SObjectTypeOverride__c` field (since `BatchApexErrorEvent` isn't one of the supported Entity Definition picklist results). The Logger SObject Handler Name should correspond to a valid instance of `LoggerSObjectHandler` - the instance of `LogBatchApexEventHandler` included in this plugin shows what an example logging implementation for unexpected failures would look like, if you want to go down that route. | ||
|
||
### Queueable Error Logging | ||
|
||
If you have Apex classes that implement `System.Queueable`, you can add error logging with some minimal code additions: | ||
|
||
```java | ||
public class MyExampleQueueable implements System.Queueable { | ||
public void execute (System.QueueableContext qc) { | ||
System.attachFinalizer(new LogFinalizer()); | ||
} | ||
} | ||
``` | ||
|
||
If you'd like to do _additional_ processing, you can alternatively choose to _extend_ `LogFinalizer`: | ||
|
||
```java | ||
public class MyCustomFinalizer extends LogFinalizer { | ||
protected override void innerExecute(System.FinalizerContext fc) { | ||
// do whatever you'd like! | ||
// errors will be logged automatically in addition to what you choose to do here | ||
// no need to call Logger.saveLog() manually on this code path | ||
} | ||
} | ||
``` |
67 changes: 67 additions & 0 deletions
67
...a-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
//------------------------------------------------------------------------------------------------// | ||
// 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. // | ||
//------------------------------------------------------------------------------------------------// | ||
/** | ||
* @group Plugins | ||
* @description `BatchApexErrorEvent` handler to log unexpected batch errors for classes that implement `Database.RaisesPlatformEvents` and opt into processing via `LoggerParameter__mdt` | ||
* @see LoggerSObjectHandler | ||
*/ | ||
public without sharing class LogBatchApexErrorEventHandler extends LoggerSObjectHandler { | ||
public static final String BATCH_ERROR_LOGGER = 'BatchError'; | ||
public static final String LOG_MESSAGE = 'An unexpected job error occurred: {0} with exception type: {1} and message: {2} during batch phase: {3}.\nStacktrace: {4}'; | ||
private static Boolean shouldSaveLogs = false; | ||
|
||
private List<BatchApexErrorEvent> batchApexErrorEvents; | ||
|
||
/** | ||
* @description Opts into the default constructor | ||
*/ | ||
public LogBatchApexErrorEventHandler() { | ||
super(); | ||
} | ||
|
||
public override Schema.SObjectType getSObjectType() { | ||
return Schema.BatchApexErrorEvent.SObjectType; | ||
} | ||
|
||
protected override void executeAfterInsert(List<SObject> triggerNew) { | ||
this.batchApexErrorEvents = (List<BatchApexErrorEvent>) triggerNew; | ||
this.handleJobErrors(); | ||
} | ||
|
||
private void handleJobErrors() { | ||
Set<Id> asyncApexJobIds = new Set<Id>(); | ||
for (BatchApexErrorEvent evt : this.batchApexErrorEvents) { | ||
asyncApexJobIds.add(evt.AsyncApexJobId); | ||
} | ||
|
||
Map<Id, AsyncApexJob> jobIdToClass = new Map<Id, AsyncApexJob>([SELECT Id, ApexClass.Name FROM AsyncApexJob WHERE Id IN :asyncApexJobIds]); | ||
Logger.error('Batch job terminated unexpectedly'); | ||
for (BatchApexErrorEvent errorEvent : this.batchApexErrorEvents) { | ||
shouldSaveLogs = this.getShouldSaveLogs(jobIdToClass, errorEvent); | ||
LogMessage logMessage = new LogMessage( | ||
LOG_MESSAGE, | ||
new List<String>{ errorEvent.AsyncApexJobId, errorEvent.ExceptionType, errorEvent.Message, errorEvent.Phase, errorEvent.StackTrace } | ||
); | ||
Logger.error(logMessage); | ||
} | ||
if (shouldSaveLogs) { | ||
Logger.saveLog(); | ||
} | ||
} | ||
|
||
private Boolean getShouldSaveLogs(Map<Id, AsyncApexJob> jobIdToClass, BatchApexErrorEvent errorEvent) { | ||
if (shouldSaveLogs == false) { | ||
AsyncApexJob job = jobIdToClass.get(errorEvent.AsyncApexJobId); | ||
List<LoggerParameter__mdt> configurationList = LoggerParameter.matchOnPrefix(BATCH_ERROR_LOGGER); | ||
for (LoggerParameter__mdt config : configurationList) { | ||
if (config.Value__c == job?.ApexClass.Name) { | ||
shouldSaveLogs = true; | ||
break; | ||
} | ||
} | ||
} | ||
return shouldSaveLogs; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
...plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>54.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
100 changes: 100 additions & 0 deletions
100
...er/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
//------------------------------------------------------------------------------------------------// | ||
// 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. // | ||
//------------------------------------------------------------------------------------------------// | ||
@SuppressWarnings('PMD.ApexDoc, PMD.ApexAssertionsShouldIncludeMessage, PMD.MethodNamingConventions, PMD.ApexUnitTestClassShouldHaveAsserts') | ||
@IsTest(IsParallel=true) | ||
private class LogBatchApexErrorEventHandler_Tests implements Database.Batchable<SObject>, Database.RaisesPlatformEvents { | ||
private enum Phase { | ||
START, | ||
EXECUTE, | ||
FINISH | ||
} | ||
|
||
private final Phase throwLocation; | ||
|
||
@IsTest | ||
static void it_should_create_log_when_batch_job_throws_in_start_method() { | ||
runTestForPhase(Phase.START); | ||
} | ||
|
||
@IsTest | ||
static void it_should_create_log_when_batch_job_throws_in_execute_method() { | ||
runTestForPhase(Phase.EXECUTE); | ||
} | ||
|
||
@IsTest | ||
static void it_should_create_log_when_batch_job_throws_in_finish_method() { | ||
runTestForPhase(Phase.FINISH); | ||
} | ||
|
||
@SuppressWarnings('PMD.EmptyCatchBlock') | ||
private static void runTestForPhase(Phase phase) { | ||
Logger.getUserSettings().IsApexSystemDebugLoggingEnabled__c = false; | ||
LoggerParameter__mdt mockParam = new LoggerParameter__mdt(); | ||
mockParam.Value__c = LogBatchApexErrorEventHandler_Tests.class.getName(); | ||
mockParam.DeveloperName = LogBatchApexErrorEventHandler.BATCH_ERROR_LOGGER + 'Test'; | ||
LoggerParameter.setMock(mockParam); | ||
try { | ||
System.Test.startTest(); | ||
Database.executeBatch(new LogBatchApexErrorEventHandler_Tests(phase)); | ||
System.Test.stopTest(); | ||
} catch (Exception ex) { | ||
// via https://salesforce.stackexchange.com/questions/263419/testing-batchapexerrorevent-trigger | ||
} | ||
// at this point, we're still two async-levels deep into Platform Event-land; we need to call "deliver()" twice | ||
System.Test.getEventBus().deliver(); // fires the platform event for Database.RaisesPlatformEvents | ||
System.Test.getEventBus().deliver(); // fires the logger's platform event | ||
|
||
assertLogWasCreatedForPhase(phase); | ||
} | ||
|
||
private static void assertLogWasCreatedForPhase(Phase phase) { | ||
Log__c log = getLog(); | ||
System.assertNotEquals(null, log, 'Log should have been created!'); | ||
System.assertEquals(2, log.LogEntries__r.size(), 'Two log entries should have been created'); | ||
System.assertEquals('Batch job terminated unexpectedly', log.LogEntries__r[0].Message__c); | ||
System.assertEquals( | ||
String.format( | ||
LogBatchApexErrorEventHandler.LOG_MESSAGE, | ||
new List<String>{ 'someId', 'System.IllegalArgumentException', phase.name(), phase.name(), 'stacktrace' } | ||
) | ||
.subStringAfter('with') | ||
.substringBefore('Stacktrace:'), | ||
log.LogEntries__r[1].Message__c.substringAfter('with').substringBefore('Stacktrace:') | ||
); | ||
} | ||
|
||
/** | ||
* the `BatchApexErrorEvent` type has a property, `Phase` with three possible values: | ||
* - START | ||
* - EXECUTE | ||
* - FINISH | ||
*/ | ||
public LogBatchApexErrorEventHandler_Tests(Phase throwLocation) { | ||
this.throwLocation = throwLocation; | ||
} | ||
|
||
public Database.QueryLocator start(Database.BatchableContext bc) { | ||
throwOnLocationMatch(Phase.START); | ||
return Database.getQueryLocator([SELECT Id FROM User LIMIT 1]); | ||
} | ||
|
||
public void execute(Database.BatchableContext bc, List<SObject> scope) { | ||
throwOnLocationMatch(Phase.EXECUTE); | ||
} | ||
|
||
public void finish(Database.BatchableContext bc) { | ||
throwOnLocationMatch(Phase.FINISH); | ||
} | ||
|
||
private void throwOnLocationMatch(Phase phase) { | ||
if (this.throwLocation == phase) { | ||
throw new IllegalArgumentException(this.throwLocation.name()); | ||
} | ||
} | ||
|
||
private static Log__c getLog() { | ||
return [SELECT Id, (SELECT Message__c FROM LogEntries__r) FROM Log__c]; | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
...s/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>54.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
33 changes: 33 additions & 0 deletions
33
nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
//------------------------------------------------------------------------------------------------// | ||
// 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. // | ||
//------------------------------------------------------------------------------------------------// | ||
/** | ||
* @group Plugins | ||
* @description `System.Finalizer` implementation that can be used by subscribers to log errors | ||
*/ | ||
public without sharing virtual class LogFinalizer implements System.Finalizer { | ||
/** | ||
* @description Is called by any `System.Queueable` where the finalizer is attached after the Queueable's `execute` method finishes | ||
* @param fc The `System.FinalizerContext` associated with the finalizer | ||
*/ | ||
public void execute(System.FinalizerContext fc) { | ||
switch on fc.getResult() { | ||
when UNHANDLED_EXCEPTION { | ||
Logger.error('There was an error during this queueable job'); | ||
Logger.error('Error details', fc.getException()); | ||
} | ||
} | ||
this.innerExecute(fc); | ||
Logger.saveLog(); | ||
} | ||
|
||
/** | ||
* @description Subscribers can optionally override this method with their own implementation to do further processing/re-queueing | ||
* @param fc The `System.FinalizerContext` associated with the finalizer | ||
*/ | ||
@SuppressWarnings('PMD.EmptyStatementBlock') | ||
protected virtual void innerExecute(System.FinalizerContext fc) { | ||
// subscribers can override this to do their own post-processing if necessary, otherwise it's a no-op | ||
} | ||
} |
5 changes: 5 additions & 0 deletions
5
nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls-meta.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>54.0</apiVersion> | ||
<status>Active</status> | ||
</ApexClass> |
Oops, something went wrong.