Skip to content

Commit 8e859c2

Browse files
jamessimonejongpie
andauthored
Slightly tweaks async context to account for additional finalizer fields (#787)
* Utilizes Nebula Logger system messages framework to internally log info about different Apex async contexts _and_ provides more information in the body of the associated log entry (as well as what prints to the Apex debug console) for Queueable finalizers * Added methods in Logger_Tests to validate that an INFO log entry is auto-generated for async contexts (when system messages are enabled). This also includes a few changes for handling Exception instances not being serializable & adding some test helper methods * Re-ran prettier on README.md to fix some code formatting Co-authored-by: Jonathan Gillespie <jonathan.c.gillespie@gmail.com>
1 parent cf04d95 commit 8e859c2

10 files changed

Lines changed: 331 additions & 70 deletions

File tree

README.md

Lines changed: 58 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations.
77

8-
## Unlocked Package - v4.14.14
8+
## Unlocked Package - v4.14.15
99

10-
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY)
11-
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015oWIQAY)
10+
[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015obxQAA)
11+
[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000015obxQAA)
1212
[![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki)
1313

14-
`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015oWIQAY`
14+
`sf package install --wait 20 --security-type AdminsOnly --package 04t5Y0000015obxQAA`
1515

16-
`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015oWIQAY`
16+
`sfdx force:package:install --wait 20 --securitytype AdminsOnly --package 04t5Y0000015obxQAA`
1717

1818
---
1919

@@ -238,41 +238,41 @@ This example batchable class shows how you can leverage this feature to relate a
238238
239239
```apex
240240
public with sharing class BatchableLoggerExample implements Database.Batchable<SObject>, Database.Stateful {
241-
private String originalTransactionId;
241+
private String originalTransactionId;
242242
243-
public Database.QueryLocator start(Database.BatchableContext batchableContext) {
244-
// Each batchable method runs in a separate transaction,
245-
// so store the first transaction ID to later relate the other transactions
246-
this.originalTransactionId = Logger.getTransactionId();
243+
public Database.QueryLocator start(Database.BatchableContext batchableContext) {
244+
// Each batchable method runs in a separate transaction,
245+
// so store the first transaction ID to later relate the other transactions
246+
this.originalTransactionId = Logger.getTransactionId();
247247
248-
Logger.info('Starting BatchableLoggerExample');
249-
Logger.saveLog();
248+
Logger.info('Starting BatchableLoggerExample');
249+
Logger.saveLog();
250250
251-
// Just as an example, query all accounts
252-
return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]);
253-
}
254-
255-
public void execute(Database.BatchableContext batchableContext, List<Account> scope) {
256-
// One-time call (per transaction) to set the parent log
257-
Logger.setParentLogTransactionId(this.originalTransactionId);
251+
// Just as an example, query all accounts
252+
return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]);
253+
}
258254
259-
for (Account account : scope) {
260-
// Add your batch job's logic here
255+
public void execute(Database.BatchableContext batchableContext, List<Account> scope) {
256+
// One-time call (per transaction) to set the parent log
257+
Logger.setParentLogTransactionId(this.originalTransactionId);
261258
262-
// Then log the result
263-
Logger.info('Processed an account record', account);
264-
}
259+
for (Account account : scope) {
260+
// Add your batch job's logic here
265261
266-
Logger.saveLog();
262+
// Then log the result
263+
Logger.info('Processed an account record', account);
267264
}
268265
269-
public void finish(Database.BatchableContext batchableContext) {
270-
// The finish method runs in yet-another transaction, so set the parent log again
271-
Logger.setParentLogTransactionId(this.originalTransactionId);
266+
Logger.saveLog();
267+
}
272268
273-
Logger.info('Finishing running BatchableLoggerExample');
274-
Logger.saveLog();
275-
}
269+
public void finish(Database.BatchableContext batchableContext) {
270+
// The finish method runs in yet-another transaction, so set the parent log again
271+
Logger.setParentLogTransactionId(this.originalTransactionId);
272+
273+
Logger.info('Finishing running BatchableLoggerExample');
274+
Logger.saveLog();
275+
}
276276
}
277277
```
278278

@@ -282,42 +282,42 @@ Queueable jobs can also leverage the parent transaction ID to relate logs togeth
282282
283283
```apex
284284
public with sharing class QueueableLoggerExample implements Queueable {
285-
private Integer numberOfJobsToChain;
286-
private String parentLogTransactionId;
285+
private Integer numberOfJobsToChain;
286+
private String parentLogTransactionId;
287287
288-
private List<LogEntryEvent__e> logEntryEvents = new List<LogEntryEvent__e>();
288+
private List<LogEntryEvent__e> logEntryEvents = new List<LogEntryEvent__e>();
289289
290-
// Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs
291-
public QueueableLoggerExample(Integer numberOfJobsToChain) {
292-
this(numberOfJobsToChain, null);
293-
}
290+
// Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs
291+
public QueueableLoggerExample(Integer numberOfJobsToChain) {
292+
this(numberOfJobsToChain, null);
293+
}
294294
295-
// Second constructor, used to pass the original transaction's ID to each chained instance of the job
296-
// You don't have to use a constructor - a public method or property would work too.
297-
// There just needs to be a way to pass the value of parentLogTransactionId between instances
298-
public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) {
299-
this.numberOfJobsToChain = numberOfJobsToChain;
300-
this.parentLogTransactionId = parentLogTransactionId;
301-
}
295+
// Second constructor, used to pass the original transaction's ID to each chained instance of the job
296+
// You don't have to use a constructor - a public method or property would work too.
297+
// There just needs to be a way to pass the value of parentLogTransactionId between instances
298+
public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) {
299+
this.numberOfJobsToChain = numberOfJobsToChain;
300+
this.parentLogTransactionId = parentLogTransactionId;
301+
}
302302
303-
// Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain)
304-
public void execute(System.QueueableContext queueableContext) {
305-
Logger.setParentLogTransactionId(this.parentLogTransactionId);
303+
// Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain)
304+
public void execute(System.QueueableContext queueableContext) {
305+
Logger.setParentLogTransactionId(this.parentLogTransactionId);
306306
307-
Logger.fine('queueableContext==' + queueableContext);
308-
Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain);
309-
Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId);
307+
Logger.fine('queueableContext==' + queueableContext);
308+
Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain);
309+
Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId);
310310
311-
// Add your queueable job's logic here
311+
// Add your queueable job's logic here
312312
313-
Logger.saveLog();
313+
Logger.saveLog();
314314
315-
--this.numberOfJobsToChain;
316-
if (this.numberOfJobsToChain > 0) {
317-
String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId();
318-
System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId));
319-
}
315+
--this.numberOfJobsToChain;
316+
if (this.numberOfJobsToChain > 0) {
317+
String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId();
318+
System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId));
320319
}
320+
}
321321
}
322322
```
323323

nebula-logger/core/main/logger-engine/classes/Logger.cls

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
global with sharing class Logger {
1616
// There's no reliable way to get the version number dynamically in Apex
1717
@TestVisible
18-
private static final String CURRENT_VERSION_NUMBER = 'v4.14.14';
18+
private static final String CURRENT_VERSION_NUMBER = 'v4.14.15';
1919
private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG;
2020
private static final List<LogEntryEventBuilder> LOG_ENTRIES_BUFFER = new List<LogEntryEventBuilder>();
2121
private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.';
@@ -3473,7 +3473,9 @@ global with sharing class Logger {
34733473
// intended behavior
34743474
if (currentAsyncContext == null) {
34753475
currentAsyncContext = asyncContext;
3476-
System.debug(System.LoggingLevel.INFO, 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext));
3476+
if (LoggerParameter.ENABLE_SYSTEM_MESSAGES) {
3477+
info('Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext, true)).setExceptionDetails(asyncContext.finalizerException);
3478+
}
34773479
}
34783480
}
34793481

@@ -3584,11 +3586,31 @@ global with sharing class Logger {
35843586

35853587
// Inner class for tracking details about the current transaction's async context
35863588
@SuppressWarnings('PMD.ApexDoc')
3589+
@TestVisible
35873590
private class AsyncContext {
35883591
public final String type;
35893592
public final String parentJobId;
35903593
public final String childJobId;
35913594
public final String triggerId;
3595+
public final String finalizerResult;
3596+
// Instances of Exception can't be serialized, but instances of AsyncContext
3597+
// are sometimes serialized - so, the AsyncContext's Exception is transient,
3598+
// and an extra getter for Map<String, Object>, containing the exception's data,
3599+
// is used to provide a quick & easy serializable version
3600+
public transient final Exception finalizerException;
3601+
public Map<String, Object> finalizerUnhandledException {
3602+
get {
3603+
return finalizerException == null
3604+
? null
3605+
: new Map<String, Object>{
3606+
'cause' => this.finalizerException.getCause(),
3607+
'lineNumber' => this.finalizerException.getLineNumber(),
3608+
'message' => this.finalizerException.getMessage(),
3609+
'stackTraceString' => this.finalizerException.getStackTraceString(),
3610+
'typeName' => this.finalizerException.getTypeName()
3611+
};
3612+
}
3613+
}
35923614

35933615
public AsyncContext(Database.BatchableContext batchableContext) {
35943616
this.childJobId = batchableContext?.getChildJobId();
@@ -3597,7 +3619,11 @@ global with sharing class Logger {
35973619
}
35983620

35993621
public AsyncContext(System.FinalizerContext finalizerContext) {
3600-
this.parentJobId = finalizerContext?.getAsyncApexJobId();
3622+
if (finalizerContext != null) {
3623+
this.finalizerException = finalizerContext.getException();
3624+
this.finalizerResult = finalizerContext.getResult()?.name();
3625+
this.parentJobId = finalizerContext.getAsyncApexJobId();
3626+
}
36013627
this.type = System.FinalizerContext.class.getName();
36023628
}
36033629

nebula-logger/core/main/logger-engine/lwc/logger/loggerService.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import LoggerServiceTaskQueue from './loggerServiceTaskQueue';
1010
import getSettings from '@salesforce/apex/ComponentLogger.getSettings';
1111
import saveComponentLogEntries from '@salesforce/apex/ComponentLogger.saveComponentLogEntries';
1212

13-
const CURRENT_VERSION_NUMBER = 'v4.14.14';
13+
const CURRENT_VERSION_NUMBER = 'v4.14.15';
1414

1515
const CONSOLE_OUTPUT_CONFIG = {
1616
messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `,

nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ public class LoggerMockDataCreator {
323323
public static Schema.Organization getOrganization() {
324324
// TODO Switch to creating mock instance of Schema.Organization with sensible defaults that tests can then update as needed for different scenarios
325325
if (cachedOrganization == null) {
326-
cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization];
326+
cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization LIMIT 1];
327327
}
328328
return cachedOrganization;
329329
}
@@ -457,6 +457,7 @@ public class LoggerMockDataCreator {
457457

458458
@SuppressWarnings('PMD.ApexDoc')
459459
public class MockFinalizerContext implements System.FinalizerContext {
460+
private Exception apexException;
460461
private Id asyncApexJobId;
461462

462463
public MockFinalizerContext() {
@@ -467,19 +468,24 @@ public class LoggerMockDataCreator {
467468
this.asyncApexJobId = asyncApexJobId;
468469
}
469470

471+
public MockFinalizerContext(Exception ex) {
472+
this();
473+
this.apexException = ex;
474+
}
475+
470476
public Id getAsyncApexJobId() {
471477
return this.asyncApexJobId;
472478
}
473479

474480
public Exception getException() {
475-
return null;
481+
return this.apexException;
476482
}
477483

478484
public System.ParentJobResult getResult() {
479-
return System.ParentJobResult.SUCCESS;
485+
return this.apexException == null ? System.ParentJobResult.SUCCESS : System.ParentJobResult.UNHANDLED_EXCEPTION;
480486
}
481487

482-
public Id getRequestId() {
488+
public String getRequestId() {
483489
return System.Request.getCurrent().getRequestId();
484490
}
485491
}

0 commit comments

Comments
 (0)