From 520a3852554723df0f4eb85e557304e27a3b1fd3 Mon Sep 17 00:00:00 2001
From: James Simone <16430727+jamessimone@users.noreply.github.com>
Date: Mon, 9 May 2022 13:11:20 -0600
Subject: [PATCH] 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
---
README.md | 4 +-
...ger SObject Handler Layout.layout-meta.xml | 4 +
.../SObjectTypeOverride__c.field-meta.xml | 15 +++
.../fields/SObjectType__c.field-meta.xml | 2 +-
.../classes/LoggerSObjectHandler.cls | 15 ++-
.../classes/LoggerSObjectHandler_Tests.cls | 20 ++++
...to-batch-logging-with-logger-parameter.png | Bin 0 -> 26889 bytes
.../plugins/async-failure-additions/README.md | 52 +++++++++
.../classes/LogBatchApexErrorEventHandler.cls | 67 ++++++++++++
...LogBatchApexErrorEventHandler.cls-meta.xml | 5 +
.../LogBatchApexErrorEventHandler_Tests.cls | 100 ++++++++++++++++++
...chApexErrorEventHandler_Tests.cls-meta.xml | 5 +
.../plugin/classes/LogFinalizer.cls | 33 ++++++
.../plugin/classes/LogFinalizer.cls-meta.xml | 5 +
.../plugin/classes/LogFinalizer_Tests.cls | 55 ++++++++++
.../classes/LogFinalizer_Tests.cls-meta.xml | 5 +
.../LogBatchApexErrorEventTrigger.trigger | 7 ++
...atchApexErrorEventTrigger.trigger-meta.xml | 5 +
sfdx-project.json | 10 +-
19 files changed, 401 insertions(+), 8 deletions(-)
create mode 100644 nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectTypeOverride__c.field-meta.xml
create mode 100644 nebula-logger/plugins/async-failure-additions/.images/opt-into-batch-logging-with-logger-parameter.png
create mode 100644 nebula-logger/plugins/async-failure-additions/README.md
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls-meta.xml
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls-meta.xml
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls-meta.xml
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls-meta.xml
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger
create mode 100644 nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger-meta.xml
diff --git a/README.md b/README.md
index 7d64467af..be2af6b7e 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ Designed for Salesforce admins, developers & architects. A robust logger for Ape
## Unlocked Package - v4.7.2
-[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lh4QAA)
-[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lh4QAA)
+[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lhEQAQ)
+[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015lhEQAQ)
[![View Documentation](./images/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/)
## Managed Package - v4.7.0
diff --git a/nebula-logger/core/main/configuration/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml b/nebula-logger/core/main/configuration/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml
index 055e5a5c6..d274e2a70 100644
--- a/nebula-logger/core/main/configuration/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml
+++ b/nebula-logger/core/main/configuration/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml
@@ -18,6 +18,10 @@
Required
SObjectHandlerApexClass__c
+
+ Edit
+ SObjectTypeOverride__c
+
diff --git a/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectTypeOverride__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectTypeOverride__c.field-meta.xml
new file mode 100644
index 000000000..fc048f435
--- /dev/null
+++ b/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectTypeOverride__c.field-meta.xml
@@ -0,0 +1,15 @@
+
+
+ SObjectTypeOverride__c
+ 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.
+ false
+ SubscriberControlled
+ 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.
+
+ 255
+ false
+ Text
+ false
+
diff --git a/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml b/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml
index 2bcd98bfb..80c5bd339 100644
--- a/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml
+++ b/nebula-logger/core/main/configuration/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml
@@ -7,7 +7,7 @@
EntityDefinition
Logger SObject Handler Configurations
LoggerSObjectHandlerConfigurations
- true
+ false
MetadataRelationship
true
diff --git a/nebula-logger/core/main/logger-engine/classes/LoggerSObjectHandler.cls b/nebula-logger/core/main/logger-engine/classes/LoggerSObjectHandler.cls
index 8eef8285c..640ca683e 100644
--- a/nebula-logger/core/main/logger-engine/classes/LoggerSObjectHandler.cls
+++ b/nebula-logger/core/main/logger-engine/classes/LoggerSObjectHandler.cls
@@ -250,12 +250,12 @@ public without sharing abstract class LoggerSObjectHandler {
private static Map queryHandlerConfigurations() {
Map sobjectTypeToHandlerConfiguration = new Map();
for (LoggerSObjectHandler__mdt handlerConfiguration : [
- SELECT IsEnabled__c, SObjectHandlerApexClass__c, SObjectType__r.QualifiedApiName
+ SELECT IsEnabled__c, SObjectHandlerApexClass__c, SObjectType__r.QualifiedApiName, SObjectTypeOverride__c
FROM LoggerSObjectHandler__mdt
WHERE IsEnabled__c = TRUE
]) {
handlerConfiguration.SObjectType__c = handlerConfiguration.SObjectType__r.QualifiedApiName;
- Schema.SObjectType sobjectType = ((SObject) Type.forName(handlerConfiguration.SObjectType__c).newInstance()).getSObjectType();
+ Schema.SObjectType sobjectType = prepHandlerType(handlerConfiguration);
sobjectTypeToHandlerConfiguration.put(sobjectType, handlerConfiguration);
}
@@ -266,6 +266,14 @@ public without sharing abstract class LoggerSObjectHandler {
return sobjectTypeToHandlerConfiguration;
}
+ private static Schema.SObjectType prepHandlerType(LoggerSObjectHandler__mdt handlerConfiguration) {
+ if (String.isNotBlank(handlerConfiguration.SObjectTypeOverride__c)) {
+ handlerConfiguration.SObjectType__c = handlerConfiguration.SObjectTypeOverride__c;
+ }
+
+ return ((SObject) Type.forName(handlerConfiguration.SObjectType__c).newInstance()).getSObjectType();
+ }
+
@TestVisible
private static LoggerSObjectHandler__mdt getHandlerConfiguration(Schema.SObjectType sobjectType) {
return SOBJECT_TYPE_TO_HANDLER_CONFIGURATIONS.get(sobjectType);
@@ -273,8 +281,7 @@ public without sharing abstract class LoggerSObjectHandler {
@TestVisible
private static void setMock(LoggerSObjectHandler__mdt handlerConfiguration) {
- //TODO cleanup code duplication
- Schema.SObjectType sobjectType = ((SObject) Type.forName(handlerConfiguration.SObjectType__c).newInstance()).getSObjectType();
+ Schema.SObjectType sobjectType = prepHandlerType(handlerConfiguration);
SOBJECT_TYPE_TO_HANDLER_CONFIGURATIONS.put(sobjectType, handlerConfiguration);
}
diff --git a/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectHandler_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectHandler_Tests.cls
index 454bd7aa9..cccd481be 100644
--- a/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectHandler_Tests.cls
+++ b/nebula-logger/core/tests/logger-engine/classes/LoggerSObjectHandler_Tests.cls
@@ -33,6 +33,26 @@ private class LoggerSObjectHandler_Tests {
);
}
+ @IsTest
+ static void it_should_return_handler_via_override() {
+ Schema.SObjectType sobjectType = new MockSObjectHandler().getSObjectType();
+ LoggerSObjectHandler.setMock(
+ new LoggerSObjectHandler__mdt(
+ IsEnabled__c = true,
+ SObjectHandlerApexClass__c = MockSObjectHandler.class.getName(),
+ SObjectTypeOverride__c = sobjectType.getDescribe().getName()
+ )
+ );
+
+ LoggerSObjectHandler configuredInstance = LoggerSObjectHandler.getHandler(sobjectType);
+
+ System.assertEquals(
+ true,
+ configuredInstance instanceof MockSObjectHandler,
+ 'The handler returned via override should be an instance of the configured class, MockSObjectHandler'
+ );
+ }
+
@IsTest
static void it_should_return_default_sobject_handler_implementation_when_no_configuration_provided() {
Schema.SObjectType sobjectType = new MockDefaultImplementationSObjectHandler().getSObjectType();
diff --git a/nebula-logger/plugins/async-failure-additions/.images/opt-into-batch-logging-with-logger-parameter.png b/nebula-logger/plugins/async-failure-additions/.images/opt-into-batch-logging-with-logger-parameter.png
new file mode 100644
index 0000000000000000000000000000000000000000..c0291d4b4e61be1de5feaa662fbae091df86f1b4
GIT binary patch
literal 26889
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 @c!9)pLz##|Y
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 :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, 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
+ }
+}
+```
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls
new file mode 100644
index 000000000..4fd27b73f
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls
@@ -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 batchApexErrorEvents;
+
+ /**
+ * @description Opts into the default constructor
+ */
+ public LogBatchApexErrorEventHandler() {
+ super();
+ }
+
+ public override Schema.SObjectType getSObjectType() {
+ return Schema.BatchApexErrorEvent.SObjectType;
+ }
+
+ protected override void executeAfterInsert(List triggerNew) {
+ this.batchApexErrorEvents = (List) triggerNew;
+ this.handleJobErrors();
+ }
+
+ private void handleJobErrors() {
+ Set asyncApexJobIds = new Set();
+ for (BatchApexErrorEvent evt : this.batchApexErrorEvents) {
+ asyncApexJobIds.add(evt.AsyncApexJobId);
+ }
+
+ Map jobIdToClass = new Map([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{ errorEvent.AsyncApexJobId, errorEvent.ExceptionType, errorEvent.Message, errorEvent.Phase, errorEvent.StackTrace }
+ );
+ Logger.error(logMessage);
+ }
+ if (shouldSaveLogs) {
+ Logger.saveLog();
+ }
+ }
+
+ private Boolean getShouldSaveLogs(Map jobIdToClass, BatchApexErrorEvent errorEvent) {
+ if (shouldSaveLogs == false) {
+ AsyncApexJob job = jobIdToClass.get(errorEvent.AsyncApexJobId);
+ List configurationList = LoggerParameter.matchOnPrefix(BATCH_ERROR_LOGGER);
+ for (LoggerParameter__mdt config : configurationList) {
+ if (config.Value__c == job?.ApexClass.Name) {
+ shouldSaveLogs = true;
+ break;
+ }
+ }
+ }
+ return shouldSaveLogs;
+ }
+}
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls-meta.xml
new file mode 100644
index 000000000..891916bb0
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls
new file mode 100644
index 000000000..e327d41dd
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls
@@ -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, 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{ '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 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];
+ }
+}
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls-meta.xml
new file mode 100644
index 000000000..891916bb0
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogBatchApexErrorEventHandler_Tests.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls
new file mode 100644
index 000000000..8e29533f7
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls
@@ -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
+ }
+}
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls-meta.xml
new file mode 100644
index 000000000..891916bb0
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls
new file mode 100644
index 000000000..fc4a56f8d
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls
@@ -0,0 +1,55 @@
+//------------------------------------------------------------------------------------------------//
+// 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')
+@IsTest(IsParallel=true)
+private class LogFinalizer_Tests {
+ private static final String EXPECTED_ERROR_MESSAGE = 'Gack';
+ @IsTest
+ static void it_should_not_log_on_queueable_success() {
+ System.Test.startTest();
+ System.enqueueJob(new ExampleQueueable());
+ System.Test.stopTest();
+ System.Test.getEventBus().deliver();
+
+ System.assertEquals(0, [SELECT COUNT() FROM Log__c], 'Should not log if no errors');
+ }
+
+ @IsTest
+ static void it_should_log_on_queueable_error() {
+ try {
+ System.Test.startTest();
+ System.enqueueJob(new ExampleFailedQueueable());
+ System.Test.stopTest();
+ } catch (Exception ex) {
+ System.assertEquals(EXPECTED_ERROR_MESSAGE, ex.getMessage());
+ }
+ System.Test.getEventBus().deliver();
+
+ List logEntries = [SELECT Message__c, ExceptionMessage__c FROM LogEntry__c];
+ System.assertEquals(2, logEntries.size(), 'Should log for errors');
+ LogEntry__c firstEntry = logEntries.get(0);
+ System.assertEquals('There was an error during this queueable job', firstEntry.Message__c);
+ LogEntry__c secondEntry = logEntries.get(1);
+ System.assertEquals('Error details', secondEntry.Message__c);
+ System.assertEquals(EXPECTED_ERROR_MESSAGE, secondEntry.ExceptionMessage__c);
+ }
+
+ private virtual class ExampleQueueable implements System.Queueable {
+ public void execute(System.QueueableContext qc) {
+ System.attachFinalizer(new LogFinalizer());
+ this.innerExecute();
+ }
+
+ @SuppressWarnings('PMD.EmptyStatementBlock')
+ protected virtual void innerExecute() {
+ }
+ }
+
+ private virtual class ExampleFailedQueueable extends ExampleQueueable {
+ protected override void innerExecute() {
+ throw new IllegalArgumentException(EXPECTED_ERROR_MESSAGE);
+ }
+ }
+}
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls-meta.xml
new file mode 100644
index 000000000..891916bb0
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/classes/LogFinalizer_Tests.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger b/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger
new file mode 100644
index 000000000..8c68f608e
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger
@@ -0,0 +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. //
+//------------------------------------------------------------------------------------------------//
+trigger LogBatchApexErrorEventTrigger on BatchApexErrorEvent(after insert) {
+ LoggerSObjectHandler.getHandler(Schema.BatchApexErrorEvent.SObjectType, new LogBatchApexErrorEventHandler()).execute();
+}
diff --git a/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger-meta.xml b/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger-meta.xml
new file mode 100644
index 000000000..5e573d796
--- /dev/null
+++ b/nebula-logger/plugins/async-failure-additions/plugin/triggers/LogBatchApexErrorEventTrigger.trigger-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 54.0
+ Active
+
diff --git a/sfdx-project.json b/sfdx-project.json
index 2c3997710..3e21e340f 100644
--- a/sfdx-project.json
+++ b/sfdx-project.json
@@ -65,6 +65,14 @@
"versionDescription": "TODO",
"default": false
},
+ {
+ "default": false,
+ "package": "Nebula Logger - Plugin - Async Failure Additions",
+ "path": "./nebula-logger/plugins/async-failure-additions/plugin",
+ "versionName": "Initial release with Finalizer and BatchApexErrorEvent functionality",
+ "versionNumber": "0.0.1.NEXT",
+ "versionDescription": "Adds the capability to get Logs created for unexpected Apex Batch class failures through LoggerParameter__mdt additions, and provides a Finalizer implementation for logging Queueable failures"
+ },
{
"path": "./nebula-logger/extra-tests",
"default": false
@@ -104,7 +112,7 @@
"Nebula Logger - Core@4.6.16-0-ui-cleanup": "04t5Y0000015lLzQAI",
"Nebula Logger - Core@4.7.0-25-spring-'22-release": "04t5Y0000015lXSQAY",
"Nebula Logger - Core@4.7.1-8-plugin-framework-overhaul": "04t5Y0000015lgBQAQ",
- "Nebula Logger - Core@4.7.2-NEXT-parent-log-transaction-id-bugfix": "04t5Y0000015lh4QAA",
+ "Nebula Logger - Core@4.7.2-NEXT-parent-log-transaction-id-bugfix": "04t5Y0000015lhEQAQ",
"Nebula Logger - Plugin - Big Object Archiving": "0Ho5Y000000blMSSAY",
"Nebula Logger - Plugin - Big Object Archiving@0.9.0-2": "04t5Y0000015lgLQAQ",
"Nebula Logger - Plugin - Log Retention Rules": "0Ho5Y000000blNfSAI",