diff --git a/.gitignore b/.gitignore index 7bbe5a6..1616be0 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,7 @@ IlluminatedCloud **/profileSessionSettings/**.* force-app/main/default/settings/ -force-app/main/default/profiles/ \ No newline at end of file +force-app/main/default/profiles/.localdevserver/ +.localdevserver/ +force-app/main/default/lwc/jsconfig.json +force-app/main/default/package.xml \ No newline at end of file diff --git a/README.md b/README.md index 4308622..260d459 100644 --- a/README.md +++ b/README.md @@ -170,7 +170,7 @@ to log INFO,DEBUG,WARN,ERROR. It might be desirable to reduce this for productio [![Deploy](https://raw.githubusercontent.com/afawcett/githubsfdeploy/master/src/main/webapp/resources/img/deploy.png)](https://githubsfdeploy.herokuapp.com/app/githubdeploy/mlockett/ApexLogger) - After deploying: +### After deploying: * Ensure users that should be able to read the logs are assigned the AppLogReader permission set. * Add an assignment on the _App Logs_ application to the profiles of users that should read logs. diff --git a/force-app/main/default/applications/App_Logs.app-meta.xml b/force-app/main/default/applications/App_Logs.app-meta.xml index 94bc76c..43ce190 100644 --- a/force-app/main/default/applications/App_Logs.app-meta.xml +++ b/force-app/main/default/applications/App_Logs.app-meta.xml @@ -1,5 +1,13 @@ + + Tab + App_Log_Home + Large + false + Flexipage + standard-home + #0070D2 false @@ -12,6 +20,7 @@ false Standard + standard-home Log_List Log_Reader Log_Tester diff --git a/force-app/main/default/classes/LogService.cls b/force-app/main/default/classes/LogService.cls index 4ce8c4a..9382740 100644 --- a/force-app/main/default/classes/LogService.cls +++ b/force-app/main/default/classes/LogService.cls @@ -20,6 +20,7 @@ global without sharing class LogService { * @param message message to be logged * @param className . if applicable */ + @AuraEnabled(cacheable=false) global static void debug(String message, String className) { logger.debug(message, className); } @@ -30,6 +31,7 @@ global without sharing class LogService { * @param message message to be logged * @param className . if applicable */ + @AuraEnabled(cacheable=false) global static void info(String message, String className) { logger.info(message, className); } @@ -40,6 +42,7 @@ global without sharing class LogService { * @param message message to be logged * @param className . if applicable */ + @AuraEnabled(cacheable=false) global static void warn(String message, String className) { logger.warn(message, className); } @@ -124,4 +127,4 @@ global without sharing class LogService { @InvocableVariable(Label='Object Id' Description='Id of object error occurred on') global String affectedId; } -} \ No newline at end of file +} \ No newline at end of file diff --git a/force-app/main/default/classes/LogUiController.cls b/force-app/main/default/classes/LogUiController.cls index 6fa0416..151cfd2 100644 --- a/force-app/main/default/classes/LogUiController.cls +++ b/force-app/main/default/classes/LogUiController.cls @@ -12,24 +12,14 @@ public without sharing class LogUiController { String query = 'SELECT AffectedId__c, Class__c, CreatedDate, ' + 'Id, LogLevel__c, Message__c, Name, ShortMessage__c ' + 'FROM AppLog__c ' - + 'WHERE LogLevel__c IN :params.logLevels ' - + ' ORDER BY CreatedDate DESC ' - + ' LIMIT = ' + params.logsPerPage; - List retVal = [ - SELECT AffectedId__c, - Class__c, - CreatedDate, - Id, - LogLevel__c, - Message__c, - Name, - ShortMessage__c - FROM AppLog__c - WHERE LogLevel__c IN :params.logLevels - ORDER BY CreatedDate DESC - LIMIT :params.logsPerPage - ]; - return retVal; + + 'WHERE LogLevel__c IN (\'' + String.join(params.logLevels, '\',\'') + '\')'; + if (params.newLogsOnly == true && params.tailTimestamp != null) { + query += ' AND CreatedDate > ' + params.tailTimestamp; + } + + query += ' ORDER BY CreatedDate DESC LIMIT ' + params.logsPerPage; + + return Database.query(query); } @AuraEnabled @@ -42,11 +32,150 @@ public without sharing class LogUiController { return results; } + @AuraEnabled + public static void insertLog(String level,String message,String cls){ + + AppLog__c log = new AppLog__c(); + log.LogLevel__c = level; + log.Message__c = message; + log.ShortMessage__c = message.left(255); + log.Class__c = cls; + insert log; + } + + // insert a lot platform event + @AuraEnabled + public static void insertLogPe(String level,String message,String cls){ + + EventBus.publish(new AppLogEvent__e( + LogLevel__c = level, + Message__c = message, + Class__c = cls + )); + } + + @AuraEnabled + public static List getLogCountByLevelDate(String relDateFilter){ + // Valid filters list + Set validFilters = new Set{ + 'TODAY', + 'YESTERDAY', + 'THIS_WEEK', + 'LAST_WEEK', + 'THIS_MONTH', + 'LAST_MONTH', + 'THIS_YEAR', + 'LAST_YEAR' + }; + + // Validate input + if (relDateFilter != null && !validFilters.contains(relDateFilter.toUpperCase())) { + throw new AuraHandledException('Invalid date filter provided'); + } + + String query = 'SELECT COUNT(Id) LogCount, MIN(CreatedDate) FirstCreatedDate, LogLevel__c ' + + 'FROM AppLog__c '; + + // Add date filter if provided (null means ALL TIME) + if (validFilters.contains(relDateFilter)) { + query += ' WHERE CreatedDate = ' + relDateFilter; // Use the literal directly in SOQL + } + // Order by custom order: INFO, DEBUG, WARN, ERROR + query += ' GROUP BY LogLevel__c ' + + 'ORDER BY LogLevel__c '; + + return Database.query(query); + } + + @AuraEnabled + public static List getLogsByDateLevel(String relDateFilter) { + system.debug('relDateFilter: ' + relDateFilter); + // Valid filters list + Set validFilters = new Set{ + 'TODAY', + 'YESTERDAY', + 'THIS_WEEK', + 'LAST_WEEK', + 'THIS_MONTH', + 'LAST_MONTH', + 'THIS_YEAR', + 'LAST_YEAR' + }; + + // Validate input + if (relDateFilter != null && !validFilters.contains(relDateFilter.toUpperCase())) { + throw new AuraHandledException('Invalid date filter provided'); + } + + String query = 'SELECT DAY_ONLY(convertTimezone(CreatedDate)) CreatedDate, LogLevel__c level, COUNT(Id) logCount ' + + 'FROM AppLog__c '; + // Add date filter if provided (null means ALL TIME) + if (validFilters.contains(relDateFilter)) { + query += ' WHERE CreatedDate = ' + relDateFilter; // Use the literal directly in SOQL + } + query += ' GROUP BY DAY_ONLY(convertTimezone(CreatedDate)), LogLevel__c ' + + 'ORDER BY DAY_ONLY(convertTimezone(CreatedDate))'; + + system.debug('Query: ' + query); + return Database.query(query); + } + + @AuraEnabled + public static void deleteLog(String logId) { + try { + delete new AppLog__c(Id = logId); + } catch (Exception e) { + throw new AuraHandledException('Error deleting log: ' + e.getMessage()); + } + } + + @AuraEnabled + public static void deleteLogsByLevel(String logLevel, String relDateFilter) { + system.debug('Deleting logs with log level: ' + logLevel + ' and date filter: ' + relDateFilter); + // Valid filters list + Set validFilters = new Set{ + 'TODAY', + 'YESTERDAY', + 'THIS_WEEK', + 'LAST_WEEK', + 'THIS_MONTH', + 'LAST_MONTH', + 'THIS_YEAR', + 'LAST_YEAR' + }; + + // Validate inputs + if (String.isBlank(logLevel)) { + throw new AuraHandledException('Log level must be specified'); + } + if (relDateFilter != null && !validFilters.contains(relDateFilter.toUpperCase())) { + throw new AuraHandledException('Invalid date filter provided'); + } + + String query = 'SELECT Id FROM AppLog__c WHERE LogLevel__c = :logLevel'; + + // Add date filter if provided (null means ALL TIME) + if (relDateFilter != null) { + query += ' AND CreatedDate = ' + relDateFilter; + } + try { + List logsToDelete = Database.query(query); + delete logsToDelete; + } catch (Exception e) { + throw new AuraHandledException('Error deleting logs: ' + e.getMessage()); + } + } + public class LogParamWrapper { @AuraEnabled public List logLevels { get; set; } + @AuraEnabled public String cacheBuster { get; set; } @AuraEnabled public Integer logsPerPage { get; set; } + @AuraEnabled + public Boolean newLogsOnly { get; set; } + @AuraEnabled + public String tailTimestamp { get; set; } } } \ No newline at end of file diff --git a/force-app/main/default/classes/LogUiController_Test.cls b/force-app/main/default/classes/LogUiController_Test.cls index 6e7ec16..bb59d74 100644 --- a/force-app/main/default/classes/LogUiController_Test.cls +++ b/force-app/main/default/classes/LogUiController_Test.cls @@ -6,12 +6,20 @@ @IsTest public with sharing class LogUiController_Test { + + @TestSetup + static void setup() { + List logs = new List(); + logs.add(new AppLog__c(LogLevel__c = 'DEBUG', Message__c = 'Test Debug')); + logs.add(new AppLog__c(LogLevel__c = 'ERROR', Message__c = 'Test Error')); + logs.add(new AppLog__c(LogLevel__c = 'WARN', Message__c = 'Test Warning')); + insert logs; + } + @IsTest static void getReturnsRecords() { LogService_Test.testSetup(); - // insert a log - insert new AppLog__c(Message__c = 'My error', LogLevel__c = 'Error'); - + // create params LogUiController.LogParamWrapper params = new LogUiController.LogParamWrapper(); params.logLevels = new List{ @@ -28,30 +36,55 @@ public with sharing class LogUiController_Test { static void getLogCountByLevelReturnCorrectValues() { LogService_Test.testSetup(); - // insert a few logs - insert new List{ - new AppLog__c(Message__c = 'My error', LogLevel__c = 'Error'), - new AppLog__c(Message__c = 'My warning', LogLevel__c = 'Warn') - }; - // call system under test List results = LogUiController.getLogCountByLevel(); // expect 2 results - Assert.areEqual(2, results.size()); + Assert.areEqual(3, results.size()); // expect 1 error log - Integer logCount = Integer.valueOf(results[0].get('LogCount')); - String logLevel = String.valueOf(results[0].get('LogLevel__c')); + Integer logCount = Integer.valueOf(results[1].get('LogCount')); + String logLevel = String.valueOf(results[1].get('LogLevel__c')); - Assert.areEqual('Error', logLevel); + Assert.areEqual('ERROR', logLevel.toUpperCase()); Assert.areEqual(1, logCount); // expect 1 warn log - Integer logCount2 = Integer.valueOf(results[1].get('LogCount')); - String logLevel2 = String.valueOf(results[1].get('LogLevel__c')); + Integer logCount2 = Integer.valueOf(results[2].get('LogCount')); + String logLevel2 = String.valueOf(results[2].get('LogLevel__c')); - Assert.areEqual('Warn', logLevel2); + Assert.areEqual('WARN', logLevel2.toUpperCase()); Assert.areEqual(1, logCount2); } + + @IsTest + static void testDeleteLog() { + AppLog__c log = [SELECT Id FROM AppLog__c LIMIT 1]; + + Test.startTest(); + LogUiController.deleteLog(log.Id); + Test.stopTest(); + + List remainingLogs = [SELECT Id FROM AppLog__c WHERE Id = :log.Id]; + System.assertEquals(0, remainingLogs.size()); + } + + @IsTest + static void testDeleteLogsByLevel() { + Test.startTest(); + LogUiController.deleteLogsByLevel('DEBUG', 'TODAY'); + Test.stopTest(); + + List remainingLogs = [SELECT Id FROM AppLog__c WHERE LogLevel__c = 'DEBUG']; + System.assertEquals(0, remainingLogs.size()); + } + + @IsTest + static void testGetLogCountByLevelDate() { + Test.startTest(); + List results = LogUiController.getLogCountByLevelDate('TODAY'); + Test.stopTest(); + + System.assertNotEquals(0, results.size()); + } } \ No newline at end of file diff --git a/force-app/main/default/classes/Logger_Test.cls b/force-app/main/default/classes/Logger_Test.cls index 9e90454..021a386 100644 --- a/force-app/main/default/classes/Logger_Test.cls +++ b/force-app/main/default/classes/Logger_Test.cls @@ -6,6 +6,28 @@ public with sharing class Logger_Test { static Logger sut = new Logger(); + @TestSetup + static void setup() { + Logger.ignoreTestMode = true; + sut.debug('My message', 'myClass'); + sut.debug('My message', 'myClass', 'my id'); + sut.warn('My message', 'myClass'); + sut.warn('My message', 'myClass', 'my id'); + sut.error('My message', 'myClass'); + try { + //noinspection ApexUnusedDeclaration + Double badNumber = 1 / 0; // force MathException + } catch (Exception ex) { + sut.error(ex, 'myClass'); + } + try { + //noinspection ApexUnusedDeclaration + Double badNumber = 1 / 0; // force MathException + } catch (Exception ex) { + sut.error(ex, 'myClass', 'idValue'); + } + } + @IsTest static void debugWritesCorrectValues() { diff --git a/force-app/main/default/flexipages/App_Log_Home.flexipage-meta.xml b/force-app/main/default/flexipages/App_Log_Home.flexipage-meta.xml new file mode 100644 index 0000000..3731629 --- /dev/null +++ b/force-app/main/default/flexipages/App_Log_Home.flexipage-meta.xml @@ -0,0 +1,63 @@ + + + Consolidated view of Log Reader, Log Storage, and Log Writer + + + + + defaultLogLevels + INFO,DEBUG,WARN,ERROR + + + defaultLogsPerPage + 10 + + + useIcons + true + + logReader + c_logReader + + + top + Region + + + bottomLeft + Region + + + bottomRight + Region + + + + + + defaultDateFilter + THIS_WEEK + + + refreshInterval + 30 + + appLogStorage + c_appLogStorage + + + + + appLogWriter + c_appLogWriter + + + sidebar + Region + + App Log Home + + HomePage + diff --git a/force-app/main/default/lwc/appLogStorage/appLogStorage.css b/force-app/main/default/lwc/appLogStorage/appLogStorage.css new file mode 100644 index 0000000..a1d2de7 --- /dev/null +++ b/force-app/main/default/lwc/appLogStorage/appLogStorage.css @@ -0,0 +1,6 @@ +.INFO{ + --slds-c-icon-color-foreground-default: #0176d3; /*blue 50*/ + } + .DEBUG{ + --slds-c-icon-color-foreground-default: #06a59a; /*teal 60*/ + } \ No newline at end of file diff --git a/force-app/main/default/lwc/appLogStorage/appLogStorage.html b/force-app/main/default/lwc/appLogStorage/appLogStorage.html index 348ed8a..2928161 100644 --- a/force-app/main/default/lwc/appLogStorage/appLogStorage.html +++ b/force-app/main/default/lwc/appLogStorage/appLogStorage.html @@ -1,30 +1,99 @@ +* List view of log entries with filtering and management capabilities +* @author mikelockett +* @contributors Ryan Schierholz (added date filtering, log deletion, and automatic refresh) +* @created 6/3/23 +* @updated 12/2/23 +* @description Shows aggregate log counts by level with the ability to: +* - Filter by date ranges +* - Delete logs by level +* - Auto-refresh data +* - View earliest log dates +-->