Skip to content

Commit

Permalink
Added methods in Logger_Tests to validate that an INFO log entry is a…
Browse files Browse the repository at this point in the history
…uto-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
  • Loading branch information
jongpie committed Oct 27, 2024
1 parent 2745784 commit 3366d40
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 6 deletions.
22 changes: 20 additions & 2 deletions nebula-logger/core/main/logger-engine/classes/Logger.cls
Original file line number Diff line number Diff line change
Expand Up @@ -3586,13 +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;
public final Exception finalizerException;
// 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 @@ -3603,7 +3621,7 @@ global with sharing class Logger {
public AsyncContext(System.FinalizerContext finalizerContext) {
if (finalizerContext != null) {
this.finalizerException = finalizerContext.getException();
this.finalizerResult = finalizerContext.getResult().name();
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 @@ -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
180 changes: 180 additions & 0 deletions nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls
Original file line number Diff line number Diff line change
Expand Up @@ -825,6 +825,41 @@ private class Logger_Tests {
System.Assert.areEqual(Database.BatchableContext.class.getName(), logEntryEvent.AsyncContextType__c);
}

@IsTest
static void it_should_auto_create_log_entry_event_for_batchable_async_context_details_when_system_messages_are_enabled() {
Database.BatchableContext mockContext = new LoggerMockDataCreator.MockBatchableContext();
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled');
System.Assert.areEqual(0, Logger.getBufferSize());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

Logger.setAsyncContext(mockContext);
Logger.saveLog(Logger.SaveMethod.EVENT_BUS);

System.Assert.areEqual(0, Logger.getBufferSize());
Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size();
System.Assert.isTrue(
publishedEventsCount >= 1,
publishedEventsCount +
' events published, but at least 1 should have been auto-created & published\n\n' +
System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents())
);
LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e(
LoggingLevel__c = System.LoggingLevel.INFO.name(),
Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true)
);
List<LogEntryEvent__e> matchingPublishedLogEntryEvents = (List<LogEntryEvent__e>) LoggerMockDataStore.getEventBus()
.getMatchingPublishedPlatformEvents(expectedLogEntryEvent);
System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size());
LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0);
System.Assert.areEqual(Database.BatchableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c);
System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c);
System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c);
}

@IsTest
static void it_should_set_async_context_details_for_finalizer_context_when_event_published() {
Id mockParentAsyncApexJobId = LoggerMockDataCreator.createId(Schema.AsyncApexJob.SObjectType);
Expand Down Expand Up @@ -884,6 +919,81 @@ private class Logger_Tests {
System.Assert.areEqual(System.FinalizerContext.class.getName(), logEntryEvent.AsyncContextType__c);
}

@IsTest
static void it_should_auto_create_log_entry_event_for_success_finalizer_async_context_details_when_system_messages_are_enabled() {
System.FinalizerContext mockContext = new LoggerMockDataCreator.MockFinalizerContext();
System.Assert.isNull(mockContext.getException());
System.Assert.areEqual(System.ParentJobResult.SUCCESS, mockContext.getResult());
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled');
System.Assert.areEqual(0, Logger.getBufferSize());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

Logger.setAsyncContext(mockContext);
Logger.saveLog(Logger.SaveMethod.EVENT_BUS);

System.Assert.areEqual(0, Logger.getBufferSize());
Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size();
System.Assert.isTrue(
publishedEventsCount >= 1,
publishedEventsCount +
' events published, but at least 1 should have been auto-created & published\n\n' +
System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents())
);
LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e(
LoggingLevel__c = System.LoggingLevel.INFO.name(),
Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true)
);
List<LogEntryEvent__e> matchingPublishedLogEntryEvents = (List<LogEntryEvent__e>) LoggerMockDataStore.getEventBus()
.getMatchingPublishedPlatformEvents(expectedLogEntryEvent);
System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size());
LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0);
System.Assert.areEqual(System.FinalizerContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c);
System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c);
System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c);
}

@IsTest
static void it_should_auto_create_log_entry_event_for_unhandled_exception_finalizer_async_context_details_when_system_messages_are_enabled() {
System.DmlException mockFinalizerUnhandledException = new System.DmlException('Oops');
System.FinalizerContext mockContext = new LoggerMockDataCreator.MockFinalizerContext(mockFinalizerUnhandledException);
System.Assert.isNotNull(mockContext.getException());
System.Assert.areEqual(System.ParentJobResult.UNHANDLED_EXCEPTION, mockContext.getResult());
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled');
System.Assert.areEqual(0, Logger.getBufferSize());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

Logger.setAsyncContext(mockContext);
Logger.saveLog(Logger.SaveMethod.EVENT_BUS);

System.Assert.areEqual(0, Logger.getBufferSize());
Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size();
System.Assert.isTrue(
publishedEventsCount >= 1,
publishedEventsCount +
' events published, but at least 1 should have been auto-created & published\n\n' +
System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents())
);
LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e(
LoggingLevel__c = System.LoggingLevel.INFO.name(),
Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true)
);
List<LogEntryEvent__e> matchingPublishedLogEntryEvents = (List<LogEntryEvent__e>) LoggerMockDataStore.getEventBus()
.getMatchingPublishedPlatformEvents(expectedLogEntryEvent);
System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size());
LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0);
System.Assert.areEqual(System.FinalizerContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c);
System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c);
System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c);
System.Assert.areEqual(mockFinalizerUnhandledException.getMessage(), matchingPublishedLogEntryEvent.ExceptionMessage__c);
System.Assert.areEqual(mockFinalizerUnhandledException.getTypeName(), matchingPublishedLogEntryEvent.ExceptionType__c);
}

@IsTest
static void it_should_set_async_context_details_for_queueable_context_when_event_published() {
Id mockParentAsyncApexJobId = LoggerMockDataCreator.createId(Schema.AsyncApexJob.SObjectType);
Expand Down Expand Up @@ -943,6 +1053,41 @@ private class Logger_Tests {
System.Assert.areEqual(System.QueueableContext.class.getName(), logEntryEvent.AsyncContextType__c);
}

@IsTest
static void it_should_auto_create_log_entry_event_for_queueable_async_context_details_when_system_messages_are_enabled() {
System.QueueableContext mockContext = new LoggerMockDataCreator.MockQueueableContext();
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled');
System.Assert.areEqual(0, Logger.getBufferSize());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

Logger.setAsyncContext(mockContext);
Logger.saveLog(Logger.SaveMethod.EVENT_BUS);

System.Assert.areEqual(0, Logger.getBufferSize());
Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size();
System.Assert.isTrue(
publishedEventsCount >= 1,
publishedEventsCount +
' events published, but at least 1 should have been auto-created & published\n\n' +
System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents())
);
LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e(
LoggingLevel__c = System.LoggingLevel.INFO.name(),
Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true)
);
List<LogEntryEvent__e> matchingPublishedLogEntryEvents = (List<LogEntryEvent__e>) LoggerMockDataStore.getEventBus()
.getMatchingPublishedPlatformEvents(expectedLogEntryEvent);
System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size());
LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0);
System.Assert.areEqual(System.QueueableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c);
System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c);
System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c);
}

@IsTest
static void it_should_set_async_context_details_for_schedulable_context_when_event_published() {
Id mockCronTriggerId = LoggerMockDataCreator.createId(Schema.CronTrigger.SObjectType);
Expand Down Expand Up @@ -1001,6 +1146,41 @@ private class Logger_Tests {
System.Assert.areEqual(System.SchedulableContext.class.getName(), logEntryEvent.AsyncContextType__c);
}

@IsTest
static void it_should_auto_create_log_entry_event_for_scheduleable_async_context_details_when_system_messages_are_enabled() {
System.SchedulableContext mockContext = new LoggerMockDataCreator.MockSchedulableContext();
LoggerDataStore.setMock(LoggerMockDataStore.getEventBus());
LoggerTestConfigurator.setMock(new LoggerParameter__mdt(DeveloperName = 'EnableLoggerSystemMessages', Value__c = 'true'));
System.Assert.isTrue(LoggerParameter.ENABLE_SYSTEM_MESSAGES, 'System messages should be enabled');
System.Assert.areEqual(0, Logger.getBufferSize());
System.Assert.areEqual(0, LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size());

Logger.setAsyncContext(mockContext);
Logger.saveLog(Logger.SaveMethod.EVENT_BUS);

System.Assert.areEqual(0, Logger.getBufferSize());
Integer publishedEventsCount = LoggerMockDataStore.getEventBus().getPublishedPlatformEvents().size();
System.Assert.isTrue(
publishedEventsCount >= 1,
publishedEventsCount +
' events published, but at least 1 should have been auto-created & published\n\n' +
System.JSON.serializePretty(LoggerMockDataStore.getEventBus().getPublishedPlatformEvents())
);
LogEntryEvent__e expectedLogEntryEvent = new LogEntryEvent__e(
LoggingLevel__c = System.LoggingLevel.INFO.name(),
Message__c = 'Nebula Logger - Async Context: ' + System.JSON.serializePretty(new Logger.AsyncContext(mockContext), true)
);
List<LogEntryEvent__e> matchingPublishedLogEntryEvents = (List<LogEntryEvent__e>) LoggerMockDataStore.getEventBus()
.getMatchingPublishedPlatformEvents(expectedLogEntryEvent);
System.Assert.areEqual(1, matchingPublishedLogEntryEvents.size());
LogEntryEvent__e matchingPublishedLogEntryEvent = matchingPublishedLogEntryEvents.get(0);
System.Assert.areEqual(System.SchedulableContext.class.getName(), matchingPublishedLogEntryEvent.AsyncContextType__c);
System.Assert.areEqual(expectedLogEntryEvent.LoggingLevel__c, matchingPublishedLogEntryEvent.LoggingLevel__c);
System.Assert.areEqual(expectedLogEntryEvent.Message__c, matchingPublishedLogEntryEvent.Message__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionMessage__c);
System.Assert.isNull(matchingPublishedLogEntryEvent.ExceptionType__c);
}

@IsTest
static void it_should_set_parent_transaction_id() {
String expectedParentTransactionId = 'imagineThisWereAGuid';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,39 @@ public without sharing class LoggerMockDataStore {
return this.publishedPlatformEvents;
}

/**
* @description Returns a list of published platform events that have the same field values
* as the provided platform event record `comparisonPlatformEvent`. This is useful for
* easily filtering to only the `LogEntryEvent__e` records relevant to a particular test method
* in a transaction/test scenario where multiple `LogEntryEvent__e` are being generated.
* Long-term, this helper method might be moved elsewhere, or replaced with something else,
* but for now, the mock event bus is a good-enough spot for it.
* @param comparisonPlatformEvent An instance of the platform event record to use for comparing
* against the list of platform event records that have been published
* @return A list containing any matches. When no matches are found, the list is empty.
*/
public List<SObject> getMatchingPublishedPlatformEvents(SObject comparisonPlatformEvent) {
Map<String, Object> comparisonFieldToValue = comparisonPlatformEvent.getPopulatedFieldsAsMap();
List<SObject> matchingRecords = new List<SObject>();
for (SObject eventToCheck : this.getPublishedPlatformEvents()) {
Boolean isMatchingRecord = true;
Map<String, Object> targetFieldToValue = eventToCheck.getPopulatedFieldsAsMap();

for (String populatedField : comparisonFieldToValue.keySet()) {
Object expectedValue = comparisonFieldToValue.get(populatedField);
if (targetFieldToValue.containsKey(populatedField) == false || eventToCheck.get(populatedField) != expectedValue) {
isMatchingRecord = false;
break;
}
}

if (isMatchingRecord) {
matchingRecords.add(eventToCheck);
}
}
return matchingRecords;
}

public override Database.SaveResult publishRecord(SObject platformEvent) {
return this.publishRecords(new List<SObject>{ platformEvent }).get(0);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,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. //
//------------------------------------------------------------------------------------------------//

// This class intentionally does nothing - it's here to ensure that any references
// in Nebula Logger's codebase use `System.ParentJobResult` instead of just `ParentJobResult`
@SuppressWarnings('PMD.ApexDoc, PMD.EmptyStatementBlock')
public without sharing class ParentJobResult {
}
Loading

0 comments on commit 3366d40

Please sign in to comment.