Skip to content

Commit

Permalink
Added support for plugins within LogBatchPurger + added archiving in …
Browse files Browse the repository at this point in the history
…BigObject plugin (#288)

* Replaced LoggerSObjectHandlerPlugin abstract class with new class LoggerPlugin that contains 2 interfaces + helper methods, switched to using multiple fields on LoggerPlugin__mdt to indicate Apex classes & Flows to run for a plugin, removed SObject-specific fields on LoggerPlugin__mdt, added new fields Log__c.LogPurgeAction__c and LoggerSettings__c.DefaultLogPurgeAction__c

* Added tests in LoggerSettingsController_Tests & LogHandler_Tests for new custom setting LoggerSettings__c.DefaultLogPurgeAction__c

* Updated LoggerSObjectHandler & plugin classes to use the NEW new plugin overhaul changes via LoggerPlugin class, removed old class LoggerSObjectHanderPlugin

* Expanded the plugin framework to support some aspects of #128 by adding the ability to create plugins for LogBatchPurger, using the interface LoggerPlugin.Batchable
    - The BigObject plugin (and other plugins) can then leverage this to run additional logic before Log__c, LogEntry__c & LogEntryTag__c records are hard-deleted
    - For the BigObject plugin, it will be able to archive data into LogEntryArchive__b before LogBatchPurger deletes the data within the custom objects
    - Also finished some test improvements for triggerable plugins within LoggerSObjectHandler

* Renamed BigObject plugin's CMDT file for save method to reflect the naming convention change ('CustomSaveMethodBigObject' instead of 'AdditionalSaveMethodsBigObject'), and renamed plugin CMDT file from 'BigObjectArchiving' to 'LogEntryArchiving', updated labels on some deprecated fields

* Added 'deprecated' to the label of several deprecated fields on LoggerPlugin__mdt & removed the related handler methods

* WIP Made progress on #128 - Split part of LogEntryArchiveBuilder into a new class, LogEntryArchivePlugin, and implemented interface LoggerPlugin.Batchable so that Log__c/LogEntry__c/LogEntryTag__c records can be archived via LogBatchPurger before they're deleted
- The plugin class handles talking with Logger, and builder class handles converting LogEntryEvent__e or LogEntry__c records to LogEntryArchive__b records

* Added tests for LogBatchPurger integration in LogEntryArchivePlugin_Tests

* WIP Stubbed out a new LWC + controller class + custom tab for viewing LogEntryArchive__b as part of #117

* Standardized test-visible method naming conventions to start with `setMock` & updated approaches to use a Map instead of List for CMDT records

* Finished some TODOs & standardized mocking approach for CMDT in LogHandler

* Added Log__c list view to show logs that will be purged in the next 10 days, fixed LogEntryArchiveBuilder using the wrong value for LoggedById__c, updated index on LogEntryArchive__b again (still a WIP), retrieved & formatted metadata for LogEntryArchive__b, updated Admin.profile

* Fixed some FLS  issues

* More improvements for CMDT records used in the classes LoggerParameter & LoggerPlugin
    - Added LoggerParameter.matchOnPrefix() to return CMDT records with a specified prefix in the DeveloperName field
    - Added inner Comparable class in LoggerPlugin to handle custom sorting, since there are some limitations with SOQL queries for CMDT
    - Classes like LogBatchPurger, LoggerSObjectHandler, and LoggerSettingsController no longer need to track their own mock CMDT records, the LoggerPlugin & LoggerParameter classes now fully handle mocks for their corresponding CMDT objects
    - LoggerParameter & LoggerPlugin now require mock CMDT records to have DeveloperName populated - they'll throw errors if the field is null
    - Also added System namespace to some calls for Test.isRunningTest() to handle crazy orgs that have a custom Test class deployed

* Added the ability to disable all triggers during tests via new @testvisible method LoggerSObjectHandler.shouldExecute(Boolean)

* Removed old integration test class + Flow for testing Flow plugins in LoggerSObjectHandler - I'll reimplement new tests when I also implement Flow plugins for LogBatchPurger

* Moved LoggerTestUtils to the configuration folder, added public methods for mocking all CMDT records

* Consolidated plugin-framework folders back into the configuration folders
  • Loading branch information
jongpie authored Mar 27, 2022
1 parent b3ebafc commit d25e766
Show file tree
Hide file tree
Showing 227 changed files with 4,196 additions and 2,250 deletions.
2 changes: 1 addition & 1 deletion .github/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ coverage:
if_ci_failed: success
patch: off
ignore:
- 'nebula-logger-recipes/**/*'
- 'nebula-logger/recipes/**/*'
comment:
behavior: new
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ Designed for Salesforce admins, developers & architects. A robust logger for Ape

## Unlocked Package - v4.7.1

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

## Managed Package - v4.7.0
Expand Down
3 changes: 3 additions & 0 deletions config/scratch-orgs/base-scratch-def.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"communitiesSettings": {
"enableNetworksEnabled": false
},
"pathAssistantSettings": {
"pathAssistantEnabled": false
},
"userManagementSettings": {
"enableEnhancedPermsetMgmt": true,
"enableEnhancedProfileMgmt": true,
Expand Down
3 changes: 3 additions & 0 deletions config/scratch-orgs/experience-cloud-scratch-def.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
"experienceBundleSettings": {
"enableExperienceBundleMetadata": true
},
"pathAssistantSettings": {
"pathAssistantEnabled": true
},
"userManagementSettings": {
"enableEnhancedPermsetMgmt": true,
"enableEnhancedProfileMgmt": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,8 @@ public without sharing class LoggerEmailUtils {
Messaging.reserveSingleEmailCapacity(1);
Messaging.reserveMassEmailCapacity(1);
IS_EMAIL_DELIVERABILITY_ENABLED = true;
return IS_EMAIL_DELIVERABILITY_ENABLED;
} catch (System.NoAccessException e) {
IS_EMAIL_DELIVERABILITY_ENABLED = false;
return IS_EMAIL_DELIVERABILITY_ENABLED;
}
}
return IS_EMAIL_DELIVERABILITY_ENABLED;
Expand Down
59 changes: 42 additions & 17 deletions nebula-logger/core/main/configuration/classes/LoggerParameter.cls
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
*/
@SuppressWarnings('PMD.CyclomaticComplexity, PMD.ExcessivePublicCount, PMD.PropertyNamingConventions')
public class LoggerParameter {
private static Set<String> parametersToLoadDuringTests = new Set<String>();
private static Map<String, LoggerParameter__mdt> mockParameterByDeveloperName = new Map<String, LoggerParameter__mdt>();
private static final Set<String> PARAMETERS_TO_LOAD_DURING_TESTS = new Set<String>();
private static final Map<String, LoggerParameter__mdt> DEVELOPER_NAME_TO_RECORD = loadRecords();

/**
* @description Indicates if Logger will make an async callout to https://api.status.salesforce.com
Expand Down Expand Up @@ -66,7 +66,7 @@ public class LoggerParameter {
// During tests, always load this CMDT record
// so that tests use the same format when calling System.debug()
String systemDebugMessageFormatParameter = 'SystemDebugMessageFormat';
parametersToLoadDuringTests.add(systemDebugMessageFormatParameter);
PARAMETERS_TO_LOAD_DURING_TESTS.add(systemDebugMessageFormatParameter);
SYSTEM_DEBUG_MESSAGE_FORMAT = getString(systemDebugMessageFormatParameter, '{OriginLocation__c}\n{Message__c}');
}
return SYSTEM_DEBUG_MESSAGE_FORMAT;
Expand Down Expand Up @@ -340,10 +340,45 @@ public class LoggerParameter {
return parameterValue != null ? parameterValue : defaultValue;
}

// Private methods
/**
* @description matchOnPrefix description
* @param developerNamePrefix A prefix that has been used in the `DeveloperName` for multiple `LoggerParameter__mdt` records
* @return The list of matching `LoggerParameter__mdt` records
*/
public static List<LoggerParameter__mdt> matchOnPrefix(String developerNamePrefix) {
List<LoggerParameter__mdt> matchingParameters = new List<LoggerParameter__mdt>();
for (String parameterDeveloperName : DEVELOPER_NAME_TO_RECORD.keySet()) {
if (parameterDeveloperName.startsWith(developerNamePrefix) == true) {
matchingParameters.add(DEVELOPER_NAME_TO_RECORD.get(parameterDeveloperName));
}
}
return matchingParameters;
}

private static Map<String, LoggerParameter__mdt> loadRecords() {
Map<String, LoggerParameter__mdt> parameters = LoggerParameter__mdt.getAll().clone();
if (System.Test.isRunningTest() == true) {
// Keep a copy of any records that *should* be loaded during tests
// Currently, only the record `SystemDebugMessageFormat` has a use case for this functionality,
// but others can be easily added if other use cases are found
Map<String, LoggerParameter__mdt> parametersToLoadDuringTests = new Map<String, LoggerParameter__mdt>();
for (String testContextParameterName : PARAMETERS_TO_LOAD_DURING_TESTS) {
if (parameters.containsKey(testContextParameterName) == true) {
parametersToLoadDuringTests.put(testContextParameterName, parameters.get(testContextParameterName));
}
}
parameters.clear();
parameters.putAll(parametersToLoadDuringTests);
}
return parameters;
}

@TestVisible
private static void setMockParameter(LoggerParameter__mdt parameter) {
mockParameterByDeveloperName.put(parameter.DeveloperName, parameter);
private static void setMock(LoggerParameter__mdt parameter) {
if (String.isBlank(parameter.DeveloperName) == true) {
throw new IllegalArgumentException('DeveloperName is required on `LoggerParameter__mdt: \n' + JSON.serializePretty(parameter));
}
DEVELOPER_NAME_TO_RECORD.put(parameter.DeveloperName, parameter);
}

private static Object castParameterValue(String parameterDeveloperName, Type dataType) {
Expand All @@ -356,16 +391,6 @@ public class LoggerParameter {
}

private static String loadParameterValue(String parameterDeveloperName) {
String parameterValue = LoggerParameter__mdt.getInstance(parameterDeveloperName)?.Value__c;

Boolean useMockParameter =
parametersToLoadDuringTests.contains(parameterDeveloperName) == false ||
mockParameterByDeveloperName.containsKey(parameterDeveloperName) == true;
if (System.Test.isRunningTest() == true && useMockParameter == true) {
// During tests, don't actually use the org's CMDT records - only use mock records
parameterValue = (String) mockParameterByDeveloperName.get(parameterDeveloperName)?.get('Value__c');
}

return parameterValue;
return DEVELOPER_NAME_TO_RECORD.get(parameterDeveloperName)?.Value__c;
}
}
139 changes: 139 additions & 0 deletions nebula-logger/core/main/configuration/classes/LoggerPlugin.cls
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//------------------------------------------------------------------------------------------------//
// 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 Configuration
* @description The core of the plugin framework, used to create custom Apex & Flow plugins for `LoggerSObjectHandler` and `LogBatchPurger`
* based on configurations stored in the custom metadata type `LoggerPlugin__mdt`
*/
public without sharing class LoggerPlugin {
private static final Map<String, LoggerPlugin__mdt> DEVELOPER_NAME_TO_RECORD = loadRecords();

/**
* @description Interface used to create plugins that can be used within Logger's batch job `LogBatchPurger`
*/
@SuppressWarnings('PMD.ApexDoc')
public interface Batchable {
void start(LoggerPlugin__mdt configuration, LoggerBatchableContext input);
void execute(LoggerPlugin__mdt configuration, LoggerBatchableContext input, List<SObject> scopeRecords);
void finish(LoggerPlugin__mdt configuration, LoggerBatchableContext input);
}

/**
* @description Interface used to create plugins that can be used within Logger's trigger handler framework `LoggerSObjectHandler`
*/
@SuppressWarnings('PMD.ApexDoc')
public interface Triggerable {
void execute(LoggerPlugin__mdt configuration, LoggerTriggerableContext input);
}

/**
* @description Filters the configured `LoggerPlugin__mdt` records based on a list of `SObjectField` - only records that have a value for 1 or more
* of the specified `populatedFilterFields` will be returned, sorted by the specified `SObjectField` parameter `sortByField`
* @param populatedFilterFields The list of `SObjectField` to check on each `LoggerPlugin__mdt` record - filtering logic checks for a non-null value
* @param sortByField The `SObjectField` to use to sort the list of matches. The method also uses `DeveloperName` as a secondary field for sorting.
* @return The list of matching `LoggerPlugin__mdt` records
*/
public static List<LoggerPlugin__mdt> getFilteredPluginConfigurations(List<Schema.SObjectField> populatedFilterFields, Schema.SObjectField sortByField) {
List<PluginConfigurationSorter> matchingPluginConfigurationSorters = new List<PluginConfigurationSorter>();
for (LoggerPlugin__mdt pluginConfiguration : DEVELOPER_NAME_TO_RECORD.values()) {
Boolean matchesFilterFields = false;
for (Schema.SObjectField filterField : populatedFilterFields) {
if (pluginConfiguration.get(filterField) != null) {
matchesFilterFields = true;
break;
}
}
if (matchesFilterFields == true) {
matchingPluginConfigurationSorters.add(
new PluginConfigurationSorter(pluginConfiguration).sortBy(sortByField).sortBy(Schema.LoggerPlugin__mdt.DeveloperName)
);
}
}
matchingPluginConfigurationSorters.sort();
List<LoggerPlugin__mdt> matchingPluginConfigurations = new List<LoggerPlugin__mdt>();
for (PluginConfigurationSorter sorter : matchingPluginConfigurationSorters) {
matchingPluginConfigurations.add(sorter.pluginConfiguration);
}
return matchingPluginConfigurations;
}

/**
* @description Creates an instance of the class `LoggerPlugin.Batchable` based on the provided `LoggerPlugin__mdt` configuration
* @param pluginConfiguration The instance of `LoggerPlugin__mdt` to use to dynamically create an instance of `LoggerPlugin.Batchable`
* @return The dynamically created instance of `LoggerPlugin.Batchable`,
* or null if an instance could not be created based on the provided configuration
*/
public static Batchable newBatchableInstance(LoggerPlugin__mdt pluginConfiguration) {
return (Batchable) Type.forName(pluginConfiguration?.BatchPurgerApexClass__c)?.newInstance();
}

/**
* @description Creates an instance of the class `LoggerPlugin.Triggerable` based on the provided `LoggerPlugin__mdt` configuration
* @param pluginConfiguration The instance of `LoggerPlugin__mdt` to use to dynamically create an instance of `LoggerPlugin.Triggerable`
* @return The dynamically created instance of `LoggerPlugin.Triggerable`,
* or null if an instance could not be created based on the provided configuration
*/
public static Triggerable newTriggerableInstance(LoggerPlugin__mdt pluginConfiguration) {
return (Triggerable) Type.forName(pluginConfiguration?.SObjectHandlerApexClass__c)?.newInstance();
}

private static Map<String, LoggerPlugin__mdt> loadRecords() {
Map<String, LoggerPlugin__mdt> pluginConfigurations = LoggerPlugin__mdt.getAll().clone();
if (System.Test.isRunningTest() == true) {
pluginConfigurations.clear();
}
return pluginConfigurations;
}

@TestVisible
private static void setMock(LoggerPlugin__mdt pluginConfiguration) {
if (String.isBlank(pluginConfiguration.DeveloperName) == true) {
throw new IllegalArgumentException('DeveloperName is required on mock LoggerPlugin__mdt: \n' + JSON.serializePretty(pluginConfiguration));
}
if (pluginConfiguration.IsEnabled__c == true) {
DEVELOPER_NAME_TO_RECORD.put(pluginConfiguration.DeveloperName, pluginConfiguration);
}
}

@SuppressWarnings('PMD.ApexDoc')
private class PluginConfigurationSorter implements Comparable {
public LoggerPlugin__mdt pluginConfiguration;
private List<Schema.SObjectField> sortByFields = new List<Schema.SObjectField>();

public PluginConfigurationSorter(LoggerPlugin__mdt pluginConfiguration) {
this.pluginConfiguration = pluginConfiguration;
}

public PluginConfigurationSorter sortBy(Schema.SObjectField field) {
sortByFields.add(field);
return this;
}

public Integer compareTo(Object compareTo) {
PluginConfigurationSorter that = (PluginConfigurationSorter) compareTo;

for (Schema.SObjectField field : this.sortByFields) {
// Ugly block to handle numeric comparisons vs string comparisons (based on the field type)
Boolean thisIsGreaterThanThat = false;
if (field.getDescribe().getSoapType() == Schema.SoapType.DOUBLE) {
thisIsGreaterThanThat = (Decimal) this.pluginConfiguration.get(field) > (Decimal) that.pluginConfiguration.get(field);
} else {
thisIsGreaterThanThat = (String) this.pluginConfiguration.get(field) > (String) that.pluginConfiguration.get(field);
}

// Now, the actual comparisons
if (this.pluginConfiguration.get(field) == that.pluginConfiguration.get(field)) {
continue;
} else if (this.pluginConfiguration.get(field) == null && that.pluginConfiguration.get(field) != null || thisIsGreaterThanThat == true) {
return 1;
} else {
return -1;
}
}
return 0;
}
}
}
Loading

0 comments on commit d25e766

Please sign in to comment.