Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
2d5be9e
temporary - adding package aliases to outer project file
jasonsiders Aug 6, 2025
3ea78ee
promoting DatabaseLayerTestUtils to global, adding method to inject s…
jasonsiders Aug 6, 2025
8903806
checkpoint - refactoring DmlTest to use new plugin utility method
jasonsiders Aug 6, 2025
79edb6a
adding new plugin method to DmlTest
jasonsiders Aug 6, 2025
724af92
checkpoint - creating failing tests, still missing way of adding the …
jasonsiders Aug 6, 2025
4547899
linking frameworks
jasonsiders Aug 6, 2025
ff36030
adding utility method to Soql tests
jasonsiders Aug 7, 2025
41fc18a
cleanup
jasonsiders Aug 7, 2025
a302900
adding new method to make it easier to instantiate plugins
jasonsiders Aug 7, 2025
db10d55
checkpoint - reworked tests
jasonsiders Aug 7, 2025
d99352a
adding comments
jasonsiders Aug 7, 2025
e8b58bd
all tests passing
jasonsiders Aug 7, 2025
b5bcc6a
adding constructor to ignore database layer's internal framework
jasonsiders Aug 7, 2025
95030ac
fixed issue with plugin initialization
jasonsiders Aug 7, 2025
2a830a8
adjusted log messages; ran prettier
jasonsiders Aug 7, 2025
59b111c
cleaning up log messages
jasonsiders Aug 8, 2025
1989b8b
continuing refactoring messages
jasonsiders Aug 8, 2025
eddc9e5
contd.
jasonsiders Aug 8, 2025
f2c8058
refactoring
jasonsiders Aug 8, 2025
aafa998
Finalizing logging behavior
jasonsiders Aug 8, 2025
2131c65
patching test coverage gaps
jasonsiders Aug 8, 2025
e147222
fixing failing test
jasonsiders Aug 8, 2025
6312489
prevent logging empty log lines
jasonsiders Aug 8, 2025
325279a
fixing test
jasonsiders Aug 8, 2025
d55fefe
cleanup
jasonsiders Aug 8, 2025
a96d2db
restructuring
jasonsiders Aug 8, 2025
ed50afd
restructuring project folders
jasonsiders Aug 8, 2025
3256785
adding missing elipses
jasonsiders Aug 8, 2025
9d6955a
simplified addLine behavior
jasonsiders Aug 9, 2025
b5800e6
adding readme
jasonsiders Aug 9, 2025
152b0e0
capitalizing SOQL plugin field to match
jasonsiders Aug 9, 2025
93e82d6
bumping nebula dependency to 4.16.4
jasonsiders Aug 9, 2025
e20ec88
updated alias
jasonsiders Aug 9, 2025
95c1995
Merge branch 'main' into nebula-plugin
jasonsiders Aug 9, 2025
30d7c70
updating dependency
jasonsiders Aug 9, 2025
d532b1e
packaged v1.0.0 of nebula logger package
jasonsiders Aug 9, 2025
536057d
enable claude PR action to run on changes outside of `source/`
jasonsiders Aug 9, 2025
40e155e
Revert "enable claude PR action to run on changes outside of `source/`"
jasonsiders Aug 9, 2025
33e6dfc
simpler approach
jasonsiders Aug 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ on:
- main
paths:
- "source/**"
- "plugins/nebula-logger/source/**"
types: [opened, ready_for_review, reopened, synchronize]

concurrency:
Expand Down
95 changes: 95 additions & 0 deletions plugins/nebula-logger/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
This plugin leverages the [Plugin Framework](https://github.com/jasonsiders/apex-database-layer/wiki/The-Plugin-Framework) to automatically logs details about your DML and SOQL operations, via _Nebula Logger_.

[Nebula Logger](https://github.com/jongpie/NebulaLogger/tree/main) is a popular logging framework for Salesforce. Like Apex Database Layer, it's free, and open-source.

## Getting Started

### Prerequisites
To use this plugin, you must have the most recent version of [Apex Database Layer](https://github.com/jasonsiders/apex-database-layer) and [Nebula Logger](https://github.com/jongpie/NebulaLogger/tree/main) installed.

### Installation

If you're using both the latest & unmanaged versions of _Apex Database Layer_ and _Nebula Logger_ installed, you may install this plugin as an unlocked package.

First, locate the latest version of the plugin package, called `nebula-logger-plugin@latest` in [`sfdx-project.json`](https://github.com/jasonsiders/apex-database-layer/blob/main/sfdx-project.json):

```sh
sf package install --package <<package_version_id>> --wait 10
```

> :warning: **Note:** If you are using a managed version of _Apex Database Layer_ and/or _Nebula Logger_, you won't be able to formally install the package. Instead, manually copy the contents of these two Apex Classes in your desired environment:
>
>- [`DatabaseLayerNebulaLoggerAdapter.cls`](https://github.com/jasonsiders/apex-database-layer/blob/main/plugins/nebula-logger/source/classes/DatabaseLayerNebulaLoggerAdapter.cls)
>- [`DatabaseLayerNebulaLoggerAdapterTest.cls`](https://github.com/jasonsiders/apex-database-layer/blob/main/plugins/nebula-logger/source/classes/DatabaseLayerNebulaLoggerAdapterTest.cls))

### Setup

Once installed, navigate to `Setup > Custom Metadata > Database Layer Settings`. If a record already exists, use that record. Else, create a new record, called "Default".

Set the Custom Metadata record's _DML: Pre & Post Processor_ and _SOQL: Pre & Post Processor_ fields to be the name of the Apex class: `DatabaseLayerNebulaLoggerAdapter`:

<img width="1178" alt="image" src="https://github.com/user-attachments/assets/db8a5ed1-453f-4c91-bf88-d4c911579669" />

**Note:** Once configured, this custom metadata record won't be altered by upgrading the _Apex Database Layer_ package, or the plugin package itself.

---

## Usage

Whenever a DML or SOQL operation runs, the plugin will log the details of those operations to Nebula Logger. This results in log entries with the `apex-database-layer` _Log Entry Tag_.

### DML Logging
Just before a DML operation is processed, the plugin will issue a `FINEST` log entry summarizing the action that's about to take place.
- The [`Dml.Request`](https://github.com/jasonsiders/apex-database-layer/wiki/The-Dml.Request-Class) is serialized and shown in the message body
- The records being operated on are shown in the `Related Records` tab

<img width="1400" alt="image" src="https://github.com/user-attachments/assets/d9e8cfd1-5f87-4a0e-ad60-abe0593902fe" />
<img width="1400" alt="image" src="https://github.com/user-attachments/assets/04beeed6-ddbf-476e-84ee-2aaf3a029a9a" />

After a DML operation is processed, the plugin issues another `FINEST` log entry summarizing the action that took place.

- The [`Dml.Request`](https://github.com/jasonsiders/apex-database-layer/wiki/The-Dml.Request-Class) is serialized and shown in the message body
- The records that were operated on are shown in the `Related Records` tab
- The relevant database result objects (ex., `Database.SaveResult`) are shown in the `Related Records` tab

<img width="1400" alt="image" src="https://github.com/user-attachments/assets/f4f18658-2021-4d14-941d-30394a984ba3" />
<img width="1402" alt="image" src="https://github.com/user-attachments/assets/975e8713-9300-41f3-be4d-0b8b276f2e89" />

If an exception is thrown during a DML operation, an `ERROR` log entry is issued:

- The `Exception` message is shown in the message body
- The [`Dml.Request`](https://github.com/jasonsiders/apex-database-layer/wiki/The-Dml.Request-Class) is serialized and shown in the message body
- The records that were operated on are shown in the `Related Records` tab

<img width="1403" alt="image" src="https://github.com/user-attachments/assets/9c3ff70f-e955-4b5c-bd49-b8cabdf3a5dd" />

### SOQL Logging

Just before a SOQL operation is processed, the plugin issues a `FINEST` log entry summarizing the query about to take place:

- The text of the query is available in the message body

<img width="1429" alt="image" src="https://github.com/user-attachments/assets/fa66b1a5-3aea-41e3-b27d-abe183694548" />

After a SOQL operation is processed, the plugin issues another `FINEST` log entry summarizing the query and its results:

- The text of the query is available in the message body
- The resulting SObject records are available in the `Related Records` tab
- Note: Other query operations (ex., `getCursor`, `countQuery`) that do _not_ output SObjects will be printed in the message body instead

<img width="1431" alt="image" src="https://github.com/user-attachments/assets/51b574ed-ef2f-42ef-b97d-96f965290982" />
<img width="1406" alt="image" src="https://github.com/user-attachments/assets/e37a5a19-72b0-4701-aa17-06c16343b034" />

If an exception is thrown during a SOQL operation, an `ERROR` log entry is issued:

- The text of the query is available in the message body
- The `Exception` message is shown in the message body

<img width="1402" alt="image" src="https://github.com/user-attachments/assets/2e879830-1a85-4e7f-881b-dc8f154aeb0b" />

### Considerations

#### `MockSoql`: Additional query logs for non-standard SOQL operations
Many `MockSoql` query operations use the `query` method as the basis for building mock results. This may result in additional logs being issued.

For example, `MockSoql.getQueryLocator` calls `MockSoql.query` to generate the list of records to be returned, and then wraps the results in a `Soql.QueryLocator`. In this scenario, the plugin issues 4 `FINEST` logs: one before/after `MockSoql.getQueryLocator`, and one before/after `MockSoql.query`.
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
/**
* @description Adapter class that integrates Nebula Logger with the Apex Database Layer framework.
* Implements both DML and SOQL pre/post processors to log database operations.
*/
@SuppressWarnings('PMD.AvoidGlobalModifier')
global class DatabaseLayerNebulaLoggerAdapter implements Dml.PreAndPostProcessor, Soql.PreAndPostProcessor {
@TestVisible
private static final String TAG_NAME = 'apex-database-layer';
private static final String DELIMITER = '\n---\n';
private static final Boolean ORIGINAL_DEBUG_FLAG_VALUE;
private static final LoggerSettings__c SETTINGS;

static {
SETTINGS = Logger.getUserSettings();
ORIGINAL_DEBUG_FLAG_VALUE = SETTINGS?.IsApexSystemDebugLoggingEnabled__c ?? false;
}

/**
* @description Configure Nebula Logger to ignore this framework's internal classes in log stack traces
*/
global DatabaseLayerNebulaLoggerAdapter() {
Logger.ignoreOrigin(DatabaseLayerNebulaLoggerAdapter.class);
Logger.ignoreOrigin(DatabaseLayer.class);
Logger.ignoreOrigin(DatabaseLayerUtils.class);
Logger.ignoreOrigin(DatabaseLayerTestUtils.class);
Logger.ignoreOrigin(Dml.class);
Logger.ignoreOrigin(MockDml.class);
Logger.ignoreOrigin(MockRecord.class);
Logger.ignoreOrigin(MockSoql.class);
Logger.ignoreOrigin(Soql.class);
}

/**
* @description Logs DML requests before execution
* @param request The DML request to be processed
*/
global void processPreDml(Dml.Request request) {
String op = this.getDmlOperationName(request?.operation);
this.log(System.LoggingLevel.FINEST)
?.addLine('⏳ DML: Processing ' + op + '...')
?.addLine('Request:\n' + JSON.serialize(request))
?.setRecords(request?.records)
?.build();
}

/**
* @description Logs a successful DML operation
* @param request The DML request that was processed
* @param results The Database.*Results returned from the DML operation
*/
global void processPostDml(Dml.Request request, List<Object> results) {
String op = this.getDmlOperationName(request?.operation);
this.log(System.LoggingLevel.FINEST)
?.addLine('✅ DML: Processed ' + op)
?.addLine('Request: ' + JSON.serialize(request))
?.setDatabaseResults(results)
?.setRecords(request?.records)
?.build();
}

/**
* @description Logs DML errors
* @param request The DML request that encountered an error
* @param error The exception that was thrown
*/
global void processDmlError(Dml.Request request, Exception error) {
String op = this.getDmlOperationName(request?.operation);
this.log(System.LoggingLevel.ERROR)
?.addLine('🚨 DML: Error processing ' + op)
?.addLine(error?.toString())
?.addLine('Request:\n' + JSON.serialize(request))
?.setRecords(request?.records)
?.build();
}

/**
* @description Logs SOQL requests before execution.
* @param request The SOQL request being processed
*/
global void processPreSoql(Soql.Request request) {
this.log(System.LoggingLevel.FINEST)
?.addLine('⏳ SOQL: Processing ' + request?.operation + '...')
?.addLine(request?.queryString)
?.build();
}

/**
* @description Logs SOQL requests after execution.
* @param request The SOQL request that was processed
* @param results The results from the SOQL operation
*/
global void processPostSoql(Soql.Request request, Object results) {
this.log(System.LoggingLevel.FINEST)
?.addLine('✅ SOQL: Processed ' + request?.operation)
?.addLine(request?.queryString)
?.setRecords(results)
?.build();
}

/**
* @description Logs SOQL errors.
* @param request The SOQL request that failed
* @param error The exception that occurred
*/
global void processSoqlError(Soql.Request request, Exception error) {
this.log(System.LoggingLevel.ERROR)
?.addLine('🚨 SOQL: Error processing ' + request?.operation)
?.addLine(request?.queryString)
?.addLine(error?.toString())
?.build();
}

// **** PRIVATE **** //
private String getDmlOperationName(Dml.Operation operation) {
return operation?.name()?.removeStart('DO_');
}

private LogBuilder log(System.LoggingLevel level) {
return new DatabaseLayerNebulaLoggerAdapter.LogBuilder(level);
}

// **** INNER **** //
private class LogBuilder {
private LogEntryEventBuilder entry;
private List<String> lines;

/**
* @description Creates a new LogBuilder instance with the specified logging level
* @param level The logging level for this log entry
*/
public LogBuilder(System.LoggingLevel level) {
this.createLogEntry(level);
this.lines = new List<String>{};
}

/**
* @description Adds a line to the log message
* @param line The line to add to the log message
* @return This LogBuilder instance for method chaining
*/
public LogBuilder addLine(String line) {
this.lines?.add(line);
return this;
}

/**
* @description Builds the final log entry with all accumulated lines
* @return The LogEntryEventBuilder with the complete log message
*/
public LogEntryEventBuilder build() {
String msg = String.join(this.lines, DELIMITER);
return this.entry?.setMessage(msg);
}

/**
* @description Sets database operation results on the log entry
* @param obj List of database results (DeleteResult, SaveResult, etc.)
* @return This LogBuilder instance for method chaining
*/
public LogBuilder setDatabaseResults(List<Object> obj) {
if (obj instanceof List<Database.DeleteResult>) {
List<Database.DeleteResult> databaseResults = (List<Database.DeleteResult>) obj;
this.entry?.setDatabaseResult(databaseResults);
} else if (obj instanceof List<Database.LeadConvertResult>) {
List<Database.LeadConvertResult> databaseResults = (List<Database.LeadConvertResult>) obj;
this.entry?.setDatabaseResult(databaseResults);
} else if (obj instanceof List<Database.SaveResult>) {
List<Database.SaveResult> databaseResults = (List<Database.SaveResult>) obj;
this.entry?.setDatabaseResult(databaseResults);
} else if (obj instanceof List<Database.UndeleteResult>) {
List<Database.UndeleteResult> databaseResults = (List<Database.UndeleteResult>) obj;
this.entry?.setDatabaseResult(databaseResults);
} else if (obj instanceof List<Database.UpsertResult>) {
List<Database.UpsertResult> databaseResults = (List<Database.UpsertResult>) obj;
this.entry?.setDatabaseResult(databaseResults);
}
return this;
}

/**
* @description Sets records or query results on the log entry
* @param obj Records to log (SObject list, QueryLocator, or other results)
* @return This LogBuilder instance for method chaining
*/
public LogBuilder setRecords(Object obj) {
if (obj instanceof List<SObject>) {
List<SObject> records = (List<SObject>) obj;
this.entry?.setRecord(records);
} else if (obj instanceof Soql.QueryLocator) {
Soql.QueryLocator locator = (Soql.QueryLocator) obj;
this.lines?.add('Results:\n' + locator);
} else {
this.lines?.add('Results:\n' + JSON.serialize(obj));
}
return this;
}

private void createLogEntry(System.LoggingLevel level) {
// Create a basic/empty log entry object for the builder to hydrate with log lines & other information
// Special care must be taken to prevent committing unnecessary '' log lines to System.debug logs:
SETTINGS.IsApexSystemDebugLoggingEnabled__c = false;
this.entry = Logger.newEntry(level, '')?.addTag(TAG_NAME);
SETTINGS.IsApexSystemDebugLoggingEnabled__c = ORIGINAL_DEBUG_FLAG_VALUE;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>64.0</apiVersion>
<status>Active</status>
</ApexClass>
Loading