diff --git a/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls b/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls new file mode 100644 index 0000000..7491d34 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls @@ -0,0 +1,42 @@ +/* + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @group Trigger Actions Framework + * @description Interface for telemetry logging implementations within the Trigger Actions Framework. + * + * This interface allows pluggable telemetry logging functionality that can be used to track + * limits, performance metrics, and other diagnostic information during trigger execution. + * + * Implementations can range from simple System.debug logging to integration with external + * logging frameworks like Nebula Logger. + * + * Example implementation: + * ``` + * public class LimitLogger implements ITriggerActionTelemetryLogger { + * public void log(TelemetryContext context) { + * System.debug('Queries: ' + Limits.getQueries() + '/' + Limits.getLimitQueries()); + * } + * } + * ``` + */ +public interface ITriggerActionTelemetryLogger { + /** + * @description Log telemetry information using the provided context. + * @param context The telemetry context containing information about the current execution state + */ + void log(TelemetryContext context); +} \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls-meta.xml b/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls-meta.xml new file mode 100644 index 0000000..835ede4 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/ITriggerActionTelemetryLogger.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/LimitLogger.cls b/trigger-actions-framework/main/default/classes/LimitLogger.cls new file mode 100644 index 0000000..0c4766f --- /dev/null +++ b/trigger-actions-framework/main/default/classes/LimitLogger.cls @@ -0,0 +1,59 @@ +/* + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @group Trigger Actions Framework + * @description Basic implementation of ITriggerActionTelemetryLogger that logs Salesforce limits + * to the debug log. This implementation tracks SOQL queries, DML statements, CPU time, and heap size. + * + * This logger is designed for debugging and development purposes and should not be enabled + * in production environments as it may impact performance. + * + * Example usage: + * Configure this class in the Trigger_Action__mdt.Telemetry_Logger_Class__c field to enable + * telemetry logging for specific trigger actions. + */ +public class LimitLogger implements ITriggerActionTelemetryLogger { + + /** + * @description Logs current Salesforce limits to the debug log with context information. + * @param context The telemetry context containing execution information + */ + public void log(TelemetryContext context) { + String phaseLabel = context.phase == TelemetryPhase.START ? 'START' : 'FINISH'; + + String message = String.format( + '[{0}] {1} on {2}.{3} ({4} records) - Queries: {5}/{6}, DML: {7}/{8}, CPU: {9}/{10}ms, Heap: {11}/{12}bytes', + new List{ + phaseLabel, + context.triggerOperation, + context.sObjectType, + context.actionClassName, + String.valueOf(context.recordCount), + String.valueOf(Limits.getQueries()), + String.valueOf(Limits.getLimitQueries()), + String.valueOf(Limits.getDmlStatements()), + String.valueOf(Limits.getLimitDmlStatements()), + String.valueOf(Limits.getCpuTime()), + String.valueOf(Limits.getLimitCpuTime()), + String.valueOf(Limits.getHeapSize()), + String.valueOf(Limits.getLimitHeapSize()) + } + ); + + System.debug(LoggingLevel.DEBUG, message); + } +} \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/LimitLogger.cls-meta.xml b/trigger-actions-framework/main/default/classes/LimitLogger.cls-meta.xml new file mode 100644 index 0000000..835ede4 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/LimitLogger.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls b/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls new file mode 100644 index 0000000..6bc6509 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls @@ -0,0 +1,85 @@ +/* + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @group Trigger Actions Framework + * @description Test class for the LimitLogger telemetry implementation. + */ +@IsTest +private class LimitLoggerTest { + + @IsTest + static void testLogWithStartPhase() { + TelemetryContext context = new TelemetryContext(); + context.actionClassName = 'TestTriggerAction'; + context.triggerOperation = 'BEFORE_INSERT'; + context.sObjectType = 'Account'; + context.recordCount = 5; + context.phase = TelemetryPhase.START; + + LimitLogger logger = new LimitLogger(); + + Test.startTest(); + logger.log(context); + Test.stopTest(); + + // Since LimitLogger uses System.debug, we can't directly assert the output + // but we can verify the method executed without exceptions + System.assert(true, 'LimitLogger.log should execute without throwing exceptions'); + } + + @IsTest + static void testLogWithEndPhase() { + TelemetryContext context = new TelemetryContext(); + context.actionClassName = 'TestTriggerAction'; + context.triggerOperation = 'AFTER_UPDATE'; + context.sObjectType = 'Contact'; + context.recordCount = 10; + context.phase = TelemetryPhase.FINISH; + + LimitLogger logger = new LimitLogger(); + + Test.startTest(); + logger.log(context); + Test.stopTest(); + + System.assert(true, 'LimitLogger.log should execute without throwing exceptions'); + } + + @IsTest + static void testLogWithZeroRecords() { + TelemetryContext context = new TelemetryContext(); + context.actionClassName = 'TestTriggerAction'; + context.triggerOperation = 'BEFORE_DELETE'; + context.sObjectType = 'Opportunity'; + context.recordCount = 0; + context.phase = TelemetryPhase.START; + + LimitLogger logger = new LimitLogger(); + + Test.startTest(); + logger.log(context); + Test.stopTest(); + + System.assert(true, 'LimitLogger.log should handle zero records without throwing exceptions'); + } + + @IsTest + static void testImplementsInterface() { + ITriggerActionTelemetryLogger logger = new LimitLogger(); + System.assert(logger != null, 'LimitLogger should implement ITriggerActionTelemetryLogger interface'); + } +} \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls-meta.xml b/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls-meta.xml new file mode 100644 index 0000000..835ede4 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/LimitLoggerTest.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls index a4fa549..5a3b5e1 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandler.cls @@ -95,7 +95,9 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem '{0}__r.TriggerRecord_Class_Name__c,', 'Allow_Flow_Recursion__c,', '{0}__r.Bypass_Permission__c,', - '{0}__r.Required_Permission__c', + '{0}__r.Required_Permission__c,', + 'Enable_Telemetry__c,', + 'Telemetry_Logger_Class__c', 'FROM Trigger_Action__mdt', 'WHERE', '{0}__c != NULL', @@ -228,6 +230,37 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem finalizerHandler.handleDynamicFinalizers(); } + /** + * @description Check a custom permission with namespace awareness. + * This method attempts to check the permission with the provided name first, + * and if that fails, tries to check without the namespace prefix. + * + * @param permissionName The name of the permission to check + * @return True if the permission is granted, false otherwise + */ + @TestVisible + private Boolean checkPermissionNamespaceAware(String permissionName) { + try { + if (FeatureManagement.checkPermission(permissionName)) { + return true; + } + } catch (Exception e) { + } + + if (permissionName.contains(DOUBLE_UNDERSCORE)) { + List parts = permissionName.split(DOUBLE_UNDERSCORE, 2); + if (parts.size() == 2) { + String permission = parts[1]; + try { + return FeatureManagement.checkPermission(permission); + } catch (Exception e) { + } + } + } + + return false; + } + /** * @description Populate the permission map. * @@ -237,7 +270,7 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem if (permissionName != null && !permissionMap.containsKey(permissionName)) { permissionMap.put( permissionName, - FeatureManagement.checkPermission(permissionName) + checkPermissionNamespaceAware(permissionName) ); } } @@ -295,6 +328,33 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem .get(relationshipName); } + /** + * @description Create a telemetry logger instance based on the metadata configuration. + * + * @param triggerMetadata The Trigger Action metadata containing telemetry configuration. + * @return An instance of ITriggerActionTelemetryLogger, or null if telemetry is disabled. + */ + @TestVisible + private ITriggerActionTelemetryLogger createTelemetryLogger( + Trigger_Action__mdt triggerMetadata + ) { + if (!triggerMetadata.Enable_Telemetry__c) { + return null; + } + + String loggerClassName = triggerMetadata.Telemetry_Logger_Class__c; + if (String.isBlank(loggerClassName)) { + loggerClassName = 'LimitLogger'; + } + + try { + return (ITriggerActionTelemetryLogger) Type.forName(loggerClassName).newInstance(); + } catch (Exception e) { + System.debug(LoggingLevel.WARN, 'Failed to create telemetry logger ' + loggerClassName + ': ' + e.getMessage()); + return new LimitLogger(); + } + } + /** * @description Check if the Trigger Action should be executed. * @@ -385,6 +445,22 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem continue; } + ITriggerActionTelemetryLogger telemetryLogger = createTelemetryLogger(triggerMetadata); + Integer recordCount = Math.max( + (filtered.triggerOld == null) ? 0 : filtered.triggerOld.size(), + (filtered.triggerNew == null) ? 0 : filtered.triggerNew.size() + ); + + if (telemetryLogger != null) { + TelemetryContext startContext = new TelemetryContext(); + startContext.actionClassName = triggerMetadata.Apex_Class_Name__c; + startContext.triggerOperation = String.valueOf(context); + startContext.sObjectType = this.sObjectName; + startContext.recordCount = recordCount; + startContext.phase = TelemetryPhase.START; + telemetryLogger.log(startContext); + } + switch on context { when BEFORE_INSERT { ((TriggerAction.BeforeInsert) triggerAction) @@ -415,6 +491,16 @@ public inherited sharing class MetadataTriggerHandler extends TriggerBase implem .afterUndelete(filtered.triggerNew); } } + + if (telemetryLogger != null) { + TelemetryContext endContext = new TelemetryContext(); + endContext.actionClassName = triggerMetadata.Apex_Class_Name__c; + endContext.triggerOperation = String.valueOf(context); + endContext.sObjectType = this.sObjectName; + endContext.recordCount = recordCount; + endContext.phase = TelemetryPhase.FINISH; + telemetryLogger.log(endContext); + } } } diff --git a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls index 20e394b..4efdc74 100644 --- a/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls +++ b/trigger-actions-framework/main/default/classes/MetadataTriggerHandlerTest.cls @@ -795,6 +795,77 @@ private class MetadataTriggerHandlerTest { } } + @IsTest + private static void testTelemetryLoggerCreation() { + Trigger_Action__mdt actionWithTelemetry = new Trigger_Action__mdt( + Apex_Class_Name__c = TEST_BEFORE_INSERT, + Enable_Telemetry__c = true, + Telemetry_Logger_Class__c = 'LimitLogger' + ); + + Test.startTest(); + ITriggerActionTelemetryLogger logger = handler.createTelemetryLogger(actionWithTelemetry); + Test.stopTest(); + + System.Assert.isNotNull(logger, 'Telemetry logger should be created when enabled'); + System.Assert.isTrue(logger instanceof LimitLogger, 'Should create LimitLogger instance'); + } + + @IsTest + private static void testTelemetryLoggerDisabled() { + Trigger_Action__mdt actionWithoutTelemetry = new Trigger_Action__mdt( + Apex_Class_Name__c = TEST_BEFORE_INSERT, + Enable_Telemetry__c = false + ); + + Test.startTest(); + ITriggerActionTelemetryLogger logger = handler.createTelemetryLogger(actionWithoutTelemetry); + Test.stopTest(); + + System.Assert.isNull(logger, 'Telemetry logger should be null when disabled'); + } + + @IsTest + private static void testTelemetryLoggerDefaultFallback() { + Trigger_Action__mdt actionWithDefaultTelemetry = new Trigger_Action__mdt( + Apex_Class_Name__c = TEST_BEFORE_INSERT, + Enable_Telemetry__c = true, + Telemetry_Logger_Class__c = null + ); + + Test.startTest(); + ITriggerActionTelemetryLogger logger = handler.createTelemetryLogger(actionWithDefaultTelemetry); + Test.stopTest(); + + System.Assert.isNotNull(logger, 'Should create default logger when class name is null'); + System.Assert.isTrue(logger instanceof LimitLogger, 'Should fallback to LimitLogger'); + } + + @IsTest + private static void testTelemetryLoggerInvalidClass() { + Trigger_Action__mdt actionWithInvalidTelemetry = new Trigger_Action__mdt( + Apex_Class_Name__c = TEST_BEFORE_INSERT, + Enable_Telemetry__c = true, + Telemetry_Logger_Class__c = 'NonExistentLoggerClass' + ); + + Test.startTest(); + ITriggerActionTelemetryLogger logger = handler.createTelemetryLogger(actionWithInvalidTelemetry); + Test.stopTest(); + + System.Assert.isNotNull(logger, 'Should fallback to default logger when invalid class specified'); + System.Assert.isTrue(logger instanceof LimitLogger, 'Should fallback to LimitLogger for invalid class'); + } + + @IsTest + private static void testCheckPermissionNamespaceAware() { + Test.startTest(); + Boolean result = handler.checkPermissionNamespaceAware('TestPermission'); + Test.stopTest(); + + System.Assert.isFalse(result, 'Non-existent permission should return false'); + } + private class FakeSelector extends MetadataTriggerHandler.Selector { List results; public FakeSelector(List results) { diff --git a/trigger-actions-framework/main/default/classes/TelemetryContext.cls b/trigger-actions-framework/main/default/classes/TelemetryContext.cls new file mode 100644 index 0000000..eed4820 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TelemetryContext.cls @@ -0,0 +1,75 @@ +/* + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @group Trigger Actions Framework + * @description Context object providing information about the current trigger execution state. + * This allows telemetry loggers to access relevant information for logging purposes. + */ +public class TelemetryContext { + /** + * @description The name of the trigger action class being executed + */ + public String actionClassName { get; set; } + + /** + * @description The trigger operation context (e.g., BEFORE_INSERT, AFTER_UPDATE) + */ + public String triggerOperation { get; set; } + + /** + * @description The sObject type being processed + */ + public String sObjectType { get; set; } + + /** + * @description The phase of execution (START or FINISH) + */ + public TelemetryPhase phase { get; set; } + + /** + * @description Number of records being processed in this execution + */ + public Integer recordCount { get; set; } + + /** + * @description Default constructor for creating a telemetry context + */ + public TelemetryContext() { + } + + /** + * @description Constructor for creating a telemetry context + * @param actionClassName The name of the action class + * @param triggerOperation The trigger operation + * @param sObjectType The sObject type + * @param phase The execution phase + * @param recordCount Number of records being processed + */ + public TelemetryContext( + String actionClassName, + String triggerOperation, + String sObjectType, + TelemetryPhase phase, + Integer recordCount + ) { + this.actionClassName = actionClassName; + this.triggerOperation = triggerOperation; + this.sObjectType = sObjectType; + this.phase = phase; + this.recordCount = recordCount; + } +} \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/TelemetryContext.cls-meta.xml b/trigger-actions-framework/main/default/classes/TelemetryContext.cls-meta.xml new file mode 100644 index 0000000..835ede4 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TelemetryContext.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/TelemetryPhase.cls b/trigger-actions-framework/main/default/classes/TelemetryPhase.cls new file mode 100644 index 0000000..6d29673 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TelemetryPhase.cls @@ -0,0 +1,24 @@ +/* + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * @group Trigger Actions Framework + * @description Enum representing the phase of trigger execution for telemetry logging. + */ +public enum TelemetryPhase { + START, + FINISH +} \ No newline at end of file diff --git a/trigger-actions-framework/main/default/classes/TelemetryPhase.cls-meta.xml b/trigger-actions-framework/main/default/classes/TelemetryPhase.cls-meta.xml new file mode 100644 index 0000000..835ede4 --- /dev/null +++ b/trigger-actions-framework/main/default/classes/TelemetryPhase.cls-meta.xml @@ -0,0 +1,5 @@ + + + 63.0 + Active + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml b/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml index 596dfd0..73ea6e6 100644 --- a/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml +++ b/trigger-actions-framework/main/default/layouts/Trigger_Action__mdt-Trigger Action Layout.layout-meta.xml @@ -79,6 +79,25 @@ + + true + true + true + + + + Edit + Enable_Telemetry__c + + + + + Edit + Telemetry_Logger_Class__c + + + + true true diff --git a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Enable_Telemetry__c.field-meta.xml b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Enable_Telemetry__c.field-meta.xml new file mode 100644 index 0000000..6f0a457 --- /dev/null +++ b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Enable_Telemetry__c.field-meta.xml @@ -0,0 +1,11 @@ + + + Enable_Telemetry__c + false + When enabled, telemetry information will be logged for this trigger action. This includes Salesforce limits and performance metrics. Should not be enabled in production environments. + false + DeveloperControlled + Enable telemetry logging for this trigger action. Logs Salesforce limits and performance metrics to help with debugging. Not recommended for production use. + + Checkbox + \ No newline at end of file diff --git a/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Telemetry_Logger_Class__c.field-meta.xml b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Telemetry_Logger_Class__c.field-meta.xml new file mode 100644 index 0000000..9d824ec --- /dev/null +++ b/trigger-actions-framework/main/default/objects/Trigger_Action__mdt/fields/Telemetry_Logger_Class__c.field-meta.xml @@ -0,0 +1,13 @@ + + + Telemetry_Logger_Class__c + The fully qualified name of the Apex class that implements ITriggerActionTelemetryLogger interface. If not specified, the default LimitLogger will be used when telemetry is enabled. + false + DeveloperControlled + Optional: Specify a custom telemetry logger class that implements ITriggerActionTelemetryLogger. Leave blank to use the default LimitLogger. + + 255 + false + Text + false + \ No newline at end of file