{"id":17405064,"url":"https://github.com/jongpie/nebulalogger","last_synced_at":"2026-02-24T20:33:11.520Z","repository":{"id":37830246,"uuid":"136146618","full_name":"jongpie/NebulaLogger","owner":"jongpie","description":"The most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations.","archived":false,"fork":false,"pushed_at":"2025-07-21T21:25:44.000Z","size":184447,"stargazers_count":815,"open_issues_count":60,"forks_count":200,"subscribers_count":29,"default_branch":"main","last_synced_at":"2025-07-21T22:25:28.051Z","etag":null,"topics":["apex","aura","flow","lightning-component","lightning-web-components","logging","lwc","observability","platform-events","process-builder","real-time-monitoring","salesforce","salesforce-admins","salesforce-architects","salesforce-developers","unified-logging"],"latest_commit_sha":null,"homepage":"https://nebulalogger.com","language":"Apex","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/jongpie.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"CONTRIBUTING.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":["jongpie"]}},"created_at":"2018-06-05T08:37:42.000Z","updated_at":"2025-07-21T20:15:15.000Z","dependencies_parsed_at":"2023-02-19T13:01:31.044Z","dependency_job_id":"cded11ee-71b9-4df8-be90-87373fd18eff","html_url":"https://github.com/jongpie/NebulaLogger","commit_stats":{"total_commits":225,"total_committers":23,"mean_commits":9.782608695652174,"dds":"0.17333333333333334","last_synced_commit":"cf04d95440c2e6a63f554913d08a0f6ff1b2e4fb"},"previous_names":[],"tags_count":152,"template":false,"template_full_name":null,"purl":"pkg:github/jongpie/NebulaLogger","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jongpie%2FNebulaLogger","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jongpie%2FNebulaLogger/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jongpie%2FNebulaLogger/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jongpie%2FNebulaLogger/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/jongpie","download_url":"https://codeload.github.com/jongpie/NebulaLogger/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/jongpie%2FNebulaLogger/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267103468,"owners_count":24036484,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-26T02:00:08.937Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["apex","aura","flow","lightning-component","lightning-web-components","logging","lwc","observability","platform-events","process-builder","real-time-monitoring","salesforce","salesforce-admins","salesforce-architects","salesforce-developers","unified-logging"],"created_at":"2024-10-16T20:23:01.136Z","updated_at":"2026-02-24T20:33:11.504Z","avatar_url":"https://github.com/jongpie.png","language":"Apex","funding_links":["https://github.com/sponsors/jongpie"],"categories":[],"sub_categories":[],"readme":"# Nebula Logger for Salesforce\n\n[![Build](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml/badge.svg)](https://github.com/jongpie/NebulaLogger/actions/workflows/build.yml)\n[![codecov](https://codecov.io/gh/jongpie/NebulaLogger/branch/main/graph/badge.svg?token=1DJPDRM3N4)](https://codecov.io/gh/jongpie/NebulaLogger)\n\nThe most robust observability solution for Salesforce experts. Built 100% natively on the platform, and designed to work seamlessly with Apex, Lightning Components, Flow, OmniStudio, and integrations.\n\n## Unlocked Package - v4.17.2\n\n[![Install Unlocked Package in a Sandbox](./images/btn-install-unlocked-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?p0=04tg700000015gnAAA)\n[![Install Unlocked Package in Production](./images/btn-install-unlocked-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?p0=04tg700000015gnAAA)\n[![View Documentation](./images/btn-view-documentation.png)](https://github.com/jongpie/NebulaLogger/wiki)\n\n`sf package install --wait 20 --security-type AdminsOnly --package 04tg700000015gnAAA`\n\n---\n\n## Managed Package - v4.17.0\n\n[![Install Managed Package in a Sandbox](./images/btn-install-managed-package-sandbox.png)](https://test.salesforce.com/packaging/installPackage.apexp?mgd=true\u0026p0=04tg70000000r5xAAA)\n[![Install Managed Package in Production](./images/btn-install-managed-package-production.png)](https://login.salesforce.com/packaging/installPackage.apexp?mgd=true\u0026p0=04tg70000000r5xAAA)\n[![View Milestone](./images/btn-view-managed-package-milestone.png)](https://github.com/jongpie/NebulaLogger/milestone/16?closed=1)\n\n`sf package install --wait 30 --security-type AdminsOnly --package 04tg70000000r5xAAA`\n\n---\n\n\u003e [!NOTE]\n\u003e Starting in September 2024, Nebula Logger's documentation is being rewritten \u0026 consolidated into [the wiki](https://github.com/jongpie/NebulaLogger/wiki). Most of the content show below will eventually be migrated to the wiki instead.\n\n## Features\n\n1. A unified logging tool that supports easily adding log entries across the Salesforce platform, using:\n\n   - [Apex](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-Apex): classes, triggers, and anonymous Apex scripts\n   - [Lightning Components](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-Components): lightning web components (LWCs) \u0026 aura components\n   - [Flow \u0026 Process Builder](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-Flow): any Flow type that supports invocable actions\n   - [OmniStudio](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-OmniStudio): omniscripts and omni integration procedures\n\n2. Built with an event-driven pub/sub messaging architecture, using `LogEntryEvent__e` [platform events](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_intro.htm). For more details on leveraging platform events, see [the Platform Events Developer Guide site](https://developer.salesforce.com/docs/atlas.en-us.platform_events.meta/platform_events/platform_events_subscribe_cometd.htm)\n\n3. Actionable observability data about your Salesforce org, available directly in your Salesforce org via the 5 included custom objects\n\n   - `Log__c`\n   - `LogEntry__c`\n   - `LogEntryTag__c`\n   - `LoggerTag__c`\n   - `LoggerScenario__c`\n\n4. Customizable logging settings for different users \u0026 profiles, using the included `LoggerSettings__c` custom hierarchy settings object\n5. Easily scales in highly complex Salesforce orgs with large data volumes, using global feature flags in `LoggerParameter__mdt`\n6. Automatic data masking of sensitive data, using rules configured in the `LogEntryDataMaskRule__mdt` custom metadata type object\n7. View related `LogEntry__c` records on any Lightning record page in App Builder by adding the 'Related Log Entries' component (`relatedLogEntries` LWC)\n8. Dynamically assign tags to `Log__c` and `LogEntry__c` records for tagging/labeling your logs\n9. Extendable with a built-in plugin framework: easily build or install plugins that enhance Nebula Logger, using Apex or Flow (not currently available in the managed package)\n10. ISVs \u0026 package developers have several options for leveraging Nebula Logger in your own packages\n\n    - **Optional Dependency**: dynamically leverage Nebula Logger in your own packages - when it's available in a subscriber's org - using [Apex's `Callable` interface](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_Callable.htm) and Nebula Logger's included implementation `CallableLogger` (requires `v4.14.10` of Nebula Logger or newer)\n    - **Hard Dependency**: add either Nebula Logger's unlocked (no namespace) package or its managed package (`Nebula` namespace) as a dependency for your package to ensure customers always have a version of Nebula Logger installed\n    - **No Dependency**: Bundle Nebula Logger's metadata into your own project - all of Nebula Logger's metadata is fully open source \u0026 freely available. This approach provides with full control of what's included in your own app/project.\n\nLearn more about the design and history of the project on [Joys Of Apex blog post](https://www.joysofapex.com/advanced-logging-using-nebula-logger/)\n\n## Architecture Overview\n\nNebula Logger is built natively on Salesforce, using Apex, lightning components and various types of objects. There are no required external dependencies. To learn more about the architecture, check out the\n[architecture overview in the wiki](https://raw.githubusercontent.com/wiki/jongpie/NebulaLogger/images/nebula-logger-architecture-overview.png).\n\n\u003ca href=\"https://github.com/jongpie/NebulaLogger/wiki/Architecture\" target=\"_blank\"\u003e\n    \u003cimg src=\"https://raw.githubusercontent.com/wiki/jongpie/NebulaLogger/images/nebula-logger-architecture-overview.png\" width=\"300\"\u003e\n\u003c/a\u003e\n\n## Installing\n\nNebula Logger is available as both an unlocked package and a managed package. The metadata is the same in both packages, but there are some differences in the available functionality \u0026 features. All examples in `README` are for the unlocked package (no namespace) - simply add the `Nebula` namespace to the examples if you are using the managed package.\n\n\u003ctable\u003e\n    \u003cthead\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003c/th\u003e\n            \u003cth\u003eUnlocked Package (Recommended)\u003c/th\u003e\n            \u003cth\u003eManaged Package\u003c/th\u003e\n        \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eNamespace\u003c/td\u003e\n            \u003ctd\u003enone\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eNebula\u003c/code\u003e\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eFuture Releases\u003c/td\u003e\n            \u003ctd\u003eFaster release cycle: new patch versions are released (e.g., \u003ccode\u003ev4.4.x\u003c/code\u003e) for new enhancements \u0026 bugfixes that are merged to the \u003ccode\u003emain\u003c/code\u003e branch in GitHub\u003c/td\u003e\n            \u003ctd\u003eSlower release cycle: new minor versions are only released (e.g., \u003ccode\u003ev4.x\u003c/code\u003e) once new enhancements \u0026 bugfixes have been tested and code is stabilized\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003ePublic \u0026 Protected Apex Methods\u003c/td\u003e\n            \u003ctd\u003eAny \u003ccode\u003epublic\u003c/code\u003e and \u003ccode\u003eprotected\u003c/code\u003e Apex methods are subject to change in the future - they can be used, but you may encounter deployment issues if future changes to \u003ccode\u003epublic\u003c/code\u003e and \u003ccode\u003eprotected\u003c/code\u003e methods are not backwards-compatible\u003c/td\u003e\n            \u003ctd\u003eOnly \u003ccode\u003eglobal\u003c/code\u003e methods are available in managed packages - any \u003ccode\u003eglobal\u003c/code\u003e Apex methods available in the managed package will be supported for the foreseeable future\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eApex Debug Statements\u003c/td\u003e\n            \u003ctd\u003e\u003ccode\u003eSystem.debug()\u003c/code\u003e is automatically called - the output can be configured with \u003ccode\u003eLoggerSettings__c.SystemLogMessageFormat__c\u003c/code\u003e to use any field on \u003ccode\u003eLogEntryEvent__e\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003eRequires adding your own calls for \u003ccode\u003eSystem.debug()\u003c/code\u003e due to Salesforce limitations with managed packages\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eLogger Plugin Framework\u003c/td\u003e\n            \u003ctd\u003eLeverage Apex or Flow to build your own \"plugins\" for Logger - easily add your own automation to the any of the included objects: \u003ccode\u003eLogEntryEvent__e\u003c/code\u003e, \u003ccode\u003eLog__c\u003c/code\u003e, \u003ccode\u003eLogEntry__c\u003c/code\u003e, \u003ccode\u003eLogEntryTag__c\u003c/code\u003e and \u003ccode\u003eLoggerTag__c\u003c/code\u003e. The logger system will then automatically run your plugins for each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on).\u003c/td\u003e\n            \u003ctd\u003eThis functionality is not currently available in the managed package\u003c/td\u003e\n        \u003c/tr\u003e\n    \u003c/tbody\u003e\n\u003c/table\u003e\n\n---\n\n## Getting Started\n\nAfter installing Nebula Logger in your org, there are a few additional configuration changes needed...\n\n- Assign permission set(s) to users\n  - See the wiki page [Assigning Permission Sets](https://github.com/jongpie/NebulaLogger/wiki/Assigning-Permission-Sets) for more details on each of the included permission sets\n- Customize the default settings in `LoggerSettings__c`\n  - You can customize settings at the org, profile and user levels\n\n---\n\n### Logger for Apex: Quick Start\n\nFor Apex developers, the `Logger` class has several methods that can be used to add entries with different logging levels. Each logging level's method has several overloads to support multiple parameters.\n\n```apex\n// This will generate a debug statement within developer console\nSystem.debug('Debug statement using native Apex');\n\n// This will create a new `Log__c` record with multiple related `LogEntry__c` records\nLogger.error('Add log entry using Nebula Logger with logging level == ERROR');\nLogger.warn('Add log entry using Nebula Logger with logging level == WARN');\nLogger.info('Add log entry using Nebula Logger with logging level == INFO');\nLogger.debug('Add log entry using Nebula Logger with logging level == DEBUG');\nLogger.fine('Add log entry using Nebula Logger with logging level == FINE');\nLogger.finer('Add log entry using Nebula Logger with logging level == FINER');\nLogger.finest('Add log entry using Nebula Logger with logging level == FINEST');\nLogger.saveLog();\n```\n\nThis results in 1 `Log__c` record with several related `LogEntry__c` records.\n\n![Apex Log Results](./images/apex-log.png)\n\n---\n\n### Logger for Lightning Components: Quick Start\n\nFor lightning component developers, the `logger` LWC provides very similar functionality that is offered in Apex. Simply incorporate the `logger` LWC into your component, and call the desired logging methods within your code.\n\n```javascript\n// For LWC, import logger's getLogger() function into your component\nimport { getLogger } from 'c/logger';\n\nexport default class LoggerDemo extends LightningElement {\n  logger = getLogger();\n\n  connectedCallback() {\n    this.logger.info('Hello, world');\n    this.logger.saveLog();\n  }\n}\n```\n\n```javascript\n// For aura, retrieve logger from your component's markup\nconst logger = component.find('logger');\n\nlogger.error('Hello, world!').addTag('some important tag');\nlogger.warn('Hello, world!');\nlogger.info('Hello, world!');\nlogger.debug('Hello, world!');\nlogger.fine('Hello, world!');\nlogger.finer('Hello, world!');\nlogger.finest('Hello, world!');\nlogger.saveLog();\n```\n\n---\n\n### Logger for Flow \u0026 Process Builder: Quick Start\n\nWithin Flow \u0026 Process Builder, you can select 1 of the several Logging actions\n\n![Flow Logger Actions](./images/flow-logger-actions.png)\n\nIn this simple example, a Flow is configured after-insert and after-update to log a Case record (using the action 'Add Log Entry for an SObject Record')\n\n![Flow Builder: Log Case](./images/flow-builder-log-case.png)\n\nThis results in a `Log__c` record with related `LogEntry__c` records.\n\n![Flow Log Results](./images/flow-log.png)\n\n---\n\n### Logger for OmniStudio: Quick Start\n\nFor OmniStudio builders, the included Apex class `CallableLogger` provides access to Nebula Logger's core features, directly in omniscripts and omni integration procedures. Simply use the `CallableLogger` class as a remote action within OmniStudio, and provide any inputs needed for the logging action. For more details (including what actions are available, and their required inputs), see [the section on the `CallableLogger` Apex class](https://github.com/jongpie/NebulaLogger/wiki/Dynamically-Call-Logger). For more details on logging in OmniStudio, [see the OmniStudio wiki page](https://github.com/jongpie/NebulaLogger/wiki/Logging-in-OmniStudio)\n\n---\n\n## Features for Apex Developers\n\nWithin Apex, there are several different methods that you can use that provide greater control over the logging system.\n\n### Transaction Controls\n\nApex developers can use additional `Logger` methods to dynamically control how logs are saved during the current transaction.\n\n- `Logger.suspendSaving()` – causes `Logger` to ignore any calls to `saveLog()` in the current transaction until `resumeSaving()` is called. Useful for reducing DML statements used by `Logger`\n- `Logger.resumeSaving()` – re-enables saving after `suspendSaving()` is used\n- `Logger.flushBuffer()` – discards any unsaved log entries\n- `Logger.setSaveMethod(SaveMethod saveMethod)` - sets the default save method used when calling `saveLog()`. Any subsequent calls to `saveLog()` in the current transaction will use the specified save method\n- `Logger.saveLog(SaveMethod saveMethod)` - saves any entries in Logger's buffer, using the specified save method for only this call. All subsequent calls to `saveLog()` will use the default save method.\n- Enum `Logger.SaveMethod` - this enum can be used for both `Logger.setSaveMethod(saveMethod)` and `Logger.saveLog(saveMethod)`\n  - `Logger.SaveMethod.EVENT_BUS` - The default save method, this uses the `EventBus` class to publish `LogEntryEvent__e` records. The default save method can also be controlled declaratively by updating the field `LoggerSettings__c.DefaultSaveMethod__c`\n  - `Logger.SaveMethod.QUEUEABLE` - This save method will trigger `Logger` to save any pending records asynchronously using a queueable job. This is useful when you need to defer some CPU usage and other limits consumed by Logger.\n  - `Logger.SaveMethod.REST` - This save method will use the current user’s session ID to make a synchronous callout to the org’s REST API. This is useful when you have other callouts being made and you need to avoid mixed DML operations.\n  - `Logger.SaveMethod.SYNCHRONOUS_DML` - This save method will skip publishing the `LogEntryEvent__e` platform events, and instead immediately creates `Log__c` and `LogEntry__c` records. This is useful when you are logging from within the context of another platform event and/or you do not anticipate any exceptions to occur in the current transaction. **Note**: when using this save method, any exceptions will prevent your log entries from being saved - Salesforce will rollback any DML statements, including your log entries! Use this save method cautiously.\n\n### Track Related Logs in Batchable and Queuable Jobs\n\nIn Salesforce, asynchronous jobs like batchable and queuable run in separate transactions - each with their own unique transaction ID. To relate these jobs back to the original log, Apex developers can use the method Logger.setParentLogTransactionId(String). `Logger` uses this value to relate child `Log__c` records, using the field `Log__c.ParentLog__c`.\n\nThis example batchable class shows how you can leverage this feature to relate all of your batch job’s logs together.\n\n\u003e :information_source: If you deploy this example class to your org,you can run it using `Database.executeBatch(new BatchableLoggerExample());`\n\n```apex\npublic with sharing class BatchableLoggerExample implements Database.Batchable\u003cSObject\u003e, Database.Stateful {\n  private String originalTransactionId;\n\n  public Database.QueryLocator start(Database.BatchableContext batchableContext) {\n    // Each batchable method runs in a separate transaction,\n    // so store the first transaction ID to later relate the other transactions\n    this.originalTransactionId = Logger.getTransactionId();\n\n    Logger.info('Starting BatchableLoggerExample');\n    Logger.saveLog();\n\n    // Just as an example, query all accounts\n    return Database.getQueryLocator([SELECT Id, Name, RecordTypeId FROM Account]);\n  }\n\n  public void execute(Database.BatchableContext batchableContext, List\u003cAccount\u003e scope) {\n    // One-time call (per transaction) to set the parent log\n    Logger.setParentLogTransactionId(this.originalTransactionId);\n\n    for (Account account : scope) {\n      // Add your batch job's logic here\n\n      // Then log the result\n      Logger.info('Processed an account record', account);\n    }\n\n    Logger.saveLog();\n  }\n\n  public void finish(Database.BatchableContext batchableContext) {\n    // The finish method runs in yet-another transaction, so set the parent log again\n    Logger.setParentLogTransactionId(this.originalTransactionId);\n\n    Logger.info('Finishing running BatchableLoggerExample');\n    Logger.saveLog();\n  }\n}\n```\n\nQueueable jobs can also leverage the parent transaction ID to relate logs together. This example queueable job will run several chained instances. Each instance uses the parentLogTransactionId to relate its log back to the original instance's log.\n\n\u003e :information_source: If you deploy this example class to your org,you can run it using `System.enqueueJob(new QueueableLoggerExample(3));`\n\n```apex\npublic with sharing class QueueableLoggerExample implements Queueable {\n  private Integer numberOfJobsToChain;\n  private String parentLogTransactionId;\n\n  private List\u003cLogEntryEvent__e\u003e logEntryEvents = new List\u003cLogEntryEvent__e\u003e();\n\n  // Main constructor - for demo purposes, it accepts an integer that controls how many times the job runs\n  public QueueableLoggerExample(Integer numberOfJobsToChain) {\n    this(numberOfJobsToChain, null);\n  }\n\n  // Second constructor, used to pass the original transaction's ID to each chained instance of the job\n  // You don't have to use a constructor - a public method or property would work too.\n  // There just needs to be a way to pass the value of parentLogTransactionId between instances\n  public QueueableLoggerExample(Integer numberOfJobsToChain, String parentLogTransactionId) {\n    this.numberOfJobsToChain = numberOfJobsToChain;\n    this.parentLogTransactionId = parentLogTransactionId;\n  }\n\n  // Creates some log entries and starts a new instance of the job when applicable (based on numberOfJobsToChain)\n  public void execute(System.QueueableContext queueableContext) {\n    Logger.setParentLogTransactionId(this.parentLogTransactionId);\n\n    Logger.fine('queueableContext==' + queueableContext);\n    Logger.info('this.numberOfJobsToChain==' + this.numberOfJobsToChain);\n    Logger.info('this.parentLogTransactionId==' + this.parentLogTransactionId);\n\n    // Add your queueable job's logic here\n\n    Logger.saveLog();\n\n    --this.numberOfJobsToChain;\n    if (this.numberOfJobsToChain \u003e 0) {\n      String parentLogTransactionId = this.parentLogTransactionId != null ? this.parentLogTransactionId : Logger.getTransactionId();\n      System.enqueueJob(new QueueableLoggerExample(this.numberOfJobsToChain, parentLogTransactionId));\n    }\n  }\n}\n```\n\n### Overloads for Logging Methods\n\nEach of the logging methods in `Logger` (such as `Logger.error()`, `Logger.debug()`, and so on) has several static overloads for various parameters. These are intended to provide simple method calls for common parameters, such as:\n\n- Log a message and a record - `Logger.error(String message, SObject record)`\n- Log a message and a record ID - `Logger.error(String message, Id recordId)`\n- Log a message and a save result - `Logger.error(String message, Database.SaveResult saveResult)`\n- ...\n\nTo see the full list of overloads, check out the `Logger` class [documentation](https://jongpie.github.io/NebulaLogger/apex/Logger-Engine/Logger).\n\n### Using the Fluent Interface\n\nEach of the logging methods in `Logger` returns an instance of the class `LogEntryEventBuilder`. This class provides several additional methods together to further customize each log entry - each of the builder methods can be chained together. In this example Apex, 3 log entries are created using different approaches for calling `Logger` - all 3 approaches result in identical log entries.\n\n```apex\n// Get the current user so we can log it (just as an example of logging an SObject)\nUser currentUser = [SELECT Id, Name, Username, Email FROM User WHERE Id = :UserInfo.getUserId()];\n\n// Using static Logger method overloads\nLogger.debug('my string', currentUser);\n\n// Using the instance of LogEntryEventBuilder\nLogEntryEventBuilder builder = Logger.debug('my string');\nbuilder.setRecord(currentUser);\n\n// Chaining builder methods together\nLogger.debug('my string').setRecord(currentUser);\n\n// Save all of the log entries\nLogger.saveLog();\n```\n\n### Using LogMessage for Dynamically-Generated Strings\n\nThe class `LogMessage` provides the ability to generate string messages on demand, using `String.format()`. This provides 2 benefits:\n\n1. Improved CPU usage by skipping unnecessary calls to `String.format()`\n\n   ```apex // Without using LogMessage, String.format() is always called, even if the FINE logging level is not enabled for a user\n   String formattedString = String.format('my example with input: {0}', List\u003cObject\u003e{'myString'});\n   Logger.fine(formattedString);\n\n   // With LogMessage, when the specified logging level (FINE) is disabled for the current user, `String.format()` is not called\n   LogMessage logMessage = new LogMessage('my example with input: {0}', 'myString');\n   Logger.fine(logMessage);\n   ```\n\n2. Easily build complex strings\n   ```apex // There are several constructors for LogMessage to support different numbers of parameters for the formatted string\n    String unformattedMessage = 'my string with 3 inputs: {0} and then {1} and finally {2}';\n    String formattedMessage = new LogMessage(unformattedMessage, 'something', 'something else', 'one more').getMessage();\n    String expectedMessage = 'my string with 3 inputs: something and then something else and finally one more';\n    System.assertEquals(expectedMessage, formattedMessage);\n   ```\n\nFor more details, check out the `LogMessage` class [documentation](https://jongpie.github.io/NebulaLogger/apex/Logger-Engine/LogMessage).\n\n### ISVs \u0026 Package Developers: Dynamically Call Nebula Logger in Your Packages with `CallableLogger`\n\nAs of `v4.14.10`, Nebula Logger includes the Apex class `CallableLogger`, which implements [Apex's `Callable` interface](https://developer.salesforce.com/docs/atlas.en-us.apexref.meta/apexref/apex_interface_System_Callable.htm).\n\n- The `Callable` interface only has 1 method: `Object call(String action, Map\u003cString,Object\u003e args)`. It leverages string values and generic `Object` values as a mechanism to provide loose coupling on Apex classes that may or may not exist in a Salesforce org.\n- This can be used by ISVs \u0026 package developers to optionally leverage Nebula Logger for logging, when it's available in a customer's org. And when it's not available, your package can still be installed, and still be used.\n\nUsing the provided `CallableLogger` class, a subset of Nebula Logger's features can be called dynamically in Apex. For example, this sample Apex code adds \u0026 saves 2 log entries (when Nebula Logger is available).\n\n```apex\n// Check for both the managed package (Nebula namespace) and the unlocked package to see if either is available\nType nebulaLoggerType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');\nCallable nebulaLoggerInstance = (Callable) nebulaLoggerType?.newInstance();\nif (nebulaLoggerInstance == null) {\n  // If it's null, then neither of Nebula Logger's packages is available in the org 🥲\n  return;\n}\n\n// Example: Add a basic \"hello, world!\" INFO extry\nMap\u003cString, Object\u003e newEntryInput = new Map\u003cString, Object\u003e{\n  'loggingLevel' =\u003e System.LoggingLevel.INFO,\n  'message' =\u003e 'hello, world!'\n};\nnebulaLoggerInstance.call('newEntry', newEntryInput);\n\n// Example: Add an ERROR extry with an Apex exception\nException someException = new DmlException('oops');\nMap\u003cString, Object\u003e newEntryInput = new Map\u003cString, Object\u003e{\n  'exception' =\u003e someException,\n  'loggingLevel' =\u003e LoggingLevel.ERROR,\n  'message' =\u003e 'An unexpected exception was thrown'\n};\nnebulaLoggerInstance.call('newEntry', newEntryInput);\n\n// Example: Save any pending log entries\nnebulaLoggerInstance.call('saveLog', null);\n```\n\nFor more details, [visit the wiki](https://github.com/jongpie/NebulaLogger/wiki/Dynamically-Call-Nebula-Logger).\n\n---\n\n## Features for Lightning Component Developers\n\nFor lightning component developers, the included `logger` LWC can be used in other LWCs \u0026 aura components for frontend logging. Similar to `Logger` and `LogEntryBuilder` Apex classes, the LWC has both `logger` and `logEntryBuilder` classes. This provides a fluent API for JavaScript developers so they can chain the method calls.\n\nOnce you've incorporated `logger` into your lightning components, you can see your `LogEntry__c` records using the included list view \"All Component Log Entries'.\n\n![Component Log Entries List View](./images/component-entries-list-view.png)\n\nEach `LogEntry__c` record automatically stores the component's type ('Aura' or 'LWC'), the component name, and the component function that called `logger`. This information is shown in the section \"Lightning Component Information\"\n\n![Component Log Entry Record](./images/component-entry-record-detail.png)\n\n#### Example LWC Usage\n\nFor lightning component developers, the `logger` LWC provides very similar functionality that is offered in Apex. Simply import the `getLogger` function in your component, use it to initialize an instance once per component, and call the desired logging methods within your code.\n\n```javascript\n// For LWC, import logger's createLogger() function into your component\nimport { getLogger } from 'c/logger';\nimport callSomeApexMethod from '@salesforce/apex/LoggerLWCDemoController.callSomeApexMethod';\n\nexport default class LoggerDemo extends LightningElement {\n  // Call getLogger() once per component\n  logger = getLogger();\n\n  async connectedCallback() {\n    this.logger.setScenario('some scenario');\n    this.logger.finer('initialized demo LWC, using async connectedCallback');\n  }\n\n  @wire(callSomeApexMethod)\n  wiredCallSomeApexMethod({ error, data }) {\n    this.logger.info('logging inside a wire function');\n    if (data) {\n      this.logger.info('wire function return value: ' + data);\n    }\n    if (error) {\n      this.logger.error('wire function error: ' + JSON.stringify(error));\n    }\n  }\n\n  logSomeStuff() {\n    this.logger.error('Add log entry using Nebula Logger with logging level == ERROR').addTag('some important tag');\n    this.logger.warn('Add log entry using Nebula Logger with logging level == WARN');\n    this.logger.info('Add log entry using Nebula Logger with logging level == INFO');\n    this.logger.debug('Add log entry using Nebula Logger with logging level == DEBUG');\n    this.logger.fine('Add log entry using Nebula Logger with logging level == FINE');\n    this.logger.finer('Add log entry using Nebula Logger with logging level == FINER');\n    this.logger.finest('Add log entry using Nebula Logger with logging level == FINEST');\n\n    this.logger.saveLog();\n  }\n\n  doSomething(event) {\n    this.logger.finest('Starting doSomething() with event: ' + JSON.stringify(event));\n    try {\n      this.logger.debug('TODO - finishing implementation of doSomething()').addTag('another tag');\n      // TODO add the function's implementation below\n    } catch (thrownError) {\n      this.logger\n        .error('An unexpected error log entry using Nebula Logger with logging level == ERROR')\n        .setExceptionDetails(thrownError)\n        .addTag('some important tag');\n    } finally {\n      this.logger.saveLog();\n    }\n  }\n}\n```\n\n#### Example Aura Usage\n\nTo use the logger component, it has to be added to your aura component's markup:\n\n```html\n\u003caura:component implements=\"force:appHostable\"\u003e\n  \u003cc:logger aura:id=\"logger\" /\u003e\n\n  \u003cdiv\u003eMy component\u003c/div\u003e\n\u003c/aura:component\u003e\n```\n\nOnce you've added logger to your markup, you can call it in your aura component's controller:\n\n```javascript\n({\n  logSomeStuff: function (component, event, helper) {\n    const logger = component.find('logger');\n\n    logger.error('Hello, world!').addTag('some important tag');\n    logger.warn('Hello, world!');\n    logger.info('Hello, world!');\n    logger.debug('Hello, world!');\n    logger.fine('Hello, world!');\n    logger.finer('Hello, world!');\n    logger.finest('Hello, world!');\n\n    logger.saveLog();\n  }\n});\n```\n\n---\n\n## Features for Flow Builders\n\nWithin Flow (and Process Builder), there are 4 invocable actions that you can use to leverage Nebula Logger\n\n1. 'Add Log Entry' - uses the class `FlowLogEntry` to add a log entry with a specified message\n2. 'Add Log Entry for an SObject Record' - uses the class `FlowRecordLogEntry` to add a log entry with a specified message for a particular SObject record\n3. 'Add Log Entry for an SObject Record Collection' - uses the class `FlowCollectionLogEntry` to add a log entry with a specified message for an SObject record collection\n4. 'Save Log' - uses the class `Logger` to save any pending logs\n\n![Flow Builder: Logging Invocable Actions](./images/flow-builder-logging-invocable-actions.png)\n\n---\n\n## Tagging Your Log Entries\n\nNebula Logger supports dynamically tagging/labeling your `LogEntry__c` records via Apex, Flow, and custom metadata records in `LogEntryTagRule__mdt`. Tags can then be stored using one of the two supported modes (discussed below).\n\n### Adding Tags in Apex\n\nApex developers can use 2 new methods in `LogEntryBuilder` to add tags - `LogEntryEventBuilder.addTag(String)` and `LogEntryEventBuilder.addTags(List\u003cString\u003e)`.\n\n```apex\n// Use addTag(String tagName) for adding 1 tag at a time\nLogger.debug('my log message').addTag('some tag').addTag('another tag');\n\n// Use addTags(List\u003cString\u003e tagNames) for adding a list of tags in 1 method call\nList\u003cString\u003e myTags = new List\u003cString\u003e{'some tag', 'another tag'};\nLogger.debug('my log message').addTags(myTags);\n```\n\n### Adding Tags in Flow\n\nFlow builders can use the `Tags` property to specify a comma-separated list of tags to apply to the log entry. This feature is available for all 3 Flow classes: `FlowLogEntry`, `FlowRecordLogEntry` and `FlowCollectionLogEntry`.\n\n![Flow Logging with Tags](./images/flow-builder-log-with-tags.png)\n\n### Adding Tags with Custom Metadata Records\n\nAdmins can configure tagging rules to append additional tags using the custom metadata type `LogEntryTagRule__mdt`.\n\n- Rule-based tags are only added when `LogEntry__c` records are created (not on update).\n- Rule-based tags are added in addition to any tags that have been added via Apex and/or Flow.\n- Each rule is configured to apply tags based on the value of a single field on `LogEntry__c` (e.g., `LogEntry__c.Message__c`).\n- Each rule can only evaluate 1 field, but multiple rules can evaluate the same field.\n- A single rule can apply multiple tags. When specifying multiple tags, put each tag on a separate line within the Tags field (`LogEntryTagRule__mdt.Tags__c`).\n\nRules can be set up by configuring a custom metadata record with these fields configured:\n\n1. Logger SObject: currently, only the \"Log Entry\" object (`LogEntry__c`) is supported.\n2. Field: the SObject's field that should be evaluated - for example, `LogEntry__c.Message__c`. Only 1 field can be selected per rule, but multiple rules can use the same field.\n3. Comparison Type: the type of operation you want to use to compare the field's value. Currently supported options are: `CONTAINS`, `EQUALS`, `MATCHES_REGEX`, and `STARTS_WITH`.\n4. Comparison Value: the comparison value that should be used for the selected field operation.\n5. Tags: a list of tag names that should be dynamically applied to any matching `LogEntry__c` records.\n6. Is Enabled: only enabled rules are used by Logger - this is a handy way to easily enable/disable a particular rule without having to entirely delete it.\n\nBelow is an example of what a rule looks like once configured. Based on this rule, any `LogEntry__c` records that contain \"My Important Text\" in the `Message__c` field will automatically have 2 tags added - \"Really important tag\" and \"A tag with an emoji, whynot?! 🔥\"\n\n![Tag Rule Example](./images/tag-rule-example.png)\n\n### Choosing a Tagging Mode\n\nOnce you've implementing log entry tagging within Apex or Flow, you can choose how the tags are stored within your org. Each mode has its own pros and cons - you can also build your own plugin if you want to leverage your own tagging system (note: plugins are not currently available in the managed package).\n\n\u003ctable\u003e\n    \u003cthead\u003e\n        \u003ctr\u003e\n            \u003cth\u003e\u003cstrong\u003eTagging Mode\u003c/strong\u003e\u003c/th\u003e\n            \u003cth\u003eLogger's Custom Tagging Objects (Default)\u003c/th\u003e\n            \u003cth\u003eSalesforce \u003ccode\u003eTopic\u003c/code\u003e and \u003ccode\u003eTopicAssignment\u003c/code\u003e Objects\u003c/th\u003e\n        \u003c/tr\u003e\n    \u003c/thead\u003e\n    \u003ctbody\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eSummary\u003c/td\u003e\n            \u003ctd\u003eStores your tags in custom objects \u003ccode\u003eLoggerTag__c\u003c/code\u003e and \u003ccode\u003eLogEntryTag__c\u003c/code\u003e\u003c/td\u003e\n            \u003ctd\u003eLeverages Salesforce's \u003ca href=\"https://www.salesforce.com/products/chatter/features/online-collaboration-tools\"\u003eChatter Topics functionality\u003c/a\u003e to store your tags. This mode is not available in the managed package.\u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eData Model\u003c/td\u003e\n            \u003ctd\u003e\n                \u003cul\u003e\n                    \u003cli\u003e\u003ccode\u003eLoggerTag__c\u003c/code\u003e: this represents the actual tag you want to apply to your log entry record. Tags are unique, based on the field \u003ccode\u003eLoggerTag__c.Name\u003c/code\u003e. The logging system will automatically create \u003ccode\u003eLoggerTag__c\u003c/code\u003e records if a matching record does not already exist in your org.\u003c/li\u003e\n                    \u003cli\u003e\u003ccode\u003eLogEntryTag__c\u003c/code\u003e: a junction object between \u003ccode\u003eLoggerTag__c\u003c/code\u003e and \u003ccode\u003eLogEntry__c\u003c/code\u003e\u003c/li\u003e\n                \u003c/ul\u003e\n            \u003c/td\u003e\n            \u003ctd\u003e\n                \u003cul\u003e\n                    \u003cli\u003e\u003ccode\u003eTopic\u003c/code\u003e: a standard object, used to tag any Salesforce record (with Topics enabled). This object is used similar to how \u003ccode\u003eLoggerTag__c\u003c/code\u003e is used. See \u003ca href=\"https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_topic.htm\"\u003eobject reference docs for more details\u003c/a\u003e\u003c/li\u003e\n                    \u003cli\u003e\u003ccode\u003eTopicAssignment\u003c/code\u003e: a junction object between a \u003ccode\u003eTopic\u003c/code\u003e record and any (supported) SObject, using a polymorphic \u003ccode\u003eEntityId\u003c/code\u003e field. See \u003ca href=\"https://developer.salesforce.com/docs/atlas.en-us.object_reference.meta/object_reference/sforce_api_objects_topicassignment.htm\"\u003eobject reference docs for more details\u003c/a\u003e\u003c/li\u003e\n                \u003c/ul\u003e\n            \u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eData Visibility\u003c/td\u003e\n            \u003ctd\u003e\n                \u003cul\u003e\n                    \u003cli\u003eAccess to the \u003ccode\u003eLoggerTag__c\u003c/code\u003e object can be granted/restricted using standard Salesforce object and record-sharing functionality (OWD, sharing rules, profiles, permission sets, etc). By default, \u003ccode\u003eLoggerTag__c\u003c/code\u003e OWD is set to 'public read-only' for internal users and 'private' for external users\u003c/li\u003e\n                    \u003cli\u003eSince \u003ccode\u003eLogEntryTag__c\u003c/code\u003e is a junction object, access to these records is controlled by a user's access to the related \u003ccode\u003eLogEntry__c\u003c/code\u003e and \u003ccode\u003eLoggerTag__c\u003c/code\u003e records\u003c/li\u003e\n                \u003c/ul\u003e\n            \u003c/td\u003e\n            \u003ctd\u003e\n                \u003cul\u003e\n                    \u003cli\u003eIn Chatter, all \u003ccode\u003eTopic\u003c/code\u003e records are visible - including any \u003ccode\u003eTopic\u003c/code\u003e records created via Logger. For some orgs that are ok with this visibility within Chatter, this is considered a great feature. But for some orgs, this visibility may not be ideal.\u003c/li\u003e\n                    \u003cli\u003eAlthough \u003ccode\u003eTopic\u003c/code\u003e records are visible to all Chatter users, \u003ccode\u003eTopicAssignment\u003c/code\u003e records are only visible to users that have access to the related \u003ccode\u003eEntityId\u003c/code\u003e (in this case, the \u003ccode\u003eLogEntry__c\u003c/code\u003e record)\u003c/li\u003e\n                \u003c/ul\u003e\n            \u003c/td\u003e\n        \u003c/tr\u003e\n        \u003ctr\u003e\n            \u003ctd\u003eLeveraging Data\u003c/td\u003e\n            \u003ctd\u003eSince the data is stored in custom objects, you can leverage any platform functionality you want, such as building custom list views, reports \u0026 dashboards, enabling Chatter feeds, creating activities/tasks, and so on.\u003c/a\u003e\n            \u003ctd\u003eTopics can be used to \u003ca href=\"http://releasenotes.docs.salesforce.com/en-us/winter20/release-notes/rn_lex_lists_topic_filters.htm\"\u003efilter list views\u003c/a\u003e, which is a really useful feature. However, using Topics \u003ca href=\"https://trailblazer.salesforce.com/ideaView?id=08730000000l12wAAA\"\u003ein reports and dashboards is only partially implemented\u003c/a\u003e at this time.\n            \u003c/td\u003e\n        \u003c/tr\u003e\n    \u003c/tbody\u003e\n\u003c/table\u003e\n\n---\n\n## Adding Custom Fields to Nebula Logger's Data Model\n\nNebula Logger supports defining, setting, and mapping custom fields within Nebula Logger's data model. This is helpful in orgs that want to extend Nebula Logger's included data model by creating their own org/project-specific fields.\n\nThis feature requires that you populate your custom fields yourself, and is only available in Apex \u0026 JavaScript currently. The plan is to add in a future release the ability to also set custom fields via Flow.\n\n- `v4.13.14` added this functionality for Apex\n- `v4.14.6` added this functionality for JavaScript (lightning components)\n\n### Adding Custom Fields to the Platform Event `LogEntryEvent__e`\n\nThe first step is to add a field to the platform event `LogEntryEvent__e`\n\n- Create a custom field on `LogEntryEvent__e`. Any data type supported by platform events can be used.\n\n  - In this example, a custom text field called `SomeCustomField__c` has been added:\n\n    ![Custom Field on LogEntryEvent__e](./images/custom-field-log-entry-event.png)\n\n- In Apex, you have 2 ways to populate your custom fields\n\n  1. Set the field once per transaction - every `LogEntryEvent__e` logged in the transaction will then automatically have the specified field populated with the same value.\n     - This is typically used for fields that are mapped to an equivalent `Log__c` or `LoggerScenario__c` field.\n\n  - How: call the static method overloads `Logger.setField(Schema.SObjectField field, Object fieldValue)` or `Logger.setField(Map\u003cSchema.SObjectField, Object\u003e fieldToValue)`\n\n  2. Set the field on a specific `LogEntryEvent__e` record - other records will not have the field automatically set.\n     - This is typically used for fields that are mapped to an equivalent `LogEntry__c` field.\n     - How: call the instance method overloads `LogEntryEventBuilder.setField(Schema.SObjectField field, Object fieldValue)` or `LogEntryEventBuilder.setField(Map\u003cSchema.SObjectField, Object\u003e fieldToValue)`\n\n  ```apex\n  // Set My_Field__c on every log entry event created in this transaction with the same value\n  Logger.setField(LogEntryEvent__e.My_Field__c, 'some value that applies to the whole Apex transaction');\n\n  // Set fields on specific entries\n  Logger.warn('hello, world - \"a value\" set for Some_Other_Field__c').setField(LogEntryEvent__e.Some_Other_Field__c, 'a value')\n  Logger.warn('hello, world - \"different value\" set for Some_Other_Field__c').setField(LogEntryEvent__e.Some_Other_Field__c, 'different value')\n  Logger.info('hello, world - no value set for Some_Other_Field__c');\n\n  Logger.saveLog();\n  ```\n\n- In JavaScript, you have 2 ways to populate your custom fields. These are very similar to the 2 ways available in Apex (above).\n\n  1. Set the field once per component - every `LogEntryEvent__e` logged in your component will then automatically have the specified field populated with the same value.\n     - This is typically used for fields that are mapped to an equivalent `Log__c` or `LoggerScenario__c` field.\n\n  - How: call the `logger` LWC function `logger.setField(Object fieldToValue)`\n\n  2. Set the field on a specific `LogEntryEvent__e` record - other records will not have the field automatically set.\n     - This is typically used for fields that are mapped to an equivalent `LogEntry__c` field.\n     - How: call the instance function `LogEntryEventBuilder.setField(Object fieldToValue)`\n\n  ```javascript\n  import { getLogger } from 'c/logger';\n\n  export default class LoggerDemo extends LightningElement {\n    logger = getLogger();\n\n    connectedCallback() {\n      // Set My_Field__c on every log entry event created in this component with the same value\n      this.logger.setField({My_Field__c, 'some value that applies to any subsequent entry'});\n\n      // Set fields on specific entries\n      this.logger.warn('hello, world - \"a value\" set for Some_Other_Field__c').setField({ Some_Other_Field__c: 'a value' });\n      this.logger.warn('hello, world - \"different value\" set for Some_Other_Field__c').setField({ Some_Other_Field__c: 'different value' });\n      this.logger.info('hello, world - no value set for Some_Other_Field__c');\n\n      this.logger.saveLog();\n    }\n  }\n  ```\n\n### Adding Custom Fields to the Custom Objects `Log__c`, `LogEntry__c`, and `LoggerScenario__c`\n\nIf you want to store the data in one of Nebula Logger's custom objects, you can follow the above steps, and also...\n\n- Create an equivalent custom field on one of Nebula Logger's custom objects - right now, only `Log__c`, `LogEntry__c`, and `LoggerScenario__c` are supported.\n\n  - In this example, a custom text field _also_ called `SomeCustomField__c` has been added to `Log__c` object - this will be used to store the value of the field `LogEntryEvent__e.SomeCustomField__c`:\n\n    ![Custom Field on LogEntryEvent__e](./images/custom-field-log.png)\n\n- Create a record in the new CMDT `LoggerFieldMapping__mdt` to map the `LogEntryEvent__e` custom field to the custom object's custom field, shown below. Nebula Logger will automatically populate the custom object's target field with the value of the source `LogEntryEvent__e` field.\n\n  - In this example, a custom text field called `SomeCustomField__c` has been added to both `LogEntryEvent__e` and `Log__c`.\n\n    ![Custom Field on LogEntryEvent__e](./images/custom-field-mapping.png)\n\n---\n\n## Log Management\n\n### Logger Console App\n\nThe Logger Console app provides access to the tabs for Logger's objects: `Log__c`, `LogEntry__c`, `LogEntryTag__c` and `LoggerTag__c` (for any users with the correct access).\n\n![Logger Console app](./images/logger-console-app.png)\n\n### Log's 'Manage' Quick Action\n\nTo help development and support teams better manage logs (and any underlying code or config issues), some fields on `Log__c` are provided to track the owner, priority and status of a log. These fields are optional, but are helpful in critical environments (production, QA sandboxes, UAT sandboxes, etc.) for monitoring ongoing user activities.\n\n- All editable fields on `Log__c` can be updated via the 'Manage Log' quick action (shown below)\n\n  ![Manage Log QuickAction](./images/manage-log-quickaction.png)\n\n- Additional fields are automatically set based on changes to `Log__c.Status__c`\n  - `Log__c.ClosedBy__c` - The user who closed the log\n  - `Log__c.ClosedDate__c` - The datetime that the log was closed\n  - `Log__c.IsClosed__c` - Indicates if the log is closed, based on the selected status (and associated config in the 'Log Status' custom metadata type)\n  - `Log__c.IsResolved__c` - Indicates if the log is resolved (meaning that it required analaysis/work, which has been completed). Only closed statuses can be considered resolved. This is also driven based on the selected status (and associated config in the 'Log Status' custom metadata type)\n- To customize the statuses provided, simply update the picklist values for `Log__c.Status__c` and create/update corresponding records in the custom metadata type `LogStatus__mdt`. This custom metadata type controls which statuses are considered closed and resolved.\n\n---\n\n### Log's 'View JSON' Quick Action\n\nEveryone loves JSON - so to make it easy to see a JSON version of a `Log__c` record, you can use the 'View JSON' quick action button. It displays the current `Log__c` + all related `LogEntry__c` records in JSON format, as well as a handy button to copy the JSON to your clipboard. All fields that the current user can view (based on field-level security) are dynamically returned, including any custom fields added directly in your org or by plugins.\n\n![View JSON Log QuickAction Button](./images/view-json-log-quickaction-btn.png)\n\n![View JSON Log QuickAction](./images/view-json-log-quickaction.png)\n\n---\n\n### Real-Time Monitoring with Log Entry Event Stream\n\nWithin Logger Console app, the Log Entry Event Stream tab provides real-time monitoring of `LogEntryEvent__e` platform events. Simply open the tab to start monitoring, and use the filters to further refine with `LogEntryEvent__e` records display in the stream.\n\n![Log Entry Event Stream](./images/log-entry-event-stream.png)\n\n---\n\n### View Related Log Entries on a Record Page\n\nWithin App Builder, admins can add the 'Related Log Entries' lightning web component (lwc) to any record page. Admins can also control which columns are displayed be creating \u0026 selecting a field set on `LogEntry__c` with the desired fields.\n\n- The component automatically shows any related log entries, based on `LogEntry__c.RecordId__c == :recordId`\n- Users can search the list of log entries for a particular record using the component's built-insearch box. The component dynamically searches all related log entries using SOSL.\n- Component automatically enforces Salesforce's security model\n  - Object-Level Security - Users without read access to `LogEntry__c` will not see the component\n  - Record-Level Security - Users will only see records that have been shared with them\n  - Field-Level Security - Users will only see the fields within the field set that they have access to\n\n![Related Log Entries](./images/relate-log-entries-lwc.png)\n\n---\n\n### Deleting Old Logs\n\nAdmins can easily delete old logs using 2 methods: list views or Apex batch jobs\n\n#### Mass Deleting with List Views\n\nSalesforce (still) does not support mass deleting records out-of-the-box. There's been [an Idea for 11+ years](https://trailblazer.salesforce.com/ideaView?id=08730000000BqczAAC) about it, but it's still not standard functionality. A custom button is available on `Log__c` list views to provide mass deletion functionality.\n\n1. Admins can select 1 or more `Log__c` records from the list view to choose which logs will be deleted\n\n![Mass Delete Selection](./images/log-mass-delete-selection.png)\n\n2. The button shows a Visualforce page `LogMassDelete` to confirm that the user wants to delete the records\n\n![Mass Delete Confirmation](./images/log-mass-delete-confirmation.png)\n\n#### Batch Deleting with Apex Jobs\n\nTwo Apex classes are provided out-of-the-box to handle automatically deleting old logs\n\n1. `LogBatchPurger` - this batch Apex class will delete any `Log__c` records with `Log__c.LogRetentionDate__c \u003c= System.today()`.\n   - By default, this field is populated with \"TODAY + 14 DAYS\" - the number of days to retain a log can be customized in `LoggerSettings__c`.\n   - Admins can also manually edit this field to change the retention date - or set it to null to prevent the log from being automatically deleted\n2. `LogBatchPurgeScheduler` - this schedulable Apex class can be schedule to run `LogBatchPurger` on a daily or weekly basis\n\n---\n\n## Beta Feature: Custom Plugin Framework for Log\\_\\_c and LogEntry\\_\\_c objects\n\nIf you want to add your own automation to the `Log__c` or `LogEntry__c` objects, you can leverage Apex or Flow to define \"plugins\" - the logger system will then automatically run the plugins after each trigger event (BEFORE_INSERT, BEFORE_UPDATE, AFTER_INSERT, AFTER_UPDATE, and so on). This framework makes it easy to build your own plugins, or deploy/install others' prebuilt packages, without having to modify the logging system directly.\n\n- Flow plugins: your Flow should be built as auto-launched Flows with these parameters:\n\n  1. `Input` parameter `triggerOperationType` - The name of the current trigger operation (such as BEFORE_INSERT, BEFORE_UPDATE, etc.)\n  2. `Input` parameter `triggerNew` - The list of logger records being processed (`Log__c` or `LogEntry__c` records)\n  3. `Output` parameter `updatedTriggerNew` - If your Flow makes any updates to the collection of records, you should return a record collection containing the updated records\n  4. `Input` parameter `triggerOld` - The list of logger records as they exist in the datatabase\n\n- Apex plugins: your Apex class should implements `LoggerPlugin.Triggerable`. For example:\n\n  ```apex\n  public class ExampleTriggerablePlugin implements LoggerPlugin.Triggerable {\n    public void execute(LoggerPlugin__mdt configuration, LoggerTriggerableContext input) {\n      // Example: only run the plugin for Log__c records\n      if (context.sobjectType != Schema.Log__c.SObjectType) {\n        return;\n      }\n\n      List\u003cLog__c\u003e logs = (List\u003cLog__c\u003e) input.triggerNew;\n      switch on input.triggerOperationType {\n        when BEFORE_INSERT {\n          for (Log__c log : logs) {\n            log.Status__c = 'On Hold';\n          }\n        }\n        when BEFORE_UPDATE {\n          // TODO add before-update logic\n        }\n      }\n    }\n  }\n  ```\n\nOnce you've created your Apex or Flow plugin(s), you will also need to configure the plugin:\n\n- 'Logger Plugin' - use the custom metadata type `LoggerPlugin__mdt` to define your plugin, including the plugin type (Apex or Flow) and the API name of your plugin's Apex class or Flow\n- 'Logger Parameter' - use the custom metadata type `LoggerParameter__mdt` to define any configurable parameters needed for your plugin, such as environment-specific URLs and other similar configurations\n\nNote: the logger plugin framework is not available in the managed package due to some platform limitations \u0026 considerations with some of the underlying code. The unlocked package is recommended (instead of the managed package) when possible, including if you want to be able to leverage the plugin framework in your org.\n\n### Beta Plugin: Slack Integration\n\nThe optional [Slack plugin](./nebula-logger/plugins/slack) leverages the Nebula Logger plugin framework to automatically send Slack notifications for logs that meet a certain (configurable) logging level. The plugin also serves as a functioning example of how to build your own plugin for Nebula Logger, such as how to:\n\n- Use Apex to apply custom logic to `Log__c` and `LogEntry__c` records\n- Add custom fields and list views to Logger's objects\n- Extend permission sets to include field-level security for your custom fields\n- Leverage the new `LoggerParameter__mdt` CMDT object to store configuration for your plugin\n\nCheck out the [Slack plugin](./nebula-logger/plugins/slack) for more details on how to install \u0026 customize the plugin\n\n![Slack plugin: notification](./nebula-logger/plugins/slack/.images/slack-plugin-notification.png)\n\n---\n\n## Uninstalling/Removing Logger\n\nIf you want to remove the unlocked or managed packages, you can do so by simply uninstalling them in your org under Setup --\u003e Installed Packages.\n\n![Uninstall Packages](./images/installed-packages-uninstall-option.png)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjongpie%2Fnebulalogger","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjongpie%2Fnebulalogger","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjongpie%2Fnebulalogger/lists"}