├── LICENSE ├── README.md ├── force-app └── main │ └── default │ ├── classes │ ├── Log.cls │ ├── Log.cls-meta.xml │ ├── Log_Test.cls │ └── Log_Test.cls-meta.xml │ ├── layouts │ └── Log__c-Log Layout.layout-meta.xml │ ├── objects │ └── Log__c │ │ ├── Log__c.object-meta.xml │ │ ├── fields │ │ ├── Class__c.field-meta.xml │ │ ├── Context__c.field-meta.xml │ │ ├── Line__c.field-meta.xml │ │ ├── Message__c.field-meta.xml │ │ └── Method__c.field-meta.xml │ │ └── listViews │ │ └── All.listView-meta.xml │ └── tabs │ └── Log__c.tab-meta.xml └── package.xml /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mehdi Cherfaoui 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Oct'2023 update: I don't recommend using this framework anymore. I don't maintain it.** 2 | 3 | **A better alternative is to use Nebula Logger for Salesforce: https://github.com/jongpie/NebulaLogger** 4 | 5 | 6 | # sfdx-ErrorLoggingFramework 7 | 8 | 9 | A basic error logging framework for Apex, derived from https://github.com/rsoesemann/apex-unified-logging 10 | 11 | 12 | The purpose of this version is to store the logs into a custom object (so that it can be persisted longer than a platform event). 13 | 14 | 15 | To add this error logging framework to your sfdx project, you can clone this repo and then push it to your scratch org by executing the command `sfdx force:source:deploy -p `, followed by `sfdx force:source:retrieve -x ` 16 | to retrieve the metadata in your sfdx project folder. 17 | 18 | 19 | ## Example Use: 20 | 21 | ### To log all errors of a bulk DML operation: 22 | ```apex 23 | // Create two accounts, one of which is missing a required field 24 | Account[] accountList = new List{ 25 | new Account(Name='Account1'), 26 | new Account()}; 27 | Database.SaveResult[] srList = Database.insert(accountList, false); 28 | 29 | // Log errors 30 | Log.error(srList); 31 | ``` 32 | 33 | ### To log a single error: 34 | ```apex 35 | try{ 36 | //try something complex 37 | }catch (exception pokemon){ 38 | // Log error 39 | Log.error(pokemon.getMessage()) 40 | } 41 | ``` 42 | 43 | ### To log a single error related to a record or job Id: 44 | ```apex 45 | try{ 46 | Update myRecord; 47 | }catch (exception pokemon){ 48 | // Log error 49 | Log.error(pokemon.getMessage(), myRecord.Id) 50 | } 51 | ``` 52 | ~~ 53 | -------------------------------------------------------------------------------- /force-app/main/default/classes/Log.cls: -------------------------------------------------------------------------------- 1 | /** 2 | Logging utility. Derived from https://github.com/rsoesemann/apex-unified-logging 3 | **/ 4 | public with sharing class Log { 5 | 6 | private static final String ORG_NAMESPACE = [SELECT NamespacePrefix FROM Organization LIMIT 1].NamespacePrefix; 7 | private static final String CLASSNAME = Log.class.getName(); 8 | private static final Pattern STACK_LINE = Pattern.compile('^(?:Class\\.)?([^.]+)\\.?([^\\.\\:]+)?[\\.\\:]?([^\\.\\:]*): line (\\d+), column (\\d+)$'); 9 | private static final String DEFAULT_CONTEXT = timestamp(); 10 | 11 | 12 | /** 13 | * @description: Logs a simple error. Result in a Log__c record being inserted, so do not call it in a Loop! 14 | * @params: message (short description of the error) 15 | * @return: void 16 | */ 17 | public static void error(String message) { 18 | error(message, new List(), null); 19 | } 20 | 21 | /** 22 | * @description: Logs an error with a list of associated information. Result in a Log__c record being inserted, so do not call it in a Loop! 23 | * @params: message (short description of the error) 24 | * @params: values (list of objects containing additional information about the error) 25 | * @return: void 26 | */ 27 | public static void error(String message, List values) { 28 | error(message, values, null); 29 | } 30 | 31 | /** 32 | * @description: Logs an error associated to a record or job ID. Result in a Log__c record being inserted, so do not call it in a Loop! 33 | * @params: message (short description of the error) 34 | * @params: contextId (ID of the associated record or job) 35 | * @return: void 36 | */ 37 | public static void error(String message, Id contextId) { 38 | error(message, new List(), contextId); 39 | } 40 | 41 | /** 42 | * @description: Logs an error associated to a record or job ID, with a list of associated information. Result in a Log__c record being inserted, so do not call it in a Loop! 43 | * @params: message (short description of the error) 44 | * @params: values (list of objects containing additional information about the error) 45 | * @params: contextId (ID of the associated record or job) 46 | * @return: void 47 | */ 48 | public static void error(String message, List values, Id contextId) { 49 | Log__c newLog = newLog(message, values, contextId); 50 | insertLogs(new List{newLog}); 51 | } 52 | 53 | /** 54 | * @description: Logs all errors associated with a database.insert or database.udpate DML operation 55 | * @params: srList (List returned by database.insert or database.udpate) 56 | * @return: void 57 | */ 58 | public static void error(List srList) { 59 | List logList = new List(); 60 | for (Database.SaveResult sr : srList) { 61 | if (!sr.isSuccess()) { 62 | Log__c newLog =newLog('Database.SaveResult error', sr.getErrors(), sr.getId()); 63 | logList.add(newLog); 64 | } 65 | } 66 | insertLogs(logList); 67 | } 68 | 69 | /** 70 | * @description: Logs all errors associated with a database.upsert DML operation 71 | * @params: urList (List returned by database.upsert) 72 | * @return: void 73 | */ 74 | public static void error(List urList) { 75 | List logList = new List(); 76 | for (Database.UpsertResult ur : urList) { 77 | if (!ur.isSuccess()) { 78 | Log__c newLog =newLog('Database.UpsertResult error; isCreated: '+ur.isCreated(), ur.getErrors(), ur.getId()); 79 | logList.add(newLog); 80 | } 81 | } 82 | insertLogs(logList); 83 | } 84 | 85 | /** 86 | * @description: Logs all errors associated with a database.delete DML operation 87 | * @params: drList (List returned by database.delete) 88 | * @return: void 89 | */ 90 | public static void error(List drList) { 91 | List logList = new List(); 92 | for (Database.DeleteResult dr : drList) { 93 | if (!dr.isSuccess()) { 94 | Log__c newLog =newLog('Database.DeleteResult error', dr.getErrors(), dr.getId()); 95 | logList.add(newLog); 96 | } 97 | } 98 | insertLogs(logList); 99 | } 100 | 101 | 102 | /** 103 | HELPER METHODS 104 | **/ 105 | 106 | private static Log__c newLog(String message, List values, Id contextId) { 107 | message = message +' ; '+ cast(values); 108 | Log__c newLog = new Log__c(Message__c = message); 109 | newLog.Context__c = (contextId == null) ? DEFAULT_CONTEXT : ' '+contextId; 110 | populateLocation(newLog); 111 | return newLog; 112 | } 113 | 114 | private static List cast(List values) { 115 | List result = new List(); 116 | for(Object value : values) { 117 | result.add(' ' + value); 118 | } 119 | return result; 120 | } 121 | 122 | 123 | private static String timestamp() { 124 | return System.now().formatGmt('HH:mm:ss.SSS'); 125 | } 126 | 127 | 128 | private static void populateLocation(Log__c logMessage) { 129 | // Note: Idea taken from https://salesforce.stackexchange.com/questions/153835 130 | List stacktrace = new DmlException().getStackTraceString().split('\n'); 131 | 132 | for(String line : stacktrace) { 133 | Matcher matcher = STACK_LINE.matcher(line); 134 | if(matcher.find() && !line.startsWith('Class.' + CLASSNAME + '.')) { 135 | logMessage.Class__c = matcher.group(1); 136 | logMessage.Method__c = prettyMethod(matcher.group(2)); 137 | logMessage.Line__c = Integer.valueOf(matcher.group(4)); 138 | return; 139 | } 140 | 141 | } 142 | } 143 | 144 | private static String prettyMethod(String method) { 145 | String result = (method == null) ? 'anonymous' : method; 146 | return (result.contains('init')) ? 'ctor' : result; 147 | } 148 | 149 | private static void insertLogs(List logsToInsert) { 150 | Database.SaveResult[] srList = Database.insert(logsToInsert, false); 151 | for (Database.SaveResult sr : srList) { 152 | if (!sr.isSuccess()) { 153 | System.debug('Unable to save Log: '+ sr.getErrors()); 154 | } 155 | } 156 | } 157 | 158 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/Log.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/classes/Log_Test.cls: -------------------------------------------------------------------------------- 1 | @IsTest 2 | private class Log_Test { 3 | 4 | @IsTest 5 | private static void singleErrorTest() { 6 | Test.startTest(); 7 | Account[] acctsFail = new List{new Account()}; 8 | Database.SaveResult[] srList = Database.insert(acctsFail, false); 9 | Log.error('test error'); //1 log record 10 | Log.error('test error Id', acctsFail[0].Id); //1 additional log record 11 | Log.error('test error List', srList); //1 log record 12 | Log.error('test error List and Id', srList, acctsFail[0].Id); //1 additional log record 13 | Test.stopTest(); 14 | //Verify 15 | List errorLogs = [SELECT Id FROM Log__c]; 16 | System.assertEquals(4, errorLogs.size(), 'No Log created'); 17 | } 18 | 19 | @IsTest 20 | private static void bulkInsertErrorTest() { 21 | Test.startTest(); 22 | Account[] acctsFail = new List{new Account()}; 23 | Database.SaveResult[] srList = Database.insert(acctsFail, false); 24 | Log.error(srList); //1 log record 25 | Test.stopTest(); 26 | //Verify 27 | List errorLogs = [SELECT Id FROM Log__c]; 28 | System.assertEquals(1, errorLogs.size(), 'No Log created'); 29 | } 30 | 31 | @IsTest 32 | private static void bulkUpsertErrorTest() { 33 | Test.startTest(); 34 | Account[] acctsFail = new List{new Account()}; 35 | Database.UpsertResult[] urList = Database.upsert(acctsFail, false); 36 | Log.error(urList); //1 log record 37 | Test.stopTest(); 38 | //Verify 39 | List errorLogs = [SELECT Id FROM Log__c]; 40 | System.assertEquals(1, errorLogs.size(), 'No Log created'); 41 | } 42 | 43 | @IsTest 44 | private static void bulkDeleteErrorTest() { 45 | Test.startTest(); 46 | Pricebook2 standardPricebook = new Pricebook2(Id = Test.getStandardPricebookId()); 47 | Database.DeleteResult[] drList = Database.delete(new List{standardPricebook}, false); 48 | Log.error(drList); //1 log record 49 | Test.stopTest(); 50 | //Verify 51 | List errorLogs = [SELECT Id FROM Log__c]; 52 | System.assertEquals(1, errorLogs.size(), 'No Log created'); 53 | } 54 | 55 | 56 | 57 | } -------------------------------------------------------------------------------- /force-app/main/default/classes/Log_Test.cls-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 45.0 4 | Active 5 | 6 | -------------------------------------------------------------------------------- /force-app/main/default/layouts/Log__c-Log Layout.layout-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ChangeOwnerOne 4 | ChangeRecordType 5 | Clone 6 | Delete 7 | Edit 8 | PrintableView 9 | Share 10 | Submit 11 | 12 | false 13 | false 14 | true 15 | 16 | 17 | 18 | Readonly 19 | CreatedById 20 | 21 | 22 | Edit 23 | Context__c 24 | 25 | 26 | 27 | 28 | Edit 29 | Class__c 30 | 31 | 32 | Edit 33 | Method__c 34 | 35 | 36 | Edit 37 | Line__c 38 | 39 | 40 | 41 | 42 | 43 | true 44 | true 45 | false 46 | 47 | 48 | 49 | Edit 50 | Message__c 51 | 52 | 53 | 54 | 55 | 56 | false 57 | false 58 | true 59 | 60 | 61 | 62 | Readonly 63 | Name 64 | 65 | 66 | 67 | 68 | 69 | 70 | true 71 | false 72 | true 73 | 74 | 75 | 76 | 77 | 78 | 79 | false 80 | false 81 | false 82 | false 83 | false 84 | 85 | 00h5E000000YXWp 86 | 4 87 | 0 88 | Default 89 | 90 | 91 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/Log__c.object-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Accept 5 | Default 6 | 7 | 8 | CancelEdit 9 | Default 10 | 11 | 12 | Clone 13 | Default 14 | 15 | 16 | Delete 17 | Default 18 | 19 | 20 | Edit 21 | Default 22 | 23 | 24 | List 25 | Default 26 | 27 | 28 | New 29 | Default 30 | 31 | 32 | SaveEdit 33 | Default 34 | 35 | 36 | Tab 37 | Default 38 | 39 | 40 | View 41 | Default 42 | 43 | false 44 | SYSTEM 45 | Deployed 46 | Technical object, storing application logs and errors. 47 | false 48 | true 49 | false 50 | false 51 | false 52 | true 53 | true 54 | true 55 | true 56 | Private 57 | 58 | 59 | Log-{0000} 60 | 61 | AutoNumber 62 | 63 | Logs 64 | 65 | Read 66 | Public 67 | 68 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/fields/Class__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Class__c 4 | Apex Class Name. 5 | false 6 | Apex Class Name. 7 | 8 | 80 9 | false 10 | false 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/fields/Context__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Context__c 4 | Context (ID or Timestamp). 5 | false 6 | Context (ID or Timestamp). 7 | 8 | 255 9 | false 10 | false 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/fields/Line__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Line__c 4 | Line number in the Apex Class. 5 | false 6 | Line number in the Apex Class. 7 | 8 | 18 9 | false 10 | 0 11 | false 12 | Number 13 | false 14 | 15 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/fields/Message__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Message__c 4 | Error Message or Description 5 | false 6 | Error Message or Description 7 | 8 | 32768 9 | false 10 | LongTextArea 11 | 5 12 | 13 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/fields/Method__c.field-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Method__c 4 | Apex Method Name. 5 | false 6 | Apex Method Name. 7 | 8 | 255 9 | false 10 | false 11 | Text 12 | false 13 | 14 | -------------------------------------------------------------------------------- /force-app/main/default/objects/Log__c/listViews/All.listView-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | All 4 | Everything 5 | 6 | 7 | -------------------------------------------------------------------------------- /force-app/main/default/tabs/Log__c.tab-meta.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | Technical object, storing application logs. 5 | Custom99: CRT TV 6 | 7 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Log 5 | Log_Test 6 | ApexClass 7 | 8 | 9 | Log__c 10 | CustomObject 11 | 12 | 13 | Log__c 14 | CustomTab 15 | 16 | 17 | Log__c-Log Layout 18 | Layout 19 | 20 | 21 | Log__c.All 22 | ListView 23 | 24 | 45.0 25 | --------------------------------------------------------------------------------