Skip to content

Add telemetry logging support to Trigger Actions Framework #182

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
@@ -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);
}
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>63.0</apiVersion>
<status>Active</status>
</ApexClass>
59 changes: 59 additions & 0 deletions trigger-actions-framework/main/default/classes/LimitLogger.cls
Original file line number Diff line number Diff line change
@@ -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<String>{
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);
}
}
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>63.0</apiVersion>
<status>Active</status>
</ApexClass>
85 changes: 85 additions & 0 deletions trigger-actions-framework/main/default/classes/LimitLoggerTest.cls
Original file line number Diff line number Diff line change
@@ -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');
}
}
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>63.0</apiVersion>
<status>Active</status>
</ApexClass>
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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<String> 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.
*
Expand All @@ -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)
);
}
}
Expand Down Expand Up @@ -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.
*
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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);
}
}
}

Expand Down
Loading