Skip to content

Commit

Permalink
Slightly tweaks async context to account for additional finalizer fie…
Browse files Browse the repository at this point in the history
…lds (#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 <[email protected]>
  • Loading branch information
jamessimone and jongpie authored Oct 29, 2024
1 parent cf04d95 commit 8e859c2
Show file tree
Hide file tree
Showing 10 changed files with 331 additions and 70 deletions.
116 changes: 58 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,15 @@

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.

## Unlocked Package - v4.14.14
## Unlocked Package - v4.14.15

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

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

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

---

Expand Down Expand Up @@ -238,41 +238,41 @@ This example batchable class shows how you can leverage this feature to relate a
```apex
public with sharing class BatchableLoggerExample implements Database.Batchable<SObject>, Database.Stateful {
private String originalTransactionId;
private String originalTransactionId;
public Database.QueryLocator start(Database.BatchableContext batchableContext) {
// Each batchable method runs in a separate transaction,
// so store the first transaction ID to later relate the other transactions
this.originalTransactionId = Logger.getTransactionId();
public Database.QueryLocator start(Database.BatchableContext batchableContext) {
// Each batchable method runs in a separate transaction,
// so store the first transaction ID to later relate the other transactions
this.originalTransactionId = Logger.getTransactionId();
Logger.info('Starting BatchableLoggerExample');
Logger.saveLog();
Logger.info('Starting BatchableLoggerExample');
Logger.saveLog();
// Just as an example, query all accounts
return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]);
}
public void execute(Database.BatchableContext batchableContext, List<Account> scope) {
// One-time call (per transaction) to set the parent log
Logger.setParentLogTransactionId(this.originalTransactionId);
// Just as an example, query all accounts
return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]);
}
for (Account account : scope) {
// Add your batch job's logic here
public void execute(Database.BatchableContext batchableContext, List<Account> scope) {
// One-time call (per transaction) to set the parent log
Logger.setParentLogTransactionId(this.originalTransactionId);
// Then log the result
Logger.info('Processed an account record', account);
}
for (Account account : scope) {
// Add your batch job's logic here
Logger.saveLog();
// Then log the result
Logger.info('Processed an account record', account);
}
public void finish(Database.BatchableContext batchableContext) {
// The finish method runs in yet-another transaction, so set the parent log again
Logger.setParentLogTransactionId(this.originalTransactionId);
Logger.saveLog();
}
Logger.info('Finishing running BatchableLoggerExample');
Logger.saveLog();
}
public void finish(Database.BatchableContext batchableContext) {
// The finish method runs in yet-another transaction, so set the parent log again
Logger.setParentLogTransactionId(this.originalTransactionId);
Logger.info('Finishing running BatchableLoggerExample');
Logger.saveLog();
}
}
```

Expand All @@ -282,42 +282,42 @@ Queueable jobs can also leverage the parent transaction ID to relate logs togeth
```apex
public with sharing class QueueableLoggerExample implements Queueable {
private Integer numberOfJobsToChain;
private String parentLogTransactionId;
private Integer numberOfJobsToChain;
private String parentLogTransactionId;
private List<LogEntryEvent__e> logEntryEvents = new List<LogEntryEvent__e>();
private List<LogEntryEvent__e> logEntryEvents = new List<LogEntryEvent__e>();
// Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs
public QueueableLoggerExample(Integer numberOfJobsToChain) {
this(numberOfJobsToChain, null);
}
// Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs
public QueueableLoggerExample(Integer numberOfJobsToChain) {
this(numberOfJobsToChain, null);
}
// Second constructor, used to pass the original transaction's ID to each chained instance of the job
// You don't have to use a constructor - a public method or property would work too.
// There just needs to be a way to pass the value of parentLogTransactionId between instances
public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) {
this.numberOfJobsToChain = numberOfJobsToChain;
this.parentLogTransactionId = parentLogTransactionId;
}
// Second constructor, used to pass the original transaction's ID to each chained instance of the job
// You don't have to use a constructor - a public method or property would work too.
// There just needs to be a way to pass the value of parentLogTransactionId between instances
public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) {
this.numberOfJobsToChain = numberOfJobsToChain;
this.parentLogTransactionId = parentLogTransactionId;
}
// Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain)
public void execute(System.QueueableContext queueableContext) {
Logger.setParentLogTransactionId(this.parentLogTransactionId);
// Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain)
public void execute(System.QueueableContext queueableContext) {
Logger.setParentLogTransactionId(this.parentLogTransactionId);
Logger.fine('queueableContext==' + queueableContext);
Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain);
Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId);
Logger.fine('queueableContext==' + queueableContext);
Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain);
Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId);
// Add your queueable job's logic here
// Add your queueable job's logic here
Logger.saveLog();
Logger.saveLog();
--this.numberOfJobsToChain;
if (this.numberOfJobsToChain > 0) {
String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId();
System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId));
}
--this.numberOfJobsToChain;
if (this.numberOfJobsToChain > 0) {
String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId();
System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId));
}
}
}
```

Expand Down
32 changes: 29 additions & 3 deletions nebula-logger/core/main/logger-engine/classes/Logger.cls
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
global with sharing class Logger {
// There's no reliable way to get the version number dynamically in Apex
@TestVisible
private static final String CURRENT_VERSION_NUMBER = 'v4.14.14';
private static final String CURRENT_VERSION_NUMBER = 'v4.14.15';
private static final System.LoggingLevel FALLBACK_LOGGING_LEVEL = System.LoggingLevel.DEBUG;
private static final List<LogEntryEventBuilder> LOG_ENTRIES_BUFFER = new List<LogEntryEventBuilder>();
private static final String MISSING_SCENARIO_ERROR_MESSAGE = 'No logger scenario specified. A scenario is required for logging in this org.';
Expand Down Expand Up @@ -3473,7 +3473,9 @@ global with sharing class Logger {
// intended behavior
if (currentAsyncContext == null) {
currentAsyncContext = asyncContext;
System.debug(System.LoggingLevel.INFO, 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext));
if (LoggerParameter.ENABLE_SYSTEM_MESSAGES) {
info('Nebula Logger - Async Context: ' + System.JSON.serializePretty(asyncContext, true)).setExceptionDetails(asyncContext.finalizerException);
}
}
}

Expand Down Expand Up @@ -3584,11 +3586,31 @@ global with sharing class Logger {

// Inner class for tracking details about the current transaction's async context
@SuppressWarnings('PMD.ApexDoc')
@TestVisible
private class AsyncContext {
public final String type;
public final String parentJobId;
public final String childJobId;
public final String triggerId;
public final String finalizerResult;
// Instances of Exception can't be serialized, but instances of AsyncContext
// are sometimes serialized - so, the AsyncContext's Exception is transient,
// and an extra getter for Map<String, Object>, containing the exception's data,
// is used to provide a quick & easy serializable version
public transient final Exception finalizerException;
public Map<String, Object> finalizerUnhandledException {
get {
return finalizerException == null
? null
: new Map<String, Object>{
'cause' => this.finalizerException.getCause(),
'lineNumber' => this.finalizerException.getLineNumber(),
'message' => this.finalizerException.getMessage(),
'stackTraceString' => this.finalizerException.getStackTraceString(),
'typeName' => this.finalizerException.getTypeName()
};
}
}

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

public AsyncContext(System.FinalizerContext finalizerContext) {
this.parentJobId = finalizerContext?.getAsyncApexJobId();
if (finalizerContext != null) {
this.finalizerException = finalizerContext.getException();
this.finalizerResult = finalizerContext.getResult()?.name();
this.parentJobId = finalizerContext.getAsyncApexJobId();
}
this.type = System.FinalizerContext.class.getName();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import LoggerServiceTaskQueue from './loggerServiceTaskQueue';
import getSettings from '@salesforce/apex/ComponentLogger.getSettings';
import saveComponentLogEntries from '@salesforce/apex/ComponentLogger.saveComponentLogEntries';

const CURRENT_VERSION_NUMBER = 'v4.14.14';
const CURRENT_VERSION_NUMBER = 'v4.14.15';

const CONSOLE_OUTPUT_CONFIG = {
messagePrefix: `%c Nebula Logger ${CURRENT_VERSION_NUMBER} `,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ public class LoggerMockDataCreator {
public static Schema.Organization getOrganization() {
// TODO Switch to creating mock instance of Schema.Organization with sensible defaults that tests can then update as needed for different scenarios
if (cachedOrganization == null) {
cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization];
cachedOrganization = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization LIMIT 1];
}
return cachedOrganization;
}
Expand Down Expand Up @@ -457,6 +457,7 @@ public class LoggerMockDataCreator {

@SuppressWarnings('PMD.ApexDoc')
public class MockFinalizerContext implements System.FinalizerContext {
private Exception apexException;
private Id asyncApexJobId;

public MockFinalizerContext() {
Expand All @@ -467,19 +468,24 @@ public class LoggerMockDataCreator {
this.asyncApexJobId = asyncApexJobId;
}

public MockFinalizerContext(Exception ex) {
this();
this.apexException = ex;
}

public Id getAsyncApexJobId() {
return this.asyncApexJobId;
}

public Exception getException() {
return null;
return this.apexException;
}

public System.ParentJobResult getResult() {
return System.ParentJobResult.SUCCESS;
return this.apexException == null ? System.ParentJobResult.SUCCESS : System.ParentJobResult.UNHANDLED_EXCEPTION;
}

public Id getRequestId() {
public String getRequestId() {
return System.Request.getCurrent().getRequestId();
}
}
Expand Down
Loading

0 comments on commit 8e859c2

Please sign in to comment.