diff --git a/.forceignore b/.forceignore
index efc018d11..04d1f4c5e 100644
--- a/.forceignore
+++ b/.forceignore
@@ -2,6 +2,11 @@
# More information: https://developer.salesforce.com/docs/atlas.en-us.sfdx_dev.meta/sfdx_dev/sfdx_dev_exclude_source.htm
#
+nebula-logger/main/default/**
+
+# Directory/package-specific README files
+**/README.md
+
# LWC configuration files
**/jsconfig.json
**/.eslintrc.json
diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index ca35a4219..1434f46bb 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -16,6 +16,7 @@ on:
- 'LICENSE'
- 'package.json'
- 'README.md'
+ - './**/README.md'
- 'sfdx-project.json'
jobs:
diff --git a/.gitignore b/.gitignore
index 619c37538..7fa3b5714 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
.sfdx/
.vscode/
coverage/
+force-app/
nebula-logger/main/default/
nebula-logger/managed-package/
.vim-force.com
diff --git a/README.md b/README.md
index 680a6e302..826656390 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@
Designed for Salesforce admins, developers & architects. A robust logger for Apex, Flow, Process Builder & Integrations.
-[![Install Unlocked Package](./content/btn-install-unlocked-package.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000027FJdQAM)
-[![Install Managed Package](./content/btn-install-managed-package.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000027FFgQAM)
+[![Install Unlocked Package](./content/btn-install-unlocked-package.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000027FMrQAM)
+[![Install Managed Package](./content/btn-install-managed-package.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5Y0000027FMhQAM)
[![Deploy Unpackaged Metadata](./content/btn-deploy-unpackaged-metadata.png)](https://githubsfdeploy.herokuapp.com/?owner=jongpie&repo=NebulaLogger&ref=main)
[![View Documentation](./content/btn-view-documentation.png)](https://jongpie.github.io/NebulaLogger/)
@@ -18,9 +18,10 @@ Designed for Salesforce admins, developers & architects. A robust logger for Ape
2. Manage & report on logging data using the `Log__c` and `LogEntry__c` objects
3. Leverage `LogEntryEvent__e` platform events for real-time monitoring & integrations
4. Enable logging and set the logging level for different users & profiles using `LoggerSettings__c` custom hierarchy setting
-5. View related log entries on any record page by adding the 'Related Log Entries' component in App Builder
+5. View related log entries on any Lighting SObject flexipage by adding the 'Related Log Entries' component in App Builder
6. Dynamically assign Topics to `Log__c` and `LogEntry__c` records for tagging/labeling your logs (not currently available in the managed package)
-7. Event-Driven Integrations with [Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm), an event-driven messaging architecture. External integrations can subscribe to log events using the `LogEntryEvent__e` object - see more details at [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm)
+7. Plugin framework: easily build or install plugins that enhance the `Log__c` and `LogEntry__c` objects, using Apex or Flow
+8. Event-Driven Integrations with [Platform Events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm), an event-driven messaging architecture. External integrations can subscribe to log events using the `LogEntryEvent__e` object - see more details at [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm)
Learn more about the design and history of the project on [Joys Of Apex blog post](https://www.joysofapex.com/advanced-logging-using-nebula-logger/)
@@ -54,15 +55,15 @@ You can choose to install the unlocked package, you can deploy the metadata from
Future Releases
-
Faster release cycle: new patch versions are released (e.g., `v4.4.x`) for new enhancements & bugfixes that are merged to the `main` branch in GitHub
-
Slower release cycle: new minor versions are only released (e.g., `v4.x`) once new enhancements & bugfixes have been tested and code is stabilized
+
Faster release cycle: new patch versions are released (e.g., v4.4.x) for new enhancements & bugfixes that are merged to the main branch in GitHub
+
Slower release cycle: new minor versions are only released (e.g., v4.x) once new enhancements & bugfixes have been tested and code is stabilized
Faster release cycle: new enhancements & bugfixes will be immediately available in GitHub
-
Public Apex Methods
-
Any public Apex methods are subject to change in the future - they can be used, but you may encounter deployment issues if future changes to public methods are not backwards-compatible
+
Public & Protected Apex Methods
+
Any public and protected Apex methods are subject to change in the future - they can be used, but you may encounter deployment issues if future changes to public and protected methods are not backwards-compatible
Only global methods are available in managed packages - any global Apex methods available in the managed package will be supported for the foreseeable future
-
Any public Apex methods are subject to change in the future - they can be used, but you may encounter deployment issues if future changes to public methods are not backwards-compatible
+
Any public and protected Apex methods are subject to change in the future - they can be used, but you may encounter deployment issues if future changes to public and protected methods are not backwards-compatible
Apex Debug Statements
@@ -82,6 +83,12 @@ You can choose to install the unlocked package, you can deploy the metadata from
This functionality is not currently available in the managed package
Provide List<String> topics in Apex or Flow to dynamically assign Salesforce Topics to Log__c and LogEntry__c records
+
+
Logger Plugin Framework
+
Leverage Apex or Flow to build your own "plugins" for Logger - to add your own automation to the Log__c or LogEntry__c objects. The logger system will then automatically run your plugins after each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on).
+
This functionality is not currently available in the managed package
+
Leverage Apex or Flow to build your own "plugins" for Logger - to add your own automation to the Log__c or LogEntry__c objects. The logger system will then automatically run your plugins after each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on).
+
@@ -104,7 +111,7 @@ After deploying Nebula Logger to your org, there are a few additional configurat
---
-## Logger for Apex: Quick Start
+### Logger for Apex: Quick Start
For Apex developers, the `Logger` class has several methods that can be used to add entries with different logging levels. Each logging level's method has several overloads to support multiple parameters.
@@ -129,7 +136,7 @@ This results in 1 `Log__c` record with several related `LogEntry__c` records.
---
-## Logger for Flow & Process Builder: Quick Start
+### Logger for Flow & Process Builder: Quick Start
Within Flow & Process Builder, you can select 1 of the several Logging actions
@@ -145,7 +152,7 @@ This results in a `Log__c` record with related `LogEntry__c` records.
---
-## All Together: Apex & Flow in One Log
+### All Together: Apex & Flow in One Log
After incorporating Logger into your Flows & Apex code (including controllers, trigger framework, etc.), you'll have a unified transaction log of all your declarative & custom code automations.
@@ -339,7 +346,17 @@ The class `LogMessage` provides the ability to generate string messages on deman
For more details, check out the `LogMessage` class [documentation](https://jongpie.github.io/NebulaLogger/logger-engine/LogMessage).
-## Managing Logs
+---
+
+## Log Management
+
+### Logger Console App
+
+The Logger Console app provides access to `Log__c` and `LogEntry__c` object tabs (for any users with the correct access).
+
+![Logger Console app](./content/logger-console-app.png)
+
+### Log's 'Manage' Quick Action
To help development and support teams better manage logs (and any underlying code or config issues), some fields on `Log__c` are provided to track the owner, priority and status of a log. These fields are optional, but are helpful in critical environments (production, QA sandboxes, UAT sandboxes, etc.) for monitoring ongoing user activities.
@@ -356,7 +373,7 @@ To help development and support teams better manage logs (and any underlying cod
---
-## View Related Log Entries on a Record Page
+### View Related Log Entries on a Record Page
Within App Builder, admins can add the 'Related Log Entries' lightning web component to any record page. Admins can also control which columns are displayed be creating & selecting a field set on `LogEntry__c` with the desired fields.
@@ -371,15 +388,15 @@ Within App Builder, admins can add the 'Related Log Entries' lightning web compo
---
-## Deleting Old Logs
+### Deleting Old Logs
Admins can easily delete old logs using 2 methods: list views or Apex batch jobs
-### Mass Deleting with List Views
+#### Mass Deleting with List Views
Salesforce (still) does not support mass deleting records out-of-the-box. There's been [an Idea for 11+ years](https://trailblazer.salesforce.com/ideaView?id=08730000000BqczAAC) about it, but it's still not standard functionality. A custom button is available on `Log__c` list views to provide mass deletion functionality.
-1. Users can select 1 or more `Log__c` records from the list view to choose which logs will be deleted
+1. Admins can select 1 or more `Log__c` records from the list view to choose which logs will be deleted
![Mass Delete Selection](./content/log-mass-delete-selection.png)
@@ -387,17 +404,75 @@ Salesforce (still) does not support mass deleting records out-of-the-box. There'
![Mass Delete Confirmation](./content/log-mass-delete-confirmation.png)
-### Batch Deleting with Apex Jobs
+#### Batch Deleting with Apex Jobs
Two Apex classes are provided out-of-the-box to handle automatically deleting old logs
1. `LogBatchPurger` - this batch Apex class will delete any `Log__c` records with `Log__c.LogRetentionDate__c <= System.today()`.
- By default, this field is populated with "TODAY + 14 DAYS" - the number of days to retain a log can be customized in `LoggerSettings__c`.
- - Users can also manually edit this field to change the retention date - or set it to null to prevent the log from being automatically deleted
+ - Admins can also manually edit this field to change the retention date - or set it to null to prevent the log from being automatically deleted
2. `LogBatchPurgeScheduler` - this schedulable Apex class can be schedule to run `LogBatchPurger` on a daily or weekly basis
---
+## Beta Feature: Custom Plugin Framework for Log\_\_c and LogEntry\_\_c objects
+
+If you want to add your own automation to the `Log__c` or `LogEntry__c` objects, you can leverage Apex or Flow to define "plugins" - the logger system will then automatically run the plugins after each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on). This framework makes it easy to build your own plugins, or deploy/install others' prebuilt packages, without having to modify the logging system directly.
+
+- Flow plugins: your Flow should be built as auto-launched Flows with these parameters:
+
+ 1. `Input` parameter `triggerOperationType` - The name of the current trigger operation (such as BEFORE_INSERT, BEFORE_UPDATE, etc.)
+ 2. `Input` parameter `triggerNew` - The list of logger records being processed (`Log__c` or `LogEntry__c` records)
+ 3. `Output` parameter `updatedTriggerNew` - If your Flow makes any updates to the collection of records, you should return a record collection containing the updated records
+ 4. `Input` parameter `triggerOld` - The list of logger records as they exist in the datatabase
+
+- Apex plugins: your Apex class should extend the abstract class `LoggerSObjectHandlerPlugin`. For example:
+
+ ```java
+ public class ExamplePlugin extends LoggerSObjectHandlerPlugin {
+ public override void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ ) {
+ switch on triggerOperationType {
+ when BEFORE_INSERT {
+ for (Log__c log : (List) triggerNew) {
+ log.Status__c = 'On Hold';
+ }
+ }
+ }
+ }
+ }
+
+ ```
+
+Once you've created your Apex or Flow plugin(s), you will also need to configure the plugin:
+
+- 'Logger Plugin' - use the custom metadata type `LoggerSObjectHandlerPlugin__mdt` to define your plugin, including the plugin type (Apex or Flow) and the API name of your plugin's Apex class or Flow
+- 'Logger Plugin Parameter' - use the custom metadata type `LoggerSObjectHandlerPluginParameter__mdt` to define any configurable parameters needed for your plugin, such as environment-specific URLs and other similar configurations
+
+![Logger plugin: configuration](./content/slack-plugin-configuration.png)
+
+Note: the logger plugin framework is not available in the managed package due to some platform limitations & considerations with some of the underlying code. The unlocked package is recommended (instead of the managed package) when possible, including if you want to be able to leverage the plugin framework in your org.
+
+### Beta Plugin: Slack Integration
+
+The optional [Slack plugin](./nebula-logger-plugins/Slack/) leverages the Nebula Logger plugin framework to automatically send Slack notifications for logs that meet a certain (configurable) logging level. The plugin also serves as a functioning example of how to build your own plugin for Nebula Logger, such as how to:
+
+- Use Apex to apply custom logic to `Log__c` and `LogEntry__c` records
+- Add custom fields and list views to Logger's objects
+- Extend permission sets to include field-level security for your custom fields
+- Leverage the new `LoggerSObjectHandlerPluginParameter__mdt` CMDT object to store configuration for your plugin
+
+Check out the [Slack plugin](./nebula-logger-plugins/Slack/) for more details on how to install & customize the plugin
+
+![Slack plugin: notification](./content/slack-plugin-notification.png)
+
+---
+
## Uninstalling/Removing Logger
If you want to remove the unlocked or managed packages, you can do so by simply uninstalling them in your org under Setup --> Installed Packages.
diff --git a/content/btn-view-documentation.png b/content/btn-view-documentation.png
index 4a45d2695..475dd95a2 100644
Binary files a/content/btn-view-documentation.png and b/content/btn-view-documentation.png differ
diff --git a/content/logger-console-app.png b/content/logger-console-app.png
new file mode 100644
index 000000000..b060b1e62
Binary files /dev/null and b/content/logger-console-app.png differ
diff --git a/content/slack-plugin-configuration.png b/content/slack-plugin-configuration.png
new file mode 100644
index 000000000..0149f7dba
Binary files /dev/null and b/content/slack-plugin-configuration.png differ
diff --git a/content/slack-plugin-notification.png b/content/slack-plugin-notification.png
new file mode 100644
index 000000000..ed78e33f0
Binary files /dev/null and b/content/slack-plugin-notification.png differ
diff --git a/docs/index.md b/docs/index.md
index c8cad3759..0309ae72e 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -56,6 +56,16 @@ Manages setting fields on `Log__c` before insert & before update
Manages mass deleting `Log__c` records that have been selected by a user on a `Log__c` list view
+### [LoggerSObjectHandler](log-management/LoggerSObjectHandler)
+
+Abstract class used by trigger handlers for shared logic
+
### [RelatedLogEntriesController](log-management/RelatedLogEntriesController)
Controller class for the component RelatedLogEntries
+
+## Plugin Framework
+
+### [LoggerSObjectHandlerPlugin](plugin-framework/LoggerSObjectHandlerPlugin)
+
+Abstract class used to create custom Apex plugins to execute for all trigger operations on `Log__c` or `LogEntry__c`
diff --git a/docs/log-management/LogEntryEventHandler.md b/docs/log-management/LogEntryEventHandler.md
index c6c1e9b94..712e69efd 100644
--- a/docs/log-management/LogEntryEventHandler.md
+++ b/docs/log-management/LogEntryEventHandler.md
@@ -30,4 +30,18 @@ Processes `LogEntryEvent__e` platform events and normalizes the data into `Log__
Runs the trigger handler's logic for the `LogEntryEvent__e` platform event object
+#### `getSObjectType()` → `SObjectType`
+
+Returns SObject Type that the handler is responsible for processing
+
+##### Return
+
+**Type**
+
+SObjectType
+
+**Description**
+
+The instance of `SObjectType`
+
---
diff --git a/docs/log-management/LogEntryHandler.md b/docs/log-management/LogEntryHandler.md
index 6836eacf2..78ea3a67a 100644
--- a/docs/log-management/LogEntryHandler.md
+++ b/docs/log-management/LogEntryHandler.md
@@ -20,4 +20,18 @@ Manages setting fields on `LogEntry__c` before insert & before update
Runs the trigger handler's logic for the `LogEntry__c` custom object
+#### `getSObjectType()` → `SObjectType`
+
+Returns SObject Type that the handler is responsible for processing
+
+##### Return
+
+**Type**
+
+SObjectType
+
+**Description**
+
+The instance of `SObjectType`
+
---
diff --git a/docs/log-management/LogHandler.md b/docs/log-management/LogHandler.md
index e0d45e20b..ae7b6d6a2 100644
--- a/docs/log-management/LogHandler.md
+++ b/docs/log-management/LogHandler.md
@@ -20,4 +20,18 @@ Manages setting fields on `Log__c` before insert & before update
Runs the trigger handler's logic for the `Log__c` custom object
+#### `getSObjectType()` → `SObjectType`
+
+Returns SObject Type that the handler is responsible for processing
+
+##### Return
+
+**Type**
+
+SObjectType
+
+**Description**
+
+The instance of `SObjectType`
+
---
diff --git a/docs/log-management/LoggerSObjectHandler.md b/docs/log-management/LoggerSObjectHandler.md
new file mode 100644
index 000000000..b606199ff
--- /dev/null
+++ b/docs/log-management/LoggerSObjectHandler.md
@@ -0,0 +1,37 @@
+---
+layout: default
+---
+
+## LoggerSObjectHandler class
+
+Abstract class used by trigger handlers for shared logic
+
+---
+
+### Constructors
+
+#### `LoggerSObjectHandler()`
+
+---
+
+### Methods
+
+#### `execute()` → `void`
+
+Runs the handler class's logic
+
+#### `getSObjectType()` → `SObjectType`
+
+Returns the SObject Type that the handler is responsible for processing
+
+##### Return
+
+**Type**
+
+SObjectType
+
+**Description**
+
+The instance of `SObjectType`
+
+---
diff --git a/docs/plugin-framework/LoggerSObjectHandlerPlugin.md b/docs/plugin-framework/LoggerSObjectHandlerPlugin.md
new file mode 100644
index 000000000..31234a243
--- /dev/null
+++ b/docs/plugin-framework/LoggerSObjectHandlerPlugin.md
@@ -0,0 +1,33 @@
+---
+layout: default
+---
+
+## LoggerSObjectHandlerPlugin class
+
+Abstract class used to create custom Apex plugins to execute for all trigger operations on `Log__c` or `LogEntry__c`
+
+---
+
+### Constructors
+
+#### `LoggerSObjectHandlerPlugin()`
+
+## All instances of `LoggerSObjectHandlerPlugin` are dynamically created, which requires aparameterless constructor
+
+### Methods
+
+#### `execute(TriggerOperation triggerOperationType,List triggerNew,Map triggerNewMap,List triggerOld,Map triggerOldMap)` → `void`
+
+This method is the entry point for plugins to execute any custom logic. It is automatically called by the logging system for any enabled plugins. Several trigger-based parameters are provided - these parameters should be used by plugins, instead of calling the platform's static variables directly (e.g., use the provided `triggerNew` variable instead of using `Trigger.new` directly, and so on).
+
+##### Parameters
+
+| Param | Description |
+| ---------------------- | ------------------------------------------------------------------------------------------ |
+| `triggerOperationType` | The enum instance of `Trigger.operationType` at the time that the handler class is created |
+| `triggerNew` | The value `Trigger.new` at the time that the handler class is created |
+| `triggerNewMap` | The value `Trigger.newMap` at the time that the handler class is created |
+| `triggerOld` | The value `Trigger.old` at the time that the handler class is created |
+| `triggerOldMap` | The value `Trigger.oldMap` at the time that the handler class is created |
+
+---
diff --git a/extra-tests/LogHandler_Tests_Flow.cls b/extra-tests/LogHandler_Tests_Flow.cls
new file mode 100644
index 000000000..c72161203
--- /dev/null
+++ b/extra-tests/LogHandler_Tests_Flow.cls
@@ -0,0 +1,36 @@
+//------------------------------------------------------------------------------------------------//
+// 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. //
+//------------------------------------------------------------------------------------------------//
+
+@isTest
+private class LogHandler_Tests_Flow {
+ @isTest
+ static void it_should_run_flow_plugin_when_configured() {
+ // Assumption: the Flow LogHandler_Tests_Flow makes an update to the current user's FirstName
+ // The specific action within the Flow isn't that important - we just want to make sure...
+ // ...that that Flow is dynamically executed
+ String pluginFlowApiName = 'LogHandler_Tests_Flow';
+ String expectedUserFirstName = 'Logger-Flow-Test';
+ System.assertNotEquals(expectedUserFirstName, UserInfo.getFirstName());
+
+ LoggerSObjectHandlerPlugin__mdt plugin = new LoggerSObjectHandlerPlugin__mdt(PluginType__c = 'Flow', PluginApiName__c = pluginFlowApiName);
+ Map> pluginsBySObjectType = new Map>{
+ Schema.Log__c.SObjectType => new List{ plugin }
+ };
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.pluginsBySObjectType = pluginsBySObjectType;
+
+ Log__c log = new Log__c(TransactionId__c = '1234');
+ insert log;
+
+ Test.stopTest();
+
+ // Verify that the Flow ran by checking if the user's FirstName was updated
+ User currentUser = [SELECT Id, FirstName FROM User WHERE Id = :UserInfo.getUserId()];
+ System.assertEquals(expectedUserFirstName, currentUser.FirstName);
+ }
+}
diff --git a/extra-tests/LogHandler_Tests_Flow.cls-meta.xml b/extra-tests/LogHandler_Tests_Flow.cls-meta.xml
new file mode 100644
index 000000000..482559c8b
--- /dev/null
+++ b/extra-tests/LogHandler_Tests_Flow.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/extra-tests/extra-metadata/flows/LogHandler_Tests_Flow.flow-meta.xml b/extra-tests/extra-metadata/flows/LogHandler_Tests_Flow.flow-meta.xml
new file mode 100644
index 000000000..263580de4
--- /dev/null
+++ b/extra-tests/extra-metadata/flows/LogHandler_Tests_Flow.flow-meta.xml
@@ -0,0 +1,112 @@
+
+
+ 51.0
+
+ Update_user_s_first_name
+
+ 176
+ 278
+
+ Get_current_user.FirstName
+ Assign
+
+ Logger-Flow-Test
+
+
+
+ Save_current_user
+
+
+ An additional Flow used test dynamically running Flow plugins from LoggerSObjectHandler
+ LogHandler_Tests_Flow {!$Flow.CurrentDateTime}
+
+
+ BuilderType
+
+ LightningFlowBuilder
+
+
+
+ CanvasMode
+
+ AUTO_LAYOUT_CANVAS
+
+
+
+ OriginBuilderType
+
+ LightningFlowBuilder
+
+
+ AutoLaunchedFlow
+
+ Get_current_user
+
+ 176
+ 158
+ false
+
+ Update_user_s_first_name
+
+ and
+
+ Id
+ EqualTo
+
+ $User.Id
+
+
+ true
+
+ true
+
+
+ Save_current_user
+
+ 176
+ 398
+ Get_current_user
+
+
+ 50
+ 0
+
+ Get_current_user
+
+
+ Active
+
+ triggerOld
+ SObject
+ true
+ true
+ false
+ Log__c
+
+
+ Used by test class to verify that the Flow ran successfully
+ ranSuccessfully
+ Boolean
+ false
+ false
+ true
+
+ false
+
+
+
+ records
+ SObject
+ true
+ true
+ false
+ Log__c
+
+
+ triggerOperationType
+ String
+ false
+ true
+ false
+
+
diff --git a/force-app/README.md b/force-app/README.md
new file mode 100644
index 000000000..9a3cf127d
--- /dev/null
+++ b/force-app/README.md
@@ -0,0 +1,4 @@
+This folder is used as the default folder by SFDX specifically so that no unwanted metadata accidentally gets added to the Nebula Logger packages.
+
+- Any metadata in this folder (`./force-app/`) can be deployed to your scratch org, but all files are ignored in `.gitignore`
+- Any metadata that needs to be added to the unlocked package should be moved to the folder `./nebula-logger/`
diff --git a/nebula-logger-plugins/Slack/README.md b/nebula-logger-plugins/Slack/README.md
new file mode 100644
index 000000000..cca1c8584
--- /dev/null
+++ b/nebula-logger-plugins/Slack/README.md
@@ -0,0 +1,49 @@
+# Slack plugin for Nebula Logger
+
+Adds a Slack integration for Nebula Logger. Any logs with log entries that meet a certain (configurable) logging level will automatically be posted to your Slack channel via an asynchronous `Queueable` job.
+
+[![Install Unlocked Package](./../../content/btn-install-unlocked-package.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04t5e00000061lHAAQ)
+
+![Slack plugin: notification](./../../content/slack-plugin-notification.png)
+
+---
+
+## What's Included
+
+This plugin includes some add-on metadata for Logger to support the Slack integration
+
+1. Apex class `SlackLoggerPlugin` and corresponding tests in `SlackLoggerPlugin_Tests`
+2. Plugin configuration details stored in Logger's CMDT objects `LoggerSObjectHandlerPlugin__mdt` and `LoggerSObjectHandlerPluginParameter__mdt`
+3. Custom fields `Log__c.SendSlackNotification__c` and `Log__c.SlackNotificationDate__c`
+4. Field-level security (FLS) via a new permission set `LoggerSlackPluginAdmin` to provide access to the custom Slack fields
+5. Custom list views for the `Log__c` and `LoggerSObjectHandlerPluginParameter__mdt` objects
+6. Remote site setting for Slack's API
+
+---
+
+## Installation Steps
+
+In order to use the Slack plugin, there are some configuration changes needed in both Slack and Salesforce
+
+### Slack setup
+
+Within Slack, you'll need to setup incoming webhooks to allow the Logger Slack plugin to create messages. The high-level steps are:
+
+1. Create a new app in your Slack workspace (or use an existing app, if you prefer)
+2. Create a new incoming webhook for your app, and copy the webhook URL. This will be used in Salesforce (see below steps)
+
+Check out [Slack's webhooks documentation](https://api.slack.com/messaging/webhooks) for more details on how to setup incoming webhooks.
+
+### Salesforce setup
+
+1. Ensure that you have the unlocked package version of Nebula Logger installed in your org
+2. Install the unlocked package for the Slack plugin
+3. Go to Setup --> Custom Metadata Types --> Logger Plugin --> Slack. There are 2 parameters to configure (shown in screenshot below)
+ - Parameter 'Slack Endpoint' - You can configure this webhook in 1 of 2 ways:
+ - Easier but less secure: Paste the Slack webhook URL into the `Value__c` field and save the Plugin Parameter record.
+ - More secure: Create a new Named Credential, using the webhook URL as the endpoint. Within the Parameter 'Slack Endpoint', enter `callout:` into the `Value__c` field and save the Plugin Parameter record
+ - Parameter 'Slack Notification Logging Level' - Set the desired logging level value that should trigger a Slack notification to be sent the Logger Plugin Parameter 'Slack Notification Logging Level`. It controls which logging level (ERROR, WARN, INFO, DEBUG, FINE, FINER, or FINEST) will trigger the Slack notifications to be sent.
+
+The Slack integration should now be setup & working - any new logs that meet the specified notification logging level (step 6 above) will send a Slack notification.
+
+![Slack plugin: configuration](./../../content/slack-plugin-configuration.png)
diff --git a/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls
new file mode 100644
index 000000000..47b86c62c
--- /dev/null
+++ b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls
@@ -0,0 +1,322 @@
+//------------------------------------------------------------------------------------------------//
+// 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 Plugins
+ * @description Optional plugin that integrates with Slack to send alerts for important logs
+ */
+public without sharing class SlackLoggerPlugin extends LoggerSObjectHandlerPlugin implements Queueable, Database.AllowsCallouts {
+ // Constants used for loading parameters from CMDT objects
+ private static final String PARAM_ENDPOINT = 'SlackEndpoint';
+ private static final String PARAM_NOTIFICATION_LOGGING_LEVEL_NAME = 'SlackNotificationLoggingLevel';
+
+ @testVisible
+ private static String endpoint;
+ @testVisible
+ private static LoggingLevel notificationLoggingLevel;
+
+ static {
+ endpoint = LoggerSObjectHandlerPluginParameter__mdt.getInstance(PARAM_ENDPOINT)?.Value__c;
+
+ LoggerSObjectHandlerPluginParameter__mdt loggingLevelParameter = LoggerSObjectHandlerPluginParameter__mdt.getInstance(
+ PARAM_NOTIFICATION_LOGGING_LEVEL_NAME
+ );
+ if (loggingLevelParameter != null && loggingLevelParameter.IsEnabled__c == true) {
+ String notificationLoggingLevelName = loggingLevelParameter?.Value__c;
+ notificationLoggingLevel = Logger.getLoggingLevel(notificationLoggingLevelName);
+ }
+ }
+
+ private List logs;
+
+ // Constructors
+ public SlackLoggerPlugin() {
+ }
+
+ private SlackLoggerPlugin(List unsentLogs) {
+ this();
+ this.logs = unsentLogs;
+ }
+
+ // Instance methods
+ public override void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ ) {
+ this.logs = (List) triggerNew;
+
+ switch on triggerOperationType {
+ when BEFORE_INSERT, BEFORE_UPDATE {
+ this.flagLogsForSlackNotification();
+ }
+ when AFTER_INSERT, AFTER_UPDATE {
+ this.sendAsyncSlackNotifications();
+ }
+ }
+ }
+
+ public void execute(System.QueueableContext queueableContext) {
+ // SInce this runs in an async context, requery the logs just in case any field values have changed
+ this.requeryLogs();
+
+ if (this.logs.isEmpty() == true) {
+ return;
+ }
+
+ List sentLogs = new List();
+ List unsentLogs = new List();
+ for (Log__c log : this.logs) {
+ if (Limits.getCallouts() == Limits.getLimitCallouts()) {
+ // If there are too many logs to send in the same transaction...
+ // ...add them to the unsentLogs list, which will be queued as a separate job
+ unsentLogs.add(log);
+ } else {
+ HttpRequest request = this.createSlackHttpRequest();
+
+ NotificationDto notification = new NotificationDto();
+ notification.text = 'Salesforce Log Alert';
+ notification.attachments = new List{ this.convertLog(log) };
+
+ // 'Short' is a reserved word in Apex, but used in Slack's API, so the conversion happens in JSON
+ String notificationJson = JSON.serialize(notification).replace('"isShort"', '"short"');
+ request.setBody(notificationJson);
+
+ HttpResponse response = new Http().send(request);
+ System.debug('response.getStatusCode()==' + response.getStatusCode());
+ System.debug('response.getStatus()==' + response.getStatus());
+
+ log.SlackNotificationDate__c = System.now();
+ sentLogs.add(log);
+ }
+ }
+ update sentLogs;
+
+ // If any logs couldn't be sent due to governor limits, start a new instance of the job
+ if (unsentLogs.isEmpty() == false) {
+ System.enqueueJob(new SlackLoggerPlugin(unsentLogs));
+ }
+ }
+
+ private void flagLogsForSlackNotification() {
+ if (notificationLoggingLevel == null) {
+ return;
+ }
+
+ for (Log__c log : this.logs) {
+ if (log.MaxLogEntryLoggingLevelOrdinal__c >= notificationLoggingLevel.ordinal()) {
+ log.SendSlackNotification__c = true;
+ }
+ }
+ }
+
+ private void sendAsyncSlackNotifications() {
+ List logsToSend = new List();
+ for (Log__c log : this.logs) {
+ if (log.SendSlackNotification__c == true) {
+ logsToSend.add(log);
+ }
+ }
+
+ // Since plugins are called from trigger handlers, and triggers can't make callouts...
+ // ...run this class as a queueable (async) job
+ if (logsToSend.isEmpty() == false) {
+ System.enqueueJob(new SlackLoggerPlugin(logsToSend));
+ }
+ }
+
+ private void requeryLogs() {
+ // TODO: switch to dynamically querying based on a new `Log__c` field set parameter
+ this.logs = [
+ SELECT
+ Id,
+ Name,
+ ApiVersion__c,
+ LoggedBy__c,
+ LoggedBy__r.Username,
+ OwnerId,
+ TYPEOF Owner
+ WHEN User THEN Username
+ ELSE Name
+ END,
+ MaxLogEntryLoggingLevelOrdinal__c,
+ OrganizationId__c,
+ OrganizationEnvironmentType__c,
+ OrganizationInstanceName__c,
+ OrganizationName__c,
+ Priority__c,
+ StartTime__c,
+ TimeZoneId__c,
+ TotalLogEntries__c,
+ TotalERRORLogEntries__c,
+ TotalWARNLogEntries__c,
+ TransactionId__c,
+ (
+ SELECT Id, LoggingLevel__c, Message__c
+ FROM LogEntries__r
+ WHERE LoggingLevelOrdinal__c >= :notificationLoggingLevel.ordinal()
+ ORDER BY Timestamp__c DESC
+ LIMIT 1
+ )
+ FROM Log__c
+ WHERE
+ Id IN :this.logs
+ AND MaxLogEntryLoggingLevelOrdinal__c >= :notificationLoggingLevel.ordinal()
+ AND SendSlackNotification__c = TRUE
+ AND SlackNotificationDate__c = NULL
+ ];
+ }
+
+ private HttpRequest createSlackHttpRequest() {
+ System.debug('endpoint==' + endpoint);
+
+ HttpRequest request = new HttpRequest();
+ request.setEndpoint(endpoint);
+ request.setMethod('POST');
+ request.setHeader('Content-Type', 'application/json');
+
+ return request;
+ }
+
+ private LogDto convertLog(Log__c log) {
+ LogEntry__c lastLogEntry = log.LogEntries__r.get(0);
+ String messageText = 'Last Log Entry Message' + '\n`' + lastLogEntry.LoggingLevel__c + ': ' + lastLogEntry.Message__c + '`';
+
+ LogDto notification = new LogDto();
+ notification.author_link = Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.LoggedBy__c;
+ notification.author_name = log.LoggedBy__r.Username;
+ notification.color = this.getNotificationColor(log);
+ notification.fields = new List();
+ notification.text = messageText; //Schema.Log__c.TotalLogEntries__c.getDescribe().getLabel() + ': `' + String.valueOf(log.TotalLogEntries__c) + '`';
+ notification.title = log.Name;
+ notification.title_link = Url.getSalesforceBaseUrl().toExternalForm() + '/' + log.Id;
+
+ // TODO: switch to dynamically creating Slack DTO fields based on a new `Log__c` field set parameter
+ FieldDto startTimeField = new FieldDto();
+ startTimeField.isShort = true;
+ startTimeField.title = Schema.Log__c.StartTime__c.getDescribe().getLabel();
+ startTimeField.value = '`' + log.StartTime__c.format() + ' ' + log.TimeZoneId__c + '`';
+ notification.fields.add(startTimeField);
+
+ FieldDto transactionIdField = new FieldDto();
+ transactionIdField.isShort = true;
+ transactionIdField.title = Schema.Log__c.TransactionId__c.getDescribe().getLabel();
+ transactionIdField.value = '`' + log.TransactionId__c + '`';
+ notification.fields.add(transactionIdField);
+
+ FieldDto totalERROREntriesField = new FieldDto();
+ totalERROREntriesField.isShort = true;
+ totalERROREntriesField.title = Schema.Log__c.TotalERRORLogEntries__c.getDescribe().getLabel();
+ totalERROREntriesField.value = '`' + String.valueOf(log.TotalERRORLogEntries__c) + '`';
+ notification.fields.add(totalERROREntriesField);
+
+ FieldDto totalWARNEntriesField = new FieldDto();
+ totalWARNEntriesField.isShort = true;
+ totalWARNEntriesField.title = Schema.Log__c.TotalWARNLogEntries__c.getDescribe().getLabel();
+ totalWARNEntriesField.value = '`' + String.valueOf(log.TotalWARNLogEntries__c) + '`';
+ notification.fields.add(totalWARNEntriesField);
+
+ String logOwnerType = log.OwnerId.getSObjectType().getDescribe().getName();
+ FieldDto logOwnerNameField = new FieldDto();
+ logOwnerNameField.isShort = true;
+ logOwnerNameField.title = 'Log Owner';
+ logOwnerNameField.value = logOwnerType == 'Group' ? '`Queue: ' + log.Owner.Name + '`' : '`User: ' + log.Owner.Username + '`';
+ notification.fields.add(logOwnerNameField);
+
+ FieldDto priorityField = new FieldDto();
+ priorityField.isShort = true;
+ priorityField.title = Schema.Log__c.Priority__c.getDescribe().getLabel();
+ priorityField.value = '`' + log.Priority__c + '`';
+ notification.fields.add(priorityField);
+
+ FieldDto orgIdField = new FieldDto();
+ orgIdField.isShort = true;
+ orgIdField.title = 'Org ID';
+ orgIdField.value = '`' + log.OrganizationId__c + '`';
+ notification.fields.add(orgIdField);
+
+ FieldDto orgNameField = new FieldDto();
+ orgNameField.isShort = true;
+ orgNameField.title = 'Org Name';
+ orgNameField.value = '`' + log.OrganizationName__c + '`';
+ notification.fields.add(orgNameField);
+
+ FieldDto orgTypeField = new FieldDto();
+ orgTypeField.isShort = true;
+ orgTypeField.title = 'Org Type & Instance';
+ orgTypeField.value = '`' + log.OrganizationEnvironmentType__c + ' - ' + log.OrganizationInstanceName__c + '`';
+ notification.fields.add(orgTypeField);
+
+ FieldDto orgApiVersion = new FieldDto();
+ orgApiVersion.isShort = true;
+ orgApiVersion.title = Schema.Log__c.ApiVersion__c.getDescribe().getLabel();
+ orgApiVersion.value = '`' + log.ApiVersion__c + '`';
+ notification.fields.add(orgApiVersion);
+
+ List topicNames = new List();
+ for (TopicAssignment topicAssignment : log.TopicAssignments) {
+ topicNames.add(topicAssignment.Topic.Name);
+ }
+ topicNames.sort();
+
+ if (topicNames.isEmpty() == false) {
+ FieldDto topicsField = new FieldDto();
+ topicsField.isShort = false;
+ topicsField.title = 'Topics';
+ topicsField.value = '`' + String.join(topicNames, '`, `') + '`';
+ notification.fields.add(topicsField);
+ }
+
+ return notification;
+ }
+
+ private String getNotificationColor(Log__c log) {
+ String color;
+
+ if (log.TotalERRORLogEntries__c >= 1) {
+ color = '#FF7373'; // Red
+ } else if (log.TotalWARNLogEntries__c >= 1) {
+ color = '#FFC873'; // Orange
+ } else {
+ color = '#7CD197'; // Green
+ }
+
+ return color;
+ }
+
+ // Private DTO classes that match Slack's API
+ private class NotificationDto {
+ public List attachments;
+ public String text;
+ }
+
+ private class LogDto {
+ public List actions;
+ public String author_name;
+ public String author_link;
+ public String author_icon;
+ public String color;
+ public String fallback;
+ public List fields;
+ public String pretext;
+ public String text;
+ public String title;
+ public String title_link;
+ }
+
+ private class ActionDto {
+ public String text;
+ public String type;
+ public String url;
+ }
+
+ private class FieldDto {
+ public Boolean isShort;
+ public String title;
+ public String value;
+ }
+}
diff --git a/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls-meta.xml b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls-meta.xml
new file mode 100644
index 000000000..d75b0582f
--- /dev/null
+++ b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls
new file mode 100644
index 000000000..5804e3dff
--- /dev/null
+++ b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls
@@ -0,0 +1,168 @@
+//------------------------------------------------------------------------------------------------//
+// 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. //
+//------------------------------------------------------------------------------------------------//
+
+// TODO: need to improve plugin framework to make it easier for plugins to test by making it easier
+// to mock CMDT records & instances of LoggerSObjectHandlerPlugin
+@isTest
+private class SlackLoggerPlugin_Tests {
+ public class SuccessCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest request) {
+ HttpResponse response = new HttpResponse();
+ response.setBody(request.getBody());
+ response.setStatusCode(200);
+ return response;
+ }
+ }
+
+ public class FailureCalloutMock implements HttpCalloutMock {
+ public HttpResponse respond(HttpRequest request) {
+ HttpResponse response = new HttpResponse();
+ response.setBody(request.getBody());
+ response.setStatusCode(400);
+ return response;
+ }
+ }
+
+ static void mockConfigurations(LoggingLevel notificationLoggingLevel) {
+ // Set the plugin's parameters
+ LoggerSObjectHandlerPlugin__mdt slackPluginConfig = new LoggerSObjectHandlerPlugin__mdt(
+ IsEnabled__c = true,
+ PluginApiName__c = SlackLoggerPlugin.class.getName(),
+ PluginType__c = 'Apex'
+ );
+ LoggerSObjectHandler.pluginsBySObjectType = new Map>{
+ Schema.Log__c.SObjectType => new List{ slackPluginConfig }
+ };
+
+ // Set the plugin's internal variables
+ SlackLoggerPlugin.endpoint = 'https://fake.slack.com/';
+ SlackLoggerPlugin.notificationLoggingLevel = notificationLoggingLevel;
+ }
+
+ static void verifyLogEntryCountEquals(Integer expectedCount) {
+ List existingLogEntries = [SELECT Id FROM LogEntry__c];
+ System.assertEquals(expectedCount, existingLogEntries.size());
+ }
+
+ static List queryLogs() {
+ return [
+ SELECT
+ Id,
+ MaxLogEntryLoggingLevelOrdinal__c,
+ SendSlackNotification__c,
+ SlackNotificationDate__c,
+ (
+ SELECT Id, LoggingLevel__c, Message__c
+ FROM LogEntries__r
+ WHERE LoggingLevelOrdinal__c >= :LoggingLevel.WARN.ordinal()
+ ORDER BY Timestamp__c DESC
+ LIMIT 1
+ )
+ FROM Log__c
+ ];
+ }
+
+ @isTest
+ static void it_should_push_a_log_when_logging_level_is_met() {
+ SuccessCalloutMock calloutMock = new SuccessCalloutMock();
+
+ verifyLogEntryCountEquals(0);
+
+ Log__c log = new Log__c(LoggedBy__c = UserInfo.getUserId(), SendSlackNotification__c = false, TransactionId__c = '1234');
+ insert log;
+
+ LoggingLevel logEntryLoggingLevel = LoggingLevel.WARN;
+ LogEntry__c logEntry = new LogEntry__c(
+ Log__c = log.Id,
+ LoggingLevel__c = logEntryLoggingLevel.name(),
+ LoggingLevelOrdinal__c = logEntryLoggingLevel.ordinal(),
+ Timestamp__c = System.now()
+ );
+ insert logEntry;
+
+ verifyLogEntryCountEquals(1);
+
+ List logs = queryLogs();
+ System.assertEquals(1, logs.size());
+ log = logs.get(0);
+
+ System.assertEquals(1, log.LogEntries__r.size());
+ System.assertEquals(false, log.SendSlackNotification__c);
+ System.assertEquals(null, log.SlackNotificationDate__c);
+
+ Test.startTest();
+ Test.setMock(HttpCalloutMock.class, calloutMock);
+
+ // Load the mock configurations - the plugin framework won't load actual CMDT records during tests
+ mockConfigurations(logEntryLoggingLevel);
+ System.assert(logEntryLoggingLevel.ordinal() >= SlackLoggerPlugin.notificationLoggingLevel.ordinal());
+
+ // Update the records to trigger the handler framework, which will then run the Slack plugin
+ update log;
+
+ // Verify that the internal queueable job has been enqueued
+ System.assertEquals(1, Limits.getAsyncCalls());
+
+ // Stop the test so the internal queueable job runs
+ Test.stopTest();
+
+ log = [SELECT Id, MaxLogEntryLoggingLevelOrdinal__c, SendSlackNotification__c, SlackNotificationDate__c FROM Log__c];
+ System.assertEquals(true, log.SendSlackNotification__c);
+ System.assertNotEquals(null, log.SlackNotificationDate__c);
+ System.assertEquals(System.today(), log.SlackNotificationDate__c.date());
+ }
+
+ @isTest
+ static void it_should_not_push_a_log_when_logging_level_is_not_met() {
+ SuccessCalloutMock calloutMock = new SuccessCalloutMock();
+
+ verifyLogEntryCountEquals(0);
+
+ Log__c log = new Log__c(LoggedBy__c = UserInfo.getUserId(), SendSlackNotification__c = false, TransactionId__c = '1234');
+ insert log;
+
+ LoggingLevel logEntryLoggingLevel = LoggingLevel.WARN;
+ LogEntry__c logEntry = new LogEntry__c(
+ Log__c = log.Id,
+ LoggingLevel__c = logEntryLoggingLevel.name(),
+ LoggingLevelOrdinal__c = logEntryLoggingLevel.ordinal(),
+ Timestamp__c = System.now()
+ );
+ insert logEntry;
+
+ verifyLogEntryCountEquals(1);
+
+ List logs = queryLogs();
+ System.assertEquals(1, logs.size());
+ log = logs.get(0);
+
+ System.assertEquals(1, log.LogEntries__r.size());
+ System.assertEquals(false, log.SendSlackNotification__c);
+ System.assertEquals(null, log.SlackNotificationDate__c);
+
+ Test.startTest();
+ Test.setMock(HttpCalloutMock.class, calloutMock);
+
+ // Load the mock configurations - the plugin framework won't load actual CMDT records during tests
+ LoggingLevel slackLoggingLevel = LoggingLevel.ERROR;
+ System.assert(logEntryLoggingLevel.ordinal() < slackLoggingLevel.ordinal());
+ mockConfigurations(slackLoggingLevel);
+ System.assert(logEntryLoggingLevel.ordinal() < SlackLoggerPlugin.notificationLoggingLevel.ordinal());
+
+ // Update the records to trigger the handler framework, which will then run the Slack plugin
+ update log;
+
+ // Verify that the internal queueable job has been enqueued
+ System.assertEquals(0, Limits.getAsyncCalls());
+
+ // Stop the test so the internal queueable job runs
+ Test.stopTest();
+
+ log = queryLogs().get(0);
+ System.assertEquals(1, log.LogEntries__r.size());
+ System.assertEquals(false, log.SendSlackNotification__c);
+ System.assertEquals(null, log.SlackNotificationDate__c);
+ }
+}
diff --git a/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls-meta.xml b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls-meta.xml
new file mode 100644
index 000000000..d75b0582f
--- /dev/null
+++ b/nebula-logger-plugins/Slack/classes/SlackLoggerPlugin_Tests.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPlugin.Slack.md-meta.xml b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPlugin.Slack.md-meta.xml
new file mode 100644
index 000000000..730b51524
--- /dev/null
+++ b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPlugin.Slack.md-meta.xml
@@ -0,0 +1,23 @@
+
+
+
+ false
+
+ Description__c
+ Adds a Slack integration for Nebula Logger.
+
+Any logs with MaxLogEntryLoggingLevelOrdinal__c >= the parameter 'SlackLoggingLevelThreshold' will send a notification to Slack
+
+
+ PluginApiName__c
+ SlackLoggerPlugin
+
+
+ PluginType__c
+ Apex
+
+
+ SObjectHandler__c
+ LogHandler
+
+
diff --git a/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackEndpoint.md-meta.xml b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackEndpoint.md-meta.xml
new file mode 100644
index 000000000..895b5d587
--- /dev/null
+++ b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackEndpoint.md-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+ false
+
+ CollectionType__c
+ Single
+
+
+ DataType__c
+ String
+
+
+ Description__c
+ The incoming webhook URL for your Slack app
+
+
+ SObjectHandlerPlugin__c
+ Slack
+
+
diff --git a/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackNotificationLoggingLevel.md-meta.xml b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackNotificationLoggingLevel.md-meta.xml
new file mode 100644
index 000000000..7996b1dfd
--- /dev/null
+++ b/nebula-logger-plugins/Slack/customMetadata/LoggerSObjectHandlerPluginParameter.SlackNotificationLoggingLevel.md-meta.xml
@@ -0,0 +1,21 @@
+
+
+
+ false
+
+ CollectionType__c
+ Single
+
+
+ DataType__c
+ String
+
+
+ Description__c
+ The logging level name that triggers a Slack notification to be sent. Possible logging levels are: ERROR, WARN, INFO, DEBUG, FINE, FINER, or FINEST
+
+
+ SObjectHandlerPlugin__c
+ Slack
+
+
diff --git a/nebula-logger-plugins/Slack/objects/Log__c/fields/SendSlackNotification__c.field-meta.xml b/nebula-logger-plugins/Slack/objects/Log__c/fields/SendSlackNotification__c.field-meta.xml
new file mode 100644
index 000000000..feee1b2e2
--- /dev/null
+++ b/nebula-logger-plugins/Slack/objects/Log__c/fields/SendSlackNotification__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ SendSlackNotification__c
+ false
+ false
+
+ true
+ true
+ false
+ Checkbox
+
diff --git a/nebula-logger-plugins/Slack/objects/Log__c/fields/SlackNotificationDate__c.field-meta.xml b/nebula-logger-plugins/Slack/objects/Log__c/fields/SlackNotificationDate__c.field-meta.xml
new file mode 100644
index 000000000..01da4256f
--- /dev/null
+++ b/nebula-logger-plugins/Slack/objects/Log__c/fields/SlackNotificationDate__c.field-meta.xml
@@ -0,0 +1,13 @@
+
+
+ SlackNotificationDate__c
+ The date/time that the log was sent to Slack
+ false
+ The date/time that the log was sent to Slack
+
+ false
+ true
+ true
+ false
+ DateTime
+
diff --git a/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsSentToSlack.listView-meta.xml b/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsSentToSlack.listView-meta.xml
new file mode 100644
index 000000000..4de6cecc2
--- /dev/null
+++ b/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsSentToSlack.listView-meta.xml
@@ -0,0 +1,21 @@
+
+
+ AllLogsSentToSlack
+ NAME
+ LoggedByUsernameLink__c
+ StartTime__c
+ OWNER.ALIAS
+ Priority__c
+ Status__c
+ TransactionId__c
+ TotalLimitsCpuTimeUsed__c
+ TotalLogEntries__c
+ TotalERRORLogEntries__c
+ TotalWARNLogEntries__c
+ Everything
+
+ SlackNotificationDate__c
+ notEqual
+
+
+
diff --git a/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsToBeSentToSlack.listView-meta.xml b/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsToBeSentToSlack.listView-meta.xml
new file mode 100644
index 000000000..0637b0e95
--- /dev/null
+++ b/nebula-logger-plugins/Slack/objects/Log__c/listViews/AllLogsToBeSentToSlack.listView-meta.xml
@@ -0,0 +1,26 @@
+
+
+ AllLogsToBeSentToSlack
+ NAME
+ LoggedByUsernameLink__c
+ StartTime__c
+ OWNER.ALIAS
+ Priority__c
+ Status__c
+ TransactionId__c
+ TotalLimitsCpuTimeUsed__c
+ TotalLogEntries__c
+ TotalERRORLogEntries__c
+ TotalWARNLogEntries__c
+ Everything
+
+ SlackNotificationDate__c
+ equals
+
+
+ SendSlackNotification__c
+ equals
+ 1
+
+
+
diff --git a/nebula-logger-plugins/Slack/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/SlackPlugin.listView-meta.xml b/nebula-logger-plugins/Slack/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/SlackPlugin.listView-meta.xml
new file mode 100644
index 000000000..814bd63d2
--- /dev/null
+++ b/nebula-logger-plugins/Slack/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/SlackPlugin.listView-meta.xml
@@ -0,0 +1,19 @@
+
+
+ SlackPlugin
+ MasterLabel
+ DeveloperName
+ SObjectHandlerPlugin__c
+ IsEnabled__c
+ CollectionType__c
+ DataType__c
+ Value__c
+ Description__c
+ Everything
+
+ SObjectHandlerPlugin__c
+ equals
+ Slack
+
+
+
diff --git a/nebula-logger-plugins/Slack/permissionsets/LoggerSlackPluginAdmin.permissionset-meta.xml b/nebula-logger-plugins/Slack/permissionsets/LoggerSlackPluginAdmin.permissionset-meta.xml
new file mode 100644
index 000000000..9db9ff4d0
--- /dev/null
+++ b/nebula-logger-plugins/Slack/permissionsets/LoggerSlackPluginAdmin.permissionset-meta.xml
@@ -0,0 +1,15 @@
+
+
+ Provides additional access for the Logger Slack plugin
+
+ true
+ Log__c.SendSlackNotification__c
+ true
+
+
+ true
+ Log__c.SlackNotificationDate__c
+ true
+
+
+
diff --git a/nebula-logger-plugins/Slack/remoteSiteSettings/Slack.remoteSite-meta.xml b/nebula-logger-plugins/Slack/remoteSiteSettings/Slack.remoteSite-meta.xml
new file mode 100644
index 000000000..3c9a47fda
--- /dev/null
+++ b/nebula-logger-plugins/Slack/remoteSiteSettings/Slack.remoteSite-meta.xml
@@ -0,0 +1,7 @@
+
+
+ Used to send notifications to Slack
+ false
+ true
+ https://hooks.slack.com
+
diff --git a/nebula-logger/main/log-management/classes/LogEntryEventHandler.cls b/nebula-logger/main/log-management/classes/LogEntryEventHandler.cls
index d3e234bc3..144d299f7 100644
--- a/nebula-logger/main/log-management/classes/LogEntryEventHandler.cls
+++ b/nebula-logger/main/log-management/classes/LogEntryEventHandler.cls
@@ -7,7 +7,7 @@
* @group Log Management
* @description Processes `LogEntryEvent__e` platform events and normalizes the data into `Log__c` and `LogEntry__c` records
*/
-public without sharing class LogEntryEventHandler {
+public without sharing class LogEntryEventHandler extends LoggerSObjectHandler {
private static final Map TRANSACTION_ID_TO_LOG = new Map();
@testVisible
@@ -15,33 +15,43 @@ public without sharing class LogEntryEventHandler {
// Trigger-based variables - tests can override these with mock objects
@testVisible
- private List logEntryEvents;
+ private List logEntryEvents {
+ get {
+ return (List) this.triggerNew;
+ }
+ }
- private List logEntries;
- private Map> logEntryToTopics;
- private Set topicNames;
+ private List logEntries = new List();
+ private Map> logEntryToTopics = new Map>();
+ private Set topicNames = new Set();
public LogEntryEventHandler() {
- this((List) Trigger.new);
}
public LogEntryEventHandler(List logEntryEvents) {
- this.logEntryEvents = logEntryEvents;
+ // Override the records provided by LoggerSObjectHandler
+ this.triggerNew = logEntryEvents;
+ }
- this.logEntries = new List();
- this.logEntryToTopics = new Map>();
- this.topicNames = new Set();
+ /**
+ * @description Returns SObject Type that the handler is responsible for processing
+ * @return The instance of `SObjectType`
+ */
+ public override SObjectType getSObjectType() {
+ return Schema.LogEntryEvent__e.SObjectType;
}
/**
* @description Runs the trigger handler's logic for the `LogEntryEvent__e` platform event object
*/
- public void execute() {
- if (this.logEntryEvents.isEmpty() == false) {
- this.upsertLogs();
- this.insertLogEntries();
- this.insertTopics();
+ public override void execute() {
+ if (this.isEnabled() == false) {
+ return;
}
+
+ this.upsertLogs();
+ this.insertLogEntries();
+ this.insertTopics();
}
private void upsertLogs() {
diff --git a/nebula-logger/main/log-management/classes/LogEntryHandler.cls b/nebula-logger/main/log-management/classes/LogEntryHandler.cls
index 9d8d31786..9922ccae1 100644
--- a/nebula-logger/main/log-management/classes/LogEntryHandler.cls
+++ b/nebula-logger/main/log-management/classes/LogEntryHandler.cls
@@ -7,22 +7,34 @@
* @group Log Management
* @description Manages setting fields on `LogEntry__c` before insert & before update
*/
-public without sharing class LogEntryHandler {
+public without sharing class LogEntryHandler extends LoggerSObjectHandler {
// Trigger-based variables - tests can override these with mock objects
@testVisible
- private List logEntries;
- @testVisible
- private TriggerOperation triggerOperationType;
+ private List logEntries {
+ get {
+ return (List) this.triggerNew;
+ }
+ }
public LogEntryHandler() {
- this.logEntries = (List) Trigger.new;
- this.triggerOperationType = Trigger.operationType;
+ }
+
+ /**
+ * @description Returns SObject Type that the handler is responsible for processing
+ * @return The instance of `SObjectType`
+ */
+ public override SObjectType getSObjectType() {
+ return Schema.LogEntry__c.SObjectType;
}
/**
* @description Runs the trigger handler's logic for the `LogEntry__c` custom object
*/
- public void execute() {
+ public override void execute() {
+ if (this.isEnabled() == false) {
+ return;
+ }
+
switch on this.triggerOperationType {
when BEFORE_INSERT {
this.setCheckboxFields();
@@ -39,6 +51,9 @@ public without sharing class LogEntryHandler {
this.setCheckboxFields();
}
}
+
+ // Run any plugins configured in the LoggerSObjectHandlerPlugin__mdt custom metadata type
+ this.executePlugins();
}
private void setCheckboxFields() {
diff --git a/nebula-logger/main/log-management/classes/LogHandler.cls b/nebula-logger/main/log-management/classes/LogHandler.cls
index 82f2624ae..e697b2075 100644
--- a/nebula-logger/main/log-management/classes/LogHandler.cls
+++ b/nebula-logger/main/log-management/classes/LogHandler.cls
@@ -7,7 +7,7 @@
* @group Log Management
* @description Manages setting fields on `Log__c` before insert & before update
*/
-public without sharing class LogHandler {
+public without sharing class LogHandler extends LoggerSObjectHandler {
private static final Organization ORGANIZATION = [SELECT Id, InstanceName, IsSandbox FROM Organization];
@testVisible
@@ -26,22 +26,38 @@ public without sharing class LogHandler {
// Trigger-based variables - tests can override these with mock objects
@testVisible
- private List logs;
- @testVisible
- private Map oldLogsById;
+ private List logs {
+ get {
+ return (List) this.triggerNew;
+ }
+ }
+
@testVisible
- private TriggerOperation triggerOperationType;
+ private Map oldLogsById {
+ get {
+ return (Map) this.triggerOldMap;
+ }
+ }
public LogHandler() {
- this.logs = (List) Trigger.new;
- this.oldLogsById = (Map) Trigger.oldMap;
- this.triggerOperationType = Trigger.operationType;
+ }
+
+ /**
+ * @description Returns SObject Type that the handler is responsible for processing
+ * @return The instance of `SObjectType`
+ */
+ public override SObjectType getSObjectType() {
+ return Schema.Log__c.SObjectType;
}
/**
* @description Runs the trigger handler's logic for the `Log__c` custom object
*/
- public void execute() {
+ public override void execute() {
+ if (this.isEnabled() == false) {
+ return;
+ }
+
switch on this.triggerOperationType {
when BEFORE_INSERT {
this.setOrgReleaseCycle();
@@ -58,6 +74,9 @@ public without sharing class LogHandler {
this.shareLogsWithLoggingUsers();
}
}
+
+ // Run any plugins configured in the LoggerSObjectHandlerPlugin__mdt custom metadata type
+ this.executePlugins();
}
private void setOrgReleaseCycle() {
diff --git a/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls b/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls
new file mode 100644
index 000000000..264ce929a
--- /dev/null
+++ b/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls
@@ -0,0 +1,146 @@
+//------------------------------------------------------------------------------------------------//
+// 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 Log Management
+ * @description Abstract class used by trigger handlers for shared logic
+ */
+public abstract class LoggerSObjectHandler {
+ @testVisible
+ private static Map configurationBySObjectType;
+ @testVisible
+ private static Map> pluginsBySObjectType;
+
+ static {
+ // When using CMDT's getAll(), it does not return relationship fields for EntityDefinition fields or child CMDT objects...
+ // ... so instead query the LoggerSObjectHandler__mdt CMDT object
+ List configurations = [
+ SELECT
+ Id,
+ IsEnabled__c,
+ SObjectType__r.QualifiedApiName,
+ (
+ SELECT Id, PluginType__c, PluginApiName__c
+ FROM LoggerSObjectHandlerPlugins__r
+ WHERE IsEnabled__c = TRUE
+ ORDER BY ExecutionOrder__c NULLS LAST, DeveloperName
+ )
+ FROM LoggerSObjectHandler__mdt
+ ];
+
+ configurationBySObjectType = new Map();
+ pluginsBySObjectType = new Map>();
+ for (LoggerSObjectHandler__mdt config : configurations) {
+ // CMDT entity-definition relationship fields are weird, so skip some headaches by copying the Qualified API name
+ config.SObjectType__c = config.SObjectType__r.QualifiedApiName;
+
+ // Schema.getGlobalDescribe() is the worst, so don't use it
+ SObjectType sobjectType = ((SObject) Type.forName(config.SObjectType__c).newInstance()).getSObjectType();
+
+ configurationBySObjectType.put(sobjectType, config);
+ pluginsBySObjectType.put(sobjectType, config?.LoggerSObjectHandlerPlugins__r);
+ }
+
+ if (Test.isRunningTest() == true) {
+ // Test shouldn't rely on the actual CMDT rules in the org - clear the loaded values, and defaults will be used
+ configurationBySObjectType.clear();
+ pluginsBySObjectType.clear();
+ }
+ }
+
+ @testVisible
+ protected TriggerOperation triggerOperationType;
+ @testVisible
+ protected List triggerNew;
+ @testVisible
+ protected Map triggerNewMap;
+ @testVisible
+ protected List triggerOld;
+ @testVisible
+ protected Map triggerOldMap;
+
+ private LoggerSObjectHandler__mdt handlerConfiguration;
+ private List plugins;
+
+ public LoggerSObjectHandler() {
+ this.setConfigurations();
+
+ if (this.handlerConfiguration.IsEnabled__c == true) {
+ this.triggerOperationType = Trigger.operationType;
+ this.triggerNew = Trigger.new;
+ this.triggerNewMap = Trigger.newMap;
+ this.triggerOld = Trigger.old;
+ this.triggerOldMap = Trigger.oldMap;
+ }
+ }
+
+ /**
+ * @description Returns the SObject Type that the handler is responsible for processing
+ * @return The instance of `SObjectType`
+ */
+ public abstract SObjectType getSObjectType();
+
+ /**
+ * @description Runs the handler class's logic
+ */
+ public abstract void execute();
+
+ /**
+ * @description Indicates if the current SObject Handler is enabled, based on `LoggerSObjectHandler__mdt.IsEnabled__c`
+ * @return The `Boolean` value (`true` == enabled, `false` == disabled)
+ */
+ protected Boolean isEnabled() {
+ return this.handlerConfiguration.IsEnabled__c;
+ }
+
+ protected void executePlugins() {
+ if (this.triggerNew == null || this.plugins == null || this.plugins.isEmpty() == true) {
+ return;
+ }
+
+ for (LoggerSObjectHandlerPlugin__mdt pluginConfiguration : this.plugins) {
+ switch on pluginConfiguration.PluginType__c {
+ when 'Apex' {
+ this.executeApexPlugin(pluginConfiguration.PluginApiName__c);
+ }
+ when 'Flow' {
+ this.executeFlowPlugin(pluginConfiguration.PluginApiName__c);
+ }
+ }
+ }
+ }
+
+ private void setConfigurations() {
+ this.handlerConfiguration = configurationBySObjectType.get(this.getSObjectType());
+ this.plugins = pluginsBySObjectType.get(this.getSObjectType());
+
+ if (this.handlerConfiguration == null) {
+ // If no config exists in the org, then load some in-memory defaults
+ this.handlerConfiguration = new LoggerSObjectHandler__mdt(IsEnabled__c = true, SObjectType__c = this.getSObjectType().getDescribe().getName());
+
+ configurationBySObjectType.put(this.getSObjectType(), this.handlerConfiguration);
+ }
+ }
+
+ private void executeApexPlugin(String apexClassName) {
+ LoggerSObjectHandlerPlugin apexPlugin = (LoggerSObjectHandlerPlugin) Type.forName(apexClassName).newInstance();
+ apexPlugin.execute(this.triggerOperationType, this.triggerNew, this.triggerNewMap, this.triggerOld, this.triggerOldMap);
+ }
+
+ private void executeFlowPlugin(String flowApiName) {
+ Map flowInputs = new Map();
+ flowInputs.put('triggerOperationType', this.triggerOperationType?.name());
+ flowInputs.put('triggerNew', this.triggerNew);
+ flowInputs.put('triggerOld', this.triggerOld);
+
+ Flow.Interview flowPlugin = Flow.Interview.createInterview(flowApiName, flowInputs);
+ flowPlugin.start();
+
+ List updatedTriggerNew = (List) flowPlugin.getVariableValue('updatedTriggerNew');
+ if (updatedTriggerNew != null && updatedTriggerNew.size() == this.triggerNew.size()) {
+ this.triggerNew = updatedTriggerNew;
+ }
+ }
+}
diff --git a/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls-meta.xml b/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls-meta.xml
new file mode 100644
index 000000000..482559c8b
--- /dev/null
+++ b/nebula-logger/main/log-management/classes/LoggerSObjectHandler.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/nebula-logger/main/log-management/classes/RelatedLogEntriesController.cls b/nebula-logger/main/log-management/classes/RelatedLogEntriesController.cls
index 8d9ef218e..fc60a4a1f 100644
--- a/nebula-logger/main/log-management/classes/RelatedLogEntriesController.cls
+++ b/nebula-logger/main/log-management/classes/RelatedLogEntriesController.cls
@@ -84,6 +84,7 @@ public with sharing class RelatedLogEntriesController {
};
String logEntryQuery = 'SELECT {0} FROM {1} WHERE {2} = :recordId ORDER BY {3} LIMIT {4}';
logEntryQuery = String.format(logEntryQuery, queryTextReplacements);
+ System.debug('logEntryQuery==' + logEntryQuery);
return (List) Database.query(logEntryQuery);
}
@@ -149,15 +150,13 @@ public with sharing class RelatedLogEntriesController {
for (String fieldName : educatedGuesses) {
Schema.SObjectField field = lookupSObjectType.getDescribe().fields.getMap().get(fieldName);
- if (field == null) {
- continue;
- }
-
- Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
+ if (field != null) {
+ Schema.DescribeFieldResult fieldDescribe = field.getDescribe();
- if (fieldDescribe.isNameField()) {
- displayFieldApiName = fieldDescribe.getName();
- break;
+ if (fieldDescribe.isNameField()) {
+ displayFieldApiName = fieldDescribe.getName();
+ break;
+ }
}
}
@@ -169,14 +168,7 @@ public with sharing class RelatedLogEntriesController {
String tabIcon;
for (Schema.DescribeTabSetResult tabSetResult : Schema.describeTabs()) {
- if (tabIcon != null) {
- break;
- }
-
for (Schema.DescribeTabResult tabResult : tabSetResult.getTabs()) {
- if (tabIcon != null) {
- break;
- }
if (tabResult.getSObjectName() != sobjectName) {
continue;
}
@@ -194,12 +186,6 @@ public with sharing class RelatedLogEntriesController {
}
}
}
- // Hardcoded exceptions - Salesforce doesn't return SVGs for these objects, so hardcoding is necessary
- if (tabIcon == null && sobjectName == 'Asset') {
- tabIcon = 'standard:maintenance_asset';
- } else if (tabIcon == null && sobjectName == 'AssetRelationship') {
- tabIcon = 'standard:asset_relationship';
- }
return tabIcon;
}
diff --git a/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryEventHandler.md-meta.xml b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryEventHandler.md-meta.xml
new file mode 100644
index 000000000..fb1b68276
--- /dev/null
+++ b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryEventHandler.md-meta.xml
@@ -0,0 +1,13 @@
+
+
+
+ false
+
+ SObjectType__c
+ LogEntryEvent__e
+
+
diff --git a/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryHandler.md-meta.xml b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryHandler.md-meta.xml
new file mode 100644
index 000000000..1fa2d72d0
--- /dev/null
+++ b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogEntryHandler.md-meta.xml
@@ -0,0 +1,13 @@
+
+
+
+ false
+
+ SObjectType__c
+ LogEntry__c
+
+
diff --git a/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogHandler.md-meta.xml b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogHandler.md-meta.xml
new file mode 100644
index 000000000..77c2ad016
--- /dev/null
+++ b/nebula-logger/main/log-management/customMetadata/LoggerSObjectHandler.LogHandler.md-meta.xml
@@ -0,0 +1,13 @@
+
+
+
+ false
+
+ SObjectType__c
+ Log__c
+
+
diff --git a/nebula-logger/main/log-management/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml b/nebula-logger/main/log-management/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml
new file mode 100644
index 000000000..c0affbfbd
--- /dev/null
+++ b/nebula-logger/main/log-management/layouts/LoggerSObjectHandler__mdt-Logger SObject Handler Layout.layout-meta.xml
@@ -0,0 +1,89 @@
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ MasterLabel
+
+
+ Required
+ DeveloperName
+
+
+
+
+ Edit
+ IsEnabled__c
+
+
+ Required
+ SObjectType__c
+
+
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ NamespacePrefix
+
+
+ Readonly
+ CreatedById
+
+
+
+
+ Edit
+ IsProtected
+
+
+ Readonly
+ LastModifiedById
+
+
+
+
+
+ true
+ false
+ false
+
+
+
+
+
+
+
+ MasterLabel
+ DeveloperName
+ IsEnabled__c
+ PluginType__c
+ PluginApiName__c
+ ExecutionOrder__c
+ LoggerSObjectHandlerPlugin__mdt.SObjectHandler__c
+ ExecutionOrder__c
+ Asc
+
+ false
+ false
+ false
+ false
+ false
+
+ 00h3F0000048Uxu
+ 4
+ 0
+ Default
+
+
diff --git a/nebula-logger/main/log-management/objects/Log__c/fields/MaxLogEntryLoggingLevelOrdinal__c.field-meta.xml b/nebula-logger/main/log-management/objects/Log__c/fields/MaxLogEntryLoggingLevelOrdinal__c.field-meta.xml
new file mode 100644
index 000000000..17fcb990c
--- /dev/null
+++ b/nebula-logger/main/log-management/objects/Log__c/fields/MaxLogEntryLoggingLevelOrdinal__c.field-meta.xml
@@ -0,0 +1,14 @@
+
+
+ MaxLogEntryLoggingLevelOrdinal__c
+ The highest logging level ordinal of any related log entries
+ false
+ The highest logging level ordinal of any related log entries
+
+ LogEntry__c.LoggingLevelOrdinal__c
+ LogEntry__c.Log__c
+ max
+ false
+ false
+ Summary
+
diff --git a/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/LoggerSObjectHandler__mdt.object-meta.xml b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/LoggerSObjectHandler__mdt.object-meta.xml
new file mode 100644
index 000000000..b97113ef0
--- /dev/null
+++ b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/LoggerSObjectHandler__mdt.object-meta.xml
@@ -0,0 +1,7 @@
+
+
+ Used to configure the Apex trigger handler classes that run on the objects LogEntryEvent__e, Log__c and LogEntry__c
+
+ Logger SObject Handlers
+ Public
+
diff --git a/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/IsEnabled__c.field-meta.xml b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/IsEnabled__c.field-meta.xml
new file mode 100644
index 000000000..2aa194e96
--- /dev/null
+++ b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/IsEnabled__c.field-meta.xml
@@ -0,0 +1,13 @@
+
+
+ IsEnabled__c
+ true
+ Controls if the SObject's trigger handler class should execute. This is useful for temporary disabling the logger's trigger handlers, but should typically be enabled.
+ false
+ SubscriberControlled
+ Controls if the SObject's trigger handler class should execute. This is useful for temporary disabling the logger's trigger handlers, but should typically be enabled.
+
+ Checkbox
+
diff --git a/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml
new file mode 100644
index 000000000..2bcd98bfb
--- /dev/null
+++ b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/fields/SObjectType__c.field-meta.xml
@@ -0,0 +1,13 @@
+
+
+ SObjectType__c
+ false
+ DeveloperControlled
+
+ EntityDefinition
+ Logger SObject Handler Configurations
+ LoggerSObjectHandlerConfigurations
+ true
+ MetadataRelationship
+ true
+
diff --git a/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/listViews/All.listView-meta.xml b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/listViews/All.listView-meta.xml
new file mode 100644
index 000000000..f5ac4c12a
--- /dev/null
+++ b/nebula-logger/main/log-management/objects/LoggerSObjectHandler__mdt/listViews/All.listView-meta.xml
@@ -0,0 +1,10 @@
+
+
+ All
+ MasterLabel
+ DeveloperName
+ IsEnabled__c
+ SObjectType__c
+ Everything
+
+
diff --git a/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml
index 73dc83470..941ff8ddf 100644
--- a/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml
+++ b/nebula-logger/main/log-management/permissionsets/LoggerAdmin.permissionset-meta.xml
@@ -584,6 +584,11 @@
Log__c.LogoutUrl__ctrue
+
+ false
+ Log__c.MaxLogEntryLoggingLevelOrdinal__c
+ true
+ falseLog__c.NetworkId__c
diff --git a/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml
index f487a98e6..4d097b02a 100644
--- a/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml
+++ b/nebula-logger/main/log-management/permissionsets/LoggerEndUser.permissionset-meta.xml
@@ -562,6 +562,11 @@
Log__c.LogoutUrl__ctrue
+
+ false
+ Log__c.MaxLogEntryLoggingLevelOrdinal__c
+ true
+ falseLog__c.NetworkId__c
diff --git a/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml b/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml
index 7057662c5..b09a3fe0b 100644
--- a/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml
+++ b/nebula-logger/main/log-management/permissionsets/LoggerLogViewer.permissionset-meta.xml
@@ -548,6 +548,11 @@
Log__c.LogoutUrl__ctrue
+
+ false
+ Log__c.MaxLogEntryLoggingLevelOrdinal__c
+ true
+ falseLog__c.NetworkId__c
diff --git a/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml b/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml
index df4f7afcf..01a9639c9 100644
--- a/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml
+++ b/nebula-logger/main/log-management/profiles/Admin.profile-meta.xml
@@ -97,6 +97,14 @@
Loggertrue
+
+ LoggerSObjectHandler
+ true
+
+
+ LoggerSObjectHandlerPlugin
+ true
+ Logger_Teststrue
@@ -1150,6 +1158,11 @@
Log__c.LogoutUrl__ctrue
+
+ false
+ Log__c.MaxLogEntryLoggingLevelOrdinal__c
+ true
+ falseLog__c.NetworkId__c
@@ -1400,6 +1413,21 @@
Log__c.WasLoggedByCurrentUser__ctrue
+
+ false
+ LoggerSObjectHandler__mdt.IsEnabled__c
+ false
+
+
+ false
+ LoggerSObjectHandlerPlugin__mdt.ExecutionOrder__c
+ false
+
+
+ false
+ LoggerSObjectHandlerPlugin__mdt.IsEnabled__c
+ false
+ LogEntry__c-Log Entry Layout
@@ -1409,6 +1437,12 @@
Log__c-Log Layout
+
+ LoggerSObjectHandler__mdt-Logger SObject Handler Layout
+
+
+ LoggerSObjectHandlerPlugin__mdt-Logger SObject Handler Plugin Layout
+ truetrue
@@ -1436,6 +1470,10 @@
true
+
+ LogMassDelete
+ true
+ LogEntry__cDefaultOn
diff --git a/nebula-logger/main/log-management/triggers/Log.trigger b/nebula-logger/main/log-management/triggers/Log.trigger
index 754ef74a0..e7bfb5ad4 100644
--- a/nebula-logger/main/log-management/triggers/Log.trigger
+++ b/nebula-logger/main/log-management/triggers/Log.trigger
@@ -2,6 +2,6 @@
// 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. //
//------------------------------------------------------------------------------------------------//
-trigger Log on Log__c(before insert, before update, after insert) {
+trigger Log on Log__c(before insert, before update, before delete, after insert, after update, after delete, after undelete) {
new LogHandler().execute();
}
diff --git a/nebula-logger/main/log-management/triggers/LogEntry.trigger b/nebula-logger/main/log-management/triggers/LogEntry.trigger
index 842830472..07009e45a 100644
--- a/nebula-logger/main/log-management/triggers/LogEntry.trigger
+++ b/nebula-logger/main/log-management/triggers/LogEntry.trigger
@@ -2,6 +2,6 @@
// 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. //
//------------------------------------------------------------------------------------------------//
-trigger LogEntry on LogEntry__c(before insert, before update) {
+trigger LogEntry on LogEntry__c(before insert, before update, before delete, after insert, after update, after delete, after undelete) {
new LogEntryHandler().execute();
}
diff --git a/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls b/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls
new file mode 100644
index 000000000..bbf36aefd
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls
@@ -0,0 +1,39 @@
+//------------------------------------------------------------------------------------------------//
+// 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 Plugin Framework
+ * @description Abstract class used to create custom Apex plugins to execute for all trigger operations on `Log__c` or `LogEntry__c`
+ */
+// TODO: If possible, convert to `global` for the managed package - there are some additional platform limitations
+// and restrictions with making future changes to abstract classes & interfaces within managed packages
+// which could lead to a lot of problems, so for now/indefinitely, plugins will only work in the unlocked package
+public abstract class LoggerSObjectHandlerPlugin {
+ /**
+ * @description All instances of `LoggerSObjectHandlerPlugin` are dynamically created, which requires aparameterless constructor
+ */
+ public LoggerSObjectHandlerPlugin() {
+ }
+
+ /**
+ * @description This method is the entry point for plugins to execute any custom logic.
+ * It is automatically called by the logging system for any enabled plugins.
+ * Several trigger-based parameters are provided - these parameters should be used by plugins,
+ * instead of calling the platform's static variables directly
+ * (e.g., use the provided `triggerNew` variable instead of using `Trigger.new` directly, and so on).
+ * @param triggerOperationType The enum instance of `Trigger.operationType` at the time that the handler class is created
+ * @param triggerNew The value `Trigger.new` at the time that the handler class is created
+ * @param triggerNewMap The value `Trigger.newMap` at the time that the handler class is created
+ * @param triggerOld The value `Trigger.old` at the time that the handler class is created
+ * @param triggerOldMap The value `Trigger.oldMap` at the time that the handler class is created
+ */
+ public abstract void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ );
+}
diff --git a/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls-meta.xml b/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls-meta.xml
new file mode 100644
index 000000000..482559c8b
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/classes/LoggerSObjectHandlerPlugin.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPluginParameter__mdt-Logger SObject Handler Plugin Parameter Layout.layout-meta.xml b/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPluginParameter__mdt-Logger SObject Handler Plugin Parameter Layout.layout-meta.xml
new file mode 100644
index 000000000..715ecc56a
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPluginParameter__mdt-Logger SObject Handler Plugin Parameter Layout.layout-meta.xml
@@ -0,0 +1,103 @@
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ MasterLabel
+
+
+ Required
+ DeveloperName
+
+
+
+
+ Edit
+ IsEnabled__c
+
+
+ Required
+ SObjectHandlerPlugin__c
+
+
+
+
+
+ true
+ true
+ true
+
+
+
+ Required
+ CollectionType__c
+
+
+ Required
+ DataType__c
+
+
+ Edit
+ Value__c
+
+
+ Edit
+ Description__c
+
+
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ NamespacePrefix
+
+
+ Readonly
+ CreatedById
+
+
+
+
+ Edit
+ IsProtected
+
+
+ Readonly
+ LastModifiedById
+
+
+
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+ false
+ false
+ false
+ false
+ false
+
+ 00h11000004BsIW
+ 4
+ 0
+ Default
+
+
diff --git a/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPlugin__mdt-Logger SObject Handler Plugin Layout.layout-meta.xml b/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPlugin__mdt-Logger SObject Handler Plugin Layout.layout-meta.xml
new file mode 100644
index 000000000..527380dbe
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/layouts/LoggerSObjectHandlerPlugin__mdt-Logger SObject Handler Plugin Layout.layout-meta.xml
@@ -0,0 +1,125 @@
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ MasterLabel
+
+
+ Required
+ DeveloperName
+
+
+
+
+ Edit
+ IsEnabled__c
+
+
+ Required
+ SObjectHandler__c
+
+
+ Edit
+ ExecutionOrder__c
+
+
+
+
+
+ true
+ true
+ true
+
+
+
+ Required
+ PluginType__c
+
+
+
+
+ Required
+ PluginApiName__c
+
+
+
+
+
+ true
+ false
+ false
+
+
+
+ Edit
+ Description__c
+
+
+
+
+
+ false
+ true
+ true
+
+
+
+ Required
+ NamespacePrefix
+
+
+ Readonly
+ CreatedById
+
+
+
+
+ Edit
+ IsProtected
+
+
+ Readonly
+ LastModifiedById
+
+
+
+
+
+ true
+ true
+ false
+
+
+
+
+
+
+
+ MasterLabel
+ DeveloperName
+ IsEnabled__c
+ CollectionType__c
+ DataType__c
+ Value__c
+ LoggerSObjectHandlerPluginParameter__mdt.SObjectHandlerPlugin__c
+ DeveloperName
+ Asc
+
+ false
+ false
+ false
+ false
+ false
+
+ 00h11000004Bf4m
+ 4
+ 0
+ Default
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/LoggerSObjectHandlerPluginParameter__mdt.object-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/LoggerSObjectHandlerPluginParameter__mdt.object-meta.xml
new file mode 100644
index 000000000..6c6f32551
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/LoggerSObjectHandlerPluginParameter__mdt.object-meta.xml
@@ -0,0 +1,7 @@
+
+
+ Used to store additional key-value pair parameters that can be used by Logger plugins
+
+ Logger Plugin Parameters
+ Protected
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/CollectionType__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/CollectionType__c.field-meta.xml
new file mode 100644
index 000000000..c67d76d33
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/CollectionType__c.field-meta.xml
@@ -0,0 +1,27 @@
+
+
+ CollectionType__c
+ The expected collection type of the parameter. For example, String vs List<String>
+ false
+ DeveloperControlled
+ The expected collection type of the parameter. For example, String vs List<String>
+
+ true
+ Picklist
+
+ true
+
+ false
+
+ Single
+ true
+
+
+
+ List
+ false
+
+
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/DataType__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/DataType__c.field-meta.xml
new file mode 100644
index 000000000..8fc10b649
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/DataType__c.field-meta.xml
@@ -0,0 +1,72 @@
+
+
+ DataType__c
+ The expected data type of the parameter value
+ false
+ DeveloperControlled
+ The expected data type of the parameter value
+
+ true
+ Picklist
+
+ true
+
+ false
+
+ Boolean
+ false
+
+
+
+ Date
+ false
+
+
+
+ Datetime
+ false
+
+
+
+ Decimal
+ false
+
+
+
+ Double
+ false
+
+
+
+ Id
+ false
+
+
+
+ Integer
+ false
+
+
+
+ Long
+ false
+
+
+
+ Object
+ false
+
+
+
+ SObject
+ false
+
+
+
+ String
+ false
+
+
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Description__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Description__c.field-meta.xml
new file mode 100644
index 000000000..53ef07be5
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Description__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ Description__c
+ Used purely for documentation purposes to store any important details about this plugin
+ false
+ DeveloperControlled
+ Used purely for documentation purposes to store any important details about this plugin
+
+ 131072
+ LongTextArea
+ 3
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/IsEnabled__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/IsEnabled__c.field-meta.xml
new file mode 100644
index 000000000..77e61e455
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/IsEnabled__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ IsEnabled__c
+ true
+ Controls if the plugin parameter is enabled/disabled. Only enabled plugin parameters will be used by the Apex plugin framework.
+ false
+ SubscriberControlled
+ Controls if the plugin parameter is enabled/disabled. Only enabled plugin parameters will be used by the Apex plugin framework.
+
+ Checkbox
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/SObjectHandlerPlugin__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/SObjectHandlerPlugin__c.field-meta.xml
new file mode 100644
index 000000000..b3ee1ca33
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/SObjectHandlerPlugin__c.field-meta.xml
@@ -0,0 +1,15 @@
+
+
+ SObjectHandlerPlugin__c
+ The configured plugin that uses this parameter
+ false
+ DeveloperControlled
+ The configured plugin that uses this parameter
+
+ LoggerSObjectHandlerPlugin__mdt
+ Logger Plugin Parameters
+ LoggerSObjectHandlerPluginParameters
+ true
+ MetadataRelationship
+ false
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Value__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Value__c.field-meta.xml
new file mode 100644
index 000000000..498ea9e20
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/fields/Value__c.field-meta.xml
@@ -0,0 +1,16 @@
+
+
+ Value__c
+ The value used by the plugin for this parameter. Since custom metadata type (CMDT) records can be deployed between orgs, you can either:
+ - Deploy the CMDT records between your orgs - for parameters that should be consistent in all environments
+ - Manually configure the CMDT records in each org - for parameters that should be environment-specific
+ false
+ SubscriberControlled
+ The value used by the plugin for this parameter. Since custom metadata type (CMDT) records can be deployed between orgs, you can either:
+ - Deploy the CMDT records between your orgs - for parameters that should be consistent in all environments
+ - Manually configure the CMDT records in each org - for parameters that should be environment-specific
+
+ 131072
+ LongTextArea
+ 3
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/All.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/All.listView-meta.xml
new file mode 100644
index 000000000..a0b6d841e
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/All.listView-meta.xml
@@ -0,0 +1,14 @@
+
+
+ All
+ MasterLabel
+ DeveloperName
+ SObjectHandlerPlugin__c
+ IsEnabled__c
+ CollectionType__c
+ DataType__c
+ Value__c
+ Description__c
+ Everything
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllDisabled.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllDisabled.listView-meta.xml
new file mode 100644
index 000000000..c048bc531
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllDisabled.listView-meta.xml
@@ -0,0 +1,18 @@
+
+
+ AllDisabled
+ MasterLabel
+ DeveloperName
+ SObjectHandlerPlugin__c
+ CollectionType__c
+ DataType__c
+ Value__c
+ Description__c
+ Everything
+
+ IsEnabled__c
+ equals
+ 0
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllEnabled.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllEnabled.listView-meta.xml
new file mode 100644
index 000000000..37eaab911
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPluginParameter__mdt/listViews/AllEnabled.listView-meta.xml
@@ -0,0 +1,18 @@
+
+
+ AllEnabled
+ MasterLabel
+ DeveloperName
+ SObjectHandlerPlugin__c
+ CollectionType__c
+ DataType__c
+ Value__c
+ Description__c
+ Everything
+
+ IsEnabled__c
+ equals
+ 1
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/LoggerSObjectHandlerPlugin__mdt.object-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/LoggerSObjectHandlerPlugin__mdt.object-meta.xml
new file mode 100644
index 000000000..427f892cc
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/LoggerSObjectHandlerPlugin__mdt.object-meta.xml
@@ -0,0 +1,7 @@
+
+
+ Used to configure additional Apex classes and Flows that should be executed all trigger operations on Log__c and LogEntry__c
+
+ Logger Plugins
+ Protected
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/Description__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/Description__c.field-meta.xml
new file mode 100644
index 000000000..163c5c139
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/Description__c.field-meta.xml
@@ -0,0 +1,12 @@
+
+
+ Description__c
+ Used purely for documentation purposes to store any important details about this parameter
+ false
+ DeveloperControlled
+ Used purely for documentation purposes to store any important details about this parameter
+
+ 131072
+ LongTextArea
+ 3
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/ExecutionOrder__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/ExecutionOrder__c.field-meta.xml
new file mode 100644
index 000000000..8b2197334
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/ExecutionOrder__c.field-meta.xml
@@ -0,0 +1,14 @@
+
+
+ ExecutionOrder__c
+ The specific order to execute plugins in the org. Plugins are executed in order by sorting by execution order, then plugin name (ORDER BY ExecutionOrder__c NULLS LAST, DeveloperName).
+ false
+ SubscriberControlled
+ The specific order to execute plugins in the org. Plugins are executed in order by sorting by execution order, then plugin name (ORDER BY ExecutionOrder__c NULLS LAST, DeveloperName).
+
+ 3
+ false
+ 0
+ Number
+ false
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/IsEnabled__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/IsEnabled__c.field-meta.xml
new file mode 100644
index 000000000..6b4db687d
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/IsEnabled__c.field-meta.xml
@@ -0,0 +1,11 @@
+
+
+ IsEnabled__c
+ true
+ Controls if the plugin is enabled/disabled. Only enabled plugins will be executed by the logging system.
+ false
+ SubscriberControlled
+ Controls if the plugin is enabled/disabled. Only enabled plugins will be executed by the logging system.
+
+ Checkbox
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginApiName__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginApiName__c.field-meta.xml
new file mode 100644
index 000000000..57c7c769d
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginApiName__c.field-meta.xml
@@ -0,0 +1,27 @@
+
+
+ PluginApiName__c
+ The API name of the metadata to run, based on the Plugin Type
+
+Apex: The name of an Apex class that implements the interface LoggerSObjectHandlerPlugin
+
+Flow: The API name of a Flow that supports these inputs:
+ - triggerOperationType: text variable
+ - triggerNew: record collection variable
+ - triggerOld: record collection variable
+ false
+ DeveloperControlled
+ The API name of the metadata to run, based on the Plugin Type
+
+Apex: The name of an Apex class that implements the interface LoggerSObjectHandlerPlugin
+
+Flow: The API name of a Flow that supports these inputs:
+ - triggerOperationType: text variable
+ - triggerNew: record collection variable
+ - triggerOld: record collection variable
+
+ 255
+ true
+ Text
+ false
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginType__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginType__c.field-meta.xml
new file mode 100644
index 000000000..cf55ff8bd
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/PluginType__c.field-meta.xml
@@ -0,0 +1,27 @@
+
+
+ PluginType__c
+ The type of plugin that the logging system should dynamically execute - Apex or Flow.
+ false
+ DeveloperControlled
+ The type of plugin that the logging system should dynamically execute - Apex or Flow.
+
+ true
+ Picklist
+
+ true
+
+ false
+
+ Apex
+ true
+
+
+
+ Flow
+ false
+
+
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/SObjectHandler__c.field-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/SObjectHandler__c.field-meta.xml
new file mode 100644
index 000000000..fa88c1444
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/fields/SObjectHandler__c.field-meta.xml
@@ -0,0 +1,15 @@
+
+
+ SObjectHandler__c
+ The logging system's trigger SObject handler class that should dynamically call the plugin. This controls when the plugin is executed.
+ false
+ SubscriberControlled
+ The logging system's trigger SObject handler class that should dynamically call the plugin. This controls when the plugin is executed.
+
+ LoggerSObjectHandler__mdt
+ Logger Plugins
+ LoggerSObjectHandlerPlugins
+ true
+ MetadataRelationship
+ false
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/All.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/All.listView-meta.xml
new file mode 100644
index 000000000..efec1f187
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/All.listView-meta.xml
@@ -0,0 +1,14 @@
+
+
+ All
+ MasterLabel
+ DeveloperName
+ IsEnabled__c
+ SObjectHandler__c
+ PluginType__c
+ PluginApiName__c
+ ExecutionOrder__c
+ Description__c
+ Everything
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllDisabled.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllDisabled.listView-meta.xml
new file mode 100644
index 000000000..6638affcc
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllDisabled.listView-meta.xml
@@ -0,0 +1,18 @@
+
+
+ AllDisabled
+ MasterLabel
+ DeveloperName
+ SObjectHandler__c
+ PluginType__c
+ PluginApiName__c
+ ExecutionOrder__c
+ Description__c
+ Everything
+
+ IsEnabled__c
+ equals
+ 0
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllEnabled.listView-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllEnabled.listView-meta.xml
new file mode 100644
index 000000000..3c0256e2d
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/listViews/AllEnabled.listView-meta.xml
@@ -0,0 +1,18 @@
+
+
+ AllEnabled
+ MasterLabel
+ DeveloperName
+ SObjectHandler__c
+ PluginType__c
+ PluginApiName__c
+ ExecutionOrder__c
+ Description__c
+ Everything
+
+ IsEnabled__c
+ equals
+ 1
+
+
+
diff --git a/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/validationRules/Plugins_are_not_supported.validationRule-meta.xml b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/validationRules/Plugins_are_not_supported.validationRule-meta.xml
new file mode 100644
index 000000000..43ba9e255
--- /dev/null
+++ b/nebula-logger/main/plugin-framework/objects/LoggerSObjectHandlerPlugin__mdt/validationRules/Plugins_are_not_supported.validationRule-meta.xml
@@ -0,0 +1,11 @@
+
+
+ Plugins_are_not_supported
+ true
+ AND(
+ SObjectHandler__r.SObjectType__r.DeveloperName != 'Log',
+ SObjectHandler__r.SObjectType__r.DeveloperName != 'LogEntry'
+)
+ SObjectHandler__c
+ Plugins are not supported for this SObject Handler
+
diff --git a/nebula-logger/tests/log-management/classes/LogEntryEventHandler_Tests.cls b/nebula-logger/tests/log-management/classes/LogEntryEventHandler_Tests.cls
index 58ae3e4c5..26f3025c4 100644
--- a/nebula-logger/tests/log-management/classes/LogEntryEventHandler_Tests.cls
+++ b/nebula-logger/tests/log-management/classes/LogEntryEventHandler_Tests.cls
@@ -154,6 +154,11 @@ private class LogEntryEventHandler_Tests {
}
}
+ private static LoggerSObjectHandler__mdt getMockConfiguration() {
+ Schema.SObjectType sobjectType = Schema.LogEntryEvent__e.SObjectType;
+ return new LoggerSObjectHandler__mdt(IsEnabled__c = true, SObjectType__c = sobjectType.getDescribe().getName());
+ }
+
static void validateLogFields(LogEntryEvent__e logEntryEvent, Log__c log) {
Organization org = [SELECT Id, Name, InstanceName, IsSandbox, NamespacePrefix, OrganizationType, TrialExpirationDate FROM Organization];
User currentUser = getCurrentUser();
@@ -262,6 +267,46 @@ private class LogEntryEventHandler_Tests {
System.assertEquals(logEntryEvent.TriggerSObjectType__c, logEntry.TriggerSObjectType__c);
}
+ @isTest
+ static void it_should_return_the_logEntryEvent_sobjectType() {
+ Test.startTest();
+ System.assertEquals(Schema.LogEntryEvent__e.SObjectType, new LogEntryEventHandler().getSObjectType());
+ Test.stopTest();
+ }
+
+ @isTest
+ static void it_should_not_run_handler_when_disabled_via_configuration() {
+ LoggerSObjectHandler__mdt logEntryEventHandlerConfiguration = getMockConfiguration();
+ logEntryEventHandlerConfiguration.IsEnabled__c = false;
+
+ Map configurationBySObjectType = new Map{
+ Schema.LogEntryEvent__e.SObjectType => logEntryEventHandlerConfiguration
+ };
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.configurationBySObjectType = configurationBySObjectType;
+
+ LogEntryEvent__e logEntryEvent = new LogEntryEvent__e(
+ EpochTimestamp__c = System.now().getTime(),
+ Message__c = 'my message',
+ Timestamp__c = System.now(),
+ TransactionEntryNumber__c = 1,
+ TransactionId__c = '1234'
+ );
+ Database.SaveResult saveResult = EventBus.publish(logEntryEvent);
+
+ Test.stopTest();
+
+ System.assertEquals(true, saveResult.isSuccess(), saveResult.getErrors());
+
+ // Assumption: if the handler had executed, then a log record would have been created
+ // ...so if there are 0 logs, it indicates that the handler did not execute
+ Integer logCount = [SELECT COUNT() FROM Log__c];
+ System.assertEquals(0, logCount);
+ }
+
@isTest
static void it_should_gracefully_skip_execution_when_logEntryEvents_list_is_empty() {
List logEntryEvents = new List();
diff --git a/nebula-logger/tests/log-management/classes/LogEntryHandler_Tests.cls b/nebula-logger/tests/log-management/classes/LogEntryHandler_Tests.cls
index c96e4c2a0..e0bc1b6b1 100644
--- a/nebula-logger/tests/log-management/classes/LogEntryHandler_Tests.cls
+++ b/nebula-logger/tests/log-management/classes/LogEntryHandler_Tests.cls
@@ -4,6 +4,30 @@
//------------------------------------------------------------------------------------------------//
@isTest
private class LogEntryHandler_Tests {
+
+ public class LogEntryPluginTest extends LoggerSObjectHandlerPlugin {
+ public override void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ ) {
+ if (triggerOperationType == TriggerOperation.BEFORE_INSERT) {
+ for (LogEntry__c logEntry : (List) triggerNew) {
+ // The specific field changed doesn't really matter - we just want to ensure that whatever...
+ // ...logic implement in the instance of LoggerSObjectHandlerPlugin is executed
+ logEntry.Message__c = 'Some String';
+ }
+ }
+ }
+ }
+
+ private static LoggerSObjectHandler__mdt getMockConfiguration() {
+ Schema.SObjectType sobjectType = Schema.LogEntry__c.SObjectType;
+ return new LoggerSObjectHandler__mdt(IsEnabled__c = true, SObjectType__c = sobjectType.getDescribe().getName());
+ }
+
@testSetup
static void setupData() {
Log__c log = new Log__c(TransactionId__c = '1234');
@@ -11,6 +35,74 @@ private class LogEntryHandler_Tests {
Test.setCreatedDate(log.Id, System.now().addDays(-8));
}
+ @isTest
+ static void it_should_return_the_logEntry_sobjectType() {
+ Test.startTest();
+ System.assertEquals(Schema.LogEntry__c.SObjectType, new LogEntryHandler().getSObjectType());
+ Test.stopTest();
+ }
+
+ @isTest
+ static void it_should_load_a_default_config_when_no_config_present() {
+ String currentUserJson = JSON.serialize(new User(Id = UserInfo.getUserId()));
+
+ Log__c log = [SELECT Id FROM Log__c LIMIT 1];
+ LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = currentUserJson);
+
+ Test.startTest();
+
+ // Clear any CMDT records that have been loaded from the database
+ LoggerSObjectHandler.configurationBySObjectType.clear();
+ LoggerSObjectHandler__mdt configuration = LoggerSObjectHandler.configurationBySObjectType.get(Schema.LogEntry__c.SObjectType);
+ System.assertEquals(null, configuration);
+
+ insert logEntry;
+
+ // Verify that a default config has been loaded w/ IsEnabled__c = true
+ configuration = LoggerSObjectHandler.configurationBySObjectType.get(Schema.LogEntry__c.SObjectType);
+ System.assertNotEquals(null, configuration);
+ System.assertEquals(null, configuration.Id);
+ System.assertEquals(true, configuration.IsEnabled__c);
+
+ Test.stopTest();
+
+ // Assumption: the default config should have been loaded with IsEnabled__c = true, resulting in logEntry.HasRecordJson__c should have been auto-set to true...
+ // ...so if it's still false, it indicates that the handler did not execute
+ logEntry = [SELECT Id, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id];
+ System.assertNotEquals(null, logEntry.RecordJson__c);
+ System.assertEquals(true, logEntry.HasRecordJson__c);
+ }
+
+ @isTest
+ static void it_should_not_run_handler_when_disabled_via_configuration() {
+ String currentUserJson = JSON.serialize(new User(Id = UserInfo.getUserId()));
+
+ LoggerSObjectHandler__mdt logEntryHandlerConfiguration = getMockConfiguration();
+ logEntryHandlerConfiguration.IsEnabled__c = false;
+
+ Map configurationBySObjectType = new Map{
+ Schema.LogEntry__c.SObjectType => logEntryHandlerConfiguration
+ };
+
+ Log__c log = [SELECT Id FROM Log__c LIMIT 1];
+ LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, RecordJson__c = currentUserJson);
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.configurationBySObjectType = configurationBySObjectType;
+
+ insert logEntry;
+
+ Test.stopTest();
+
+ // Assumption: if the handler had executed, then logEntry.HasRecordJson__c should have been auto-set to true...
+ // ...so if it's still false, it indicates that the handler did not execute
+ logEntry = [SELECT Id, HasRecordJson__c, RecordJson__c FROM LogEntry__c WHERE Id = :logEntry.Id];
+ System.assertNotEquals(null, logEntry.RecordJson__c);
+ System.assertEquals(false, logEntry.HasRecordJson__c);
+ }
+
@isTest
static void it_should_save_log_entry_without_related_record_id() {
Log__c log = [SELECT Id FROM Log__c LIMIT 1];
@@ -200,4 +292,32 @@ private class LogEntryHandler_Tests {
System.assert(logEntry.HasStackTrace__c);
System.assertEquals(stackTrace, logEntry.StackTrace__c);
}
+
+ @isTest
+ static void it_should_run_apex_plugin_when_configured() {
+ String expectedMessage = 'Some String';
+
+ LoggerSObjectHandlerPlugin__mdt plugin = new LoggerSObjectHandlerPlugin__mdt(
+ PluginType__c = 'Apex',
+ PluginApiName__c = LogEntryPluginTest.class.getName()
+ );
+ Map> pluginsBySObjectType = new Map>{
+ Schema.LogEntry__c.SObjectType => new List{ plugin }
+ };
+
+ Log__c log = [SELECT Id FROM Log__c LIMIT 1];
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.pluginsBySObjectType = pluginsBySObjectType;
+
+ LogEntry__c logEntry = new LogEntry__c(Log__c = log.Id, Message__c = 'qwerty');
+ insert logEntry;
+
+ logEntry = [SELECT Id, Message__c FROM LogEntry__c WHERE Id = :logEntry.Id];
+ System.assertEquals(expectedMessage, logEntry.Message__c);
+
+ Test.stopTest();
+ }
}
diff --git a/nebula-logger/tests/log-management/classes/LogHandler_Tests.cls b/nebula-logger/tests/log-management/classes/LogHandler_Tests.cls
index cf8fc1d00..3aee61aa4 100644
--- a/nebula-logger/tests/log-management/classes/LogHandler_Tests.cls
+++ b/nebula-logger/tests/log-management/classes/LogHandler_Tests.cls
@@ -11,6 +11,34 @@ private class LogHandler_Tests {
private static final String FIRST_STATUS = Schema.Log__c.Status__c.getDescribe().getPicklistValues().get(0).getValue();
private static final String SECOND_STATUS = Schema.Log__c.Status__c.getDescribe().getPicklistValues().get(1).getValue();
+ public class LogPluginTest extends LoggerSObjectHandlerPlugin {
+ public override void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ ) {
+ if (triggerOperationType == TriggerOperation.BEFORE_INSERT) {
+ for (Log__c log : (List) triggerNew) {
+ // The specific field changed doesn't really matter - we just want to ensure that whatever...
+ // ...logic implement in the instance of LoggerSObjectHandlerPlugin is executed
+ log.ProfileName__c = 'Some String';
+ }
+ }
+ }
+ }
+
+ private static LoggerSObjectHandler__mdt getMockConfiguration() {
+ Schema.SObjectType sobjectType = Schema.Log__c.SObjectType;
+ return new LoggerSObjectHandler__mdt(IsEnabled__c = true, SObjectType__c = sobjectType.getDescribe().getName());
+ }
+
+ private static LoggerSObjectHandler__mdt getMockPlugin() {
+ Schema.SObjectType sobjectType = Schema.Log__c.SObjectType;
+ return new LoggerSObjectHandler__mdt(IsEnabled__c = true, SObjectType__c = sobjectType.getDescribe().getName());
+ }
+
@testSetup
static void setupData() {
Map logStatusByName = new Map();
@@ -23,6 +51,68 @@ private class LogHandler_Tests {
LogHandler.logStatusByName = logStatusByName;
}
+ @isTest
+ static void it_should_return_the_log_sobjectType() {
+ Test.startTest();
+ System.assertEquals(Schema.Log__c.SObjectType, new LogHandler().getSObjectType());
+ Test.stopTest();
+ }
+
+ @isTest
+ static void it_should_load_a_default_config_when_no_config_present() {
+ LogStatus__mdt closedLogStatus = [SELECT Id, MasterLabel FROM LogStatus__mdt WHERE IsActive__c = true AND IsClosed__c = true LIMIT 1];
+
+ Test.startTest();
+
+ // Clear any CMDT records that have been loaded from the database
+ LoggerSObjectHandler.configurationBySObjectType.clear();
+ LoggerSObjectHandler__mdt configuration = LoggerSObjectHandler.configurationBySObjectType.get(Schema.Log__c.SObjectType);
+ System.assertEquals(null, configuration);
+
+ Log__c log = new Log__c(LoggedBy__c = UserInfo.getUserId(), Status__c = closedLogStatus.MasterLabel, TransactionId__c = '1234');
+ insert log;
+
+ // Verify that a default config has been loaded w/ IsEnabled__c = true
+ configuration = LoggerSObjectHandler.configurationBySObjectType.get(Schema.Log__c.SObjectType);
+ System.assertNotEquals(null, configuration);
+ System.assertEquals(null, configuration.Id);
+ System.assertEquals(true, configuration.IsEnabled__c);
+
+ Test.stopTest();
+
+ // Assumption: the default config should have been loaded with IsEnabled__c = true, resulting in log.IsClosed__c being auto-set to true...
+ // ...so if it's true, it indicates that the handler executed with a default config
+ log = [SELECT Id, IsClosed__c FROM Log__c WHERE Id = :log.Id];
+ System.assertEquals(true, log.IsClosed__c);
+ }
+
+ @isTest
+ static void it_should_not_run_handler_when_disabled_via_configuration() {
+ LoggerSObjectHandler__mdt logHandlerConfiguration = getMockConfiguration();
+ logHandlerConfiguration.IsEnabled__c = false;
+
+ Map configurationBySObjectType = new Map{
+ Schema.Log__c.SObjectType => logHandlerConfiguration
+ };
+
+ LogStatus__mdt closedLogStatus = [SELECT Id, MasterLabel FROM LogStatus__mdt WHERE IsActive__c = true AND IsClosed__c = true LIMIT 1];
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.configurationBySObjectType = configurationBySObjectType;
+
+ Log__c log = new Log__c(LoggedBy__c = UserInfo.getUserId(), Status__c = closedLogStatus.MasterLabel, TransactionId__c = '1234');
+ insert log;
+
+ Test.stopTest();
+
+ // Assumption: if the handler had executed, then log.IsClosed__c should have been auto-set to true...
+ // ...so if it's still false, it indicates that the handler did not execute
+ log = [SELECT Id, IsClosed__c FROM Log__c WHERE Id = :log.Id];
+ System.assertEquals(false, log.IsClosed__c);
+ }
+
@isTest
static void it_should_set_org_release_cycle_on_insert() {
Set previewInstances = new Set{
@@ -401,4 +491,30 @@ private class LogHandler_Tests {
System.assertEquals(0, logShares.size(), logShares);
}
+
+ @isTest
+ static void it_should_run_apex_plugin_when_configured() {
+ String expectedProfileName = 'Some String';
+
+ LoggerSObjectHandlerPlugin__mdt plugin = new LoggerSObjectHandlerPlugin__mdt(
+ PluginType__c = 'Apex',
+ PluginApiName__c = LogPluginTest.class.getName()
+ );
+ Map> pluginsBySObjectType = new Map>{
+ Schema.Log__c.SObjectType => new List{ plugin }
+ };
+
+ Test.startTest();
+
+ // Use the mock configurations
+ LoggerSObjectHandler.pluginsBySObjectType = pluginsBySObjectType;
+
+ Log__c log = new Log__c(LoggedBy__c = UserInfo.getUserId(), TransactionId__c = '1234');
+ insert log;
+
+ log = [SELECT Id, ProfileName__c FROM Log__c WHERE Id = :log.Id];
+ System.assertEquals(expectedProfileName, log.ProfileName__c);
+
+ Test.stopTest();
+ }
}
diff --git a/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls b/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls
new file mode 100644
index 000000000..b2b89b8be
--- /dev/null
+++ b/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls
@@ -0,0 +1,42 @@
+//------------------------------------------------------------------------------------------------//
+// 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. //
+//------------------------------------------------------------------------------------------------//
+
+@isTest
+private class LoggerSObjectHandlerPlugin_Tests {
+ private static String PLUGIN_LOG_STATUS = 'On Hold';
+
+ public class ExamplePlugin extends LoggerSObjectHandlerPlugin {
+ public override void execute(
+ TriggerOperation triggerOperationType,
+ List triggerNew,
+ Map triggerNewMap,
+ List triggerOld,
+ Map triggerOldMap
+ ) {
+ switch on triggerOperationType {
+ when BEFORE_INSERT {
+ for (Log__c log : (List) triggerNew) {
+ log.Status__c = PLUGIN_LOG_STATUS;
+ }
+ }
+ }
+ }
+ }
+
+ @isTest
+ static void it_should_execute_plugin_logic() {
+ Log__c log = new Log__c(TransactionId__c = '1234');
+ System.assertEquals(null, log.Status__c);
+
+ Test.startTest();
+
+ ExamplePlugin plugin = new ExamplePlugin();
+ plugin.execute(triggerOperation.BEFORE_INSERT, new List{ log }, null, null, null);
+
+ Test.stopTest();
+
+ System.assertEquals(PLUGIN_LOG_STATUS, log.Status__c);
+ }
+}
diff --git a/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls-meta.xml b/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls-meta.xml
new file mode 100644
index 000000000..482559c8b
--- /dev/null
+++ b/nebula-logger/tests/plugin-framework/classes/LoggerSObjectHandlerPlugin_Tests.cls-meta.xml
@@ -0,0 +1,5 @@
+
+
+ 51.0
+ Active
+
diff --git a/package.json b/package.json
index d87f71517..d719ecda4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "nebula-logger",
- "version": "4.4.6",
+ "version": "4.5.0",
"description": "Designed for Salesforce admins, developers & architects. A robust logger for Apex, Flow, Process Builder & Integrations.",
"scripts": {
"deploy": "npm run deploy:logger && npm run deploy:managedpackage && npm run deploy:extratests",
@@ -21,10 +21,8 @@
"org:delete:noprompt": "sfdx force:org:delete --json --noprompt",
"org:details": "sfdx force:org:display --json --verbose",
"org:open": "sfdx force:org:open",
- "package:version:create:managed": "pwsh .scripts/switch-to-managed-package-project-json.ps1 && sfdx force:package:version:create --json --package \"Nebula Logger - Managed Package\" --codecoverage --installationkeybypass --wait 30 && pwsh ./scripts/restore-unlocked-package-project-json.ps1",
- "package:version:create:managed:skipvalidation": "pwsh ./scripts/switch-to-managed-package-project-json.ps1 && sfdx force:package:version:create --json --package \"Nebula Logger - Managed Package\" --skipvalidation --installationkeybypass --wait 30 && pwsh ./scripts/restore-unlocked-package-project-json.ps1",
+ "package:version:create:managed": "pwsh ./scripts/switch-to-managed-package-project-json.ps1 && sfdx force:package:version:create --json --package \"Nebula Logger - Managed Package\" --codecoverage --installationkeybypass --wait 30 && pwsh ./scripts/restore-unlocked-package-project-json.ps1",
"package:version:create:unlocked": "sfdx force:package:version:create --json --package \"Nebula Logger - Unlocked Package\" --codecoverage --installationkeybypass --wait 30",
- "package:version:create:unlocked:skipvalidation": "sfdx force:package:version:create --json --package \"Nebula Logger - Unlocked Package\" --skipvalidation --installationkeybypass --wait 30",
"package:version:list": "sfdx force:package:version:list --json --verbose --orderby PatchVersion",
"package:version:list:released": "sfdx force:package:version:list --json --verbose --released --orderby PatchVersion",
"package:version:list:managed": "pwsh ./scripts/switch-to-managed-package-project-json.ps1 && sfdx force:package:version:list --json --verbose --orderby PatchVersion --packages \"Nebula Logger - Managed Package\" && pwsh ./scripts/restore-unlocked-package-project-json.ps1",
diff --git a/packages/managed-package/sfdx-project.json b/packages/managed-package/sfdx-project.json
index a4064b032..9426bbdc8 100644
--- a/packages/managed-package/sfdx-project.json
+++ b/packages/managed-package/sfdx-project.json
@@ -9,10 +9,10 @@
"default": true,
"definitionFile": "config/project-scratch-def.json",
"postInstallScript": "LoggerInstallHandler",
- "versionName": "Configurable Default Save Method & New Epoch Timestamp Fields",
- "versionNumber": "4.4.0.2",
- "ancestorVersion": "4.3.0.3",
- "versionDescription": "Includes new Logger Setting for the default save method, added new Epoch Timestamp fields and bugfixes",
+ "versionName": "Code Stability & Bugfixes",
+ "versionNumber": "4.5.0.0",
+ "ancestorVersion": "4.4.0.2",
+ "versionDescription": "Code stability & bugfixes for the managed package. Unlocked package only: new logger plugin framework",
"releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases"
}
],
@@ -21,6 +21,7 @@
"Nebula Logger - Managed Package@4.0.0-9-managed-package-release": "04t5Y000000XJZ7QAO",
"Nebula Logger - Managed Package@4.2.0-0-more-fields-and-methods": "04t5Y000000Xg4wQAC",
"Nebula Logger - Managed Package@4.3.0-3-logger-console-app": "04t5Y000000YLDLQA4",
- "Nebula Logger - Managed Package@4.4.0-2-config-default-save-method": "04t5Y0000027FFgQAM"
+ "Nebula Logger - Managed Package@4.4.0-2-config-default-save-method": "04t5Y0000027FFgQAM",
+ "Nebula Logger - Managed Package@4.5.0-0-logger-plugin-framework": "04t5Y0000027FMhQAM"
}
-}
\ No newline at end of file
+}
diff --git a/scripts/generate-docs.ps1 b/scripts/generate-docs.ps1
index 4b7d88c58..0af7fb704 100644
--- a/scripts/generate-docs.ps1
+++ b/scripts/generate-docs.ps1
@@ -1,5 +1,5 @@
# This script is used to generate the markdown files used by Github pages
-npx apexdocs-generate --configPath config/apexdocs.json --scope global public --sourceDir nebula-logger/main --targetDir docs
+npx apexdocs-generate --configPath config/apexdocs.json --scope global public --sourceDir nebula-logger/ --targetDir docs
# Make a few adjustments to the generated markdown files so that they work correctly in Github Pages
$indexPageFile = "docs/index.md"
@@ -7,6 +7,7 @@ Write-Output "Processing file: $indexPageFile"
(Get-Content -path $indexPageFile -Raw) -replace ".md","" | Set-Content -Path $indexPageFile -NoNewline
(Get-Content -path $indexPageFile -Raw) -replace "/Logger-Engine/","logger-engine/" | Set-Content -Path $indexPageFile -NoNewline
(Get-Content -path $indexPageFile -Raw) -replace "/Log-Management/","log-management/" | Set-Content -Path $indexPageFile -NoNewline
+(Get-Content -path $indexPageFile -Raw) -replace "/Plugin-Framework/","plugin-framework/" | Set-Content -Path $indexPageFile -NoNewline
$docsSubdirectories = "docs/*/*.*"
foreach($file in Get-ChildItem $docsSubdirectories) {
@@ -14,6 +15,7 @@ foreach($file in Get-ChildItem $docsSubdirectories) {
(Get-Content -path $file -Raw) -replace ".md","" | Set-Content -Path $file -NoNewline
(Get-Content -path $file -Raw) -replace "/Logger-Engine/","" | Set-Content -Path $file -NoNewline
(Get-Content -path $file -Raw) -replace "/Log-Management/","" | Set-Content -Path $file -NoNewline
+ (Get-Content -path $file -Raw) -replace "/Plugin-Framework/","" | Set-Content -Path $file -NoNewline
}
prettier ./docs --write
diff --git a/sfdx-project.json b/sfdx-project.json
index 8b4fbe7b6..5dfd48e0c 100644
--- a/sfdx-project.json
+++ b/sfdx-project.json
@@ -3,15 +3,36 @@
"sfdcLoginUrl": "https://login.salesforce.com",
"sourceApiVersion": "51.0",
"packageDirectories": [
+ {
+ "path": "force-app",
+ "default": true
+ },
{
"package": "Nebula Logger - Unlocked Package",
"path": "nebula-logger",
- "default": true,
+ "default": false,
"definitionFile": "config/project-scratch-def-with-experience-cloud.json",
- "versionName": "New save method SYNCHRONOUS_DML",
- "versionNumber": "4.4.6.0",
- "versionDescription": "The new save method skips publishing platform events and instead immediately creates Log__c and LogEntry__c records. Use cautiously!",
+ "versionName": "Logger Plugin Framework",
+ "versionNumber": "4.5.0.0",
+ "versionDescription": "Easily build or install plugins that enhance the Log__c and LogEntry__c objects, using Apex or Flow",
"releaseNotesUrl": "https://github.com/jongpie/NebulaLogger/releases"
+ },
+ {
+ "package": "Nebula Logger Plugin - Slack",
+ "path": "nebula-logger-plugins/Slack",
+ "dependencies": [
+ {
+ "package": "Nebula Logger - Unlocked Package@4.5.0-0-logger-plugin-framework"
+ }
+ ],
+ "versionName": "Beta Release",
+ "versionNumber": "0.9.0.0",
+ "versionDescription": "New beta plugin for Nebula Logger",
+ "default": false
+ },
+ {
+ "path": "extra-tests",
+ "default": false
}
],
"packageAliases": {
@@ -21,6 +42,9 @@
"Nebula Logger - Unlocked Package@4.4.3-0-guest-user-bugfix": "04t5Y0000027FI1QAM",
"Nebula Logger - Unlocked Package@4.4.4-0-timestamp-bugfix": "04t5Y0000027FIQQA2",
"Nebula Logger - Unlocked Package@4.4.5-0-log-batch-purger-bugfixes": "04t5Y0000027FIVQA2",
- "Nebula Logger - Unlocked Package@4.4.6-0-new-save-method-synchronous_dml": "04t5Y0000027FJdQAM"
+ "Nebula Logger - Unlocked Package@4.4.6-0-new-save-method-synchronous_dml": "04t5Y0000027FJdQAM",
+ "Nebula Logger - Unlocked Package@4.5.0-0-logger-plugin-framework": "04t5Y0000027FMrQAM",
+ "Nebula Logger Plugin - Slack": "0Ho5e000000oM3pCAE",
+ "Nebula Logger Plugin - Slack@0.9.0-0-beta-release": "04t5e00000061lHAAQ"
}
}
\ No newline at end of file