diff --git a/nebula-logger/core/main/logger-engine/classes/Logger.cls b/nebula-logger/core/main/logger-engine/classes/Logger.cls index 12ca73043..5d6bc9926 100644 --- a/nebula-logger/core/main/logger-engine/classes/Logger.cls +++ b/nebula-logger/core/main/logger-engine/classes/Logger.cls @@ -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, containing the exception's data, + // is used to provide a quick & easy serializable version + public transient final Exception finalizerException; + public Map finalizerUnhandledException { + get { + return finalizerException == null + ? null + : new Map{ + '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(); @@ -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(); diff --git a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls index 6b95a0b52..72419c376 100644 --- a/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls +++ b/nebula-logger/core/tests/configuration/utilities/LoggerMockDataCreator.cls @@ -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; } @@ -457,6 +457,7 @@ public class LoggerMockDataCreator { @SuppressWarnings('PMD.ApexDoc') public class MockFinalizerContext implements System.FinalizerContext { + private Exception apexException; private Id asyncApexJobId; public MockFinalizerContext() { @@ -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(); } } diff --git a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls index 2bd16ad8b..1c7c0bd2d 100644 --- a/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls +++ b/nebula-logger/core/tests/logger-engine/classes/Logger_Tests.cls @@ -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 matchingPublishedLogEntryEvents = (List) 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); @@ -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 matchingPublishedLogEntryEvents = (List) 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 matchingPublishedLogEntryEvents = (List) 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); @@ -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 matchingPublishedLogEntryEvents = (List) 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); @@ -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 matchingPublishedLogEntryEvents = (List) 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'; diff --git a/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls b/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls index 37311c772..70eb581f9 100644 --- a/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls +++ b/nebula-logger/core/tests/logger-engine/utilities/LoggerMockDataStore.cls @@ -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 getMatchingPublishedPlatformEvents(SObject comparisonPlatformEvent) { + Map comparisonFieldToValue = comparisonPlatformEvent.getPopulatedFieldsAsMap(); + List matchingRecords = new List(); + for (SObject eventToCheck : this.getPublishedPlatformEvents()) { + Boolean isMatchingRecord = true; + Map 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{ platformEvent }).get(0); } diff --git a/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls new file mode 100644 index 000000000..4294d6b06 --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls @@ -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 { +} diff --git a/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml new file mode 100644 index 000000000..651b17293 --- /dev/null +++ b/nebula-logger/extra-tests/classes/name-shadowing/System/ParentJobResult.cls-meta.xml @@ -0,0 +1,5 @@ + + + 61.0 + Active +