Skip to content

Dynamically Call Nebula Logger

Jonathan Gillespie edited this page Sep 18, 2024 · 10 revisions

Overview

As of v4.14.10, Nebula Logger includes the ability for ISVs & 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.

This functionality is provided via the Apex class CallableLogger, which implements Apex's Callable interface.

  • The Callable interface only has 1 method: Object call(String action, Map<String,Object> args). It leverages string keys and generic Object values as a mechanism to provide loose coupling on Apex classes that may or may not exist in a Salesforce org.

  • The CallableLogger class provides dynamic access to Nebula Logger's core features.

  • Instances of the CallableLogger class be dynamically instantiated using something like this approach:

    // Check if some version of Nebula Logger is available in the org
    Type nebulaLoggerCallableType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');
    Callable nebulaLoggerCallable = (Callable) nebulaLoggerCallableType?.newInstance();
    if (nebulaLoggerCallable == null) {
      // If it's null, then Nebula Logger isn't available in the org 🥲, 
      // so we'll return early & skip logging in this example. You could also take 
      // some other approach here, like use a fallback logging tool, 
      // or call System.debug() instead, etc.
      
      return;
    }
    
    // If we made it here, then some version of Nebula Logger is available 🥳
    // So, now we know we can use it to log
    nebulaLoggerCallable.call('someAction', new Map<String, Object>();

    Quick Start Example

    This sample Apex class adds 2 log entries & saves them, if Nebula Logger is available in the current org. If not, it exits early / nothing happens.

    // Dynamically create a instance Nebula Logger's Callable Apex class (if it's available)
    Type nebulaLoggerCallableType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');
    Callable nebulaLoggerCallable = (Callable) nebulaLoggerCallableType?.newInstance();
    if (nebulaLoggerCallable == null) {
      return;
    }
    
    // Example action: Add a basic "hello, world!" INFO entry
    Map<String, Object> infoEntryInput = new Map<String, Object>{
      'loggingLevel' => System.LoggingLevel.INFO,
      'message' => 'hello, world!'
    };
    nebulaLoggerCallable.call('newEntry', infoEntryInput);
    
    // Example action: Add an ERROR entry with an Apex exception
    Exception someException = new DmlException('oops');
    Map<String, Object> errorEntryInput = new Map<String, Object>{
      'exception' => someException,
      'loggingLevel' => LoggingLevel.ERROR,
      'message' => 'An unexpected exception was thrown'
    };
    nebulaLoggerCallable.call('newEntry', errorEntryInput);
    
    // Example: Save any pending log entries
    nebulaLoggerCallable.call('saveLog', null);

Available Actions

There are currently 8 actions supported by CallablerLogger - each action (discussed below) essentially corresponds to a similar method in the core Logger Apex class.

OmniStudio Support

The actions in CallableLogger can also be used when logging in OmniStudio. As of Salesforce's Summer '21, OmniScripts and Integration Procedures support using Callable Apex classes via remote actions. In Salesforce's docs, it states:

Vlocity Apex classes and the Remote Actions of OmniScripts and Integration Procedures support the Callable interface beginning in the Summer '21 release.

Although the VlocityOpenInterface and VlocityOpenInterface2 interfaces enable flexible implementations with a uniform signature, they reside in the Vlocity managed package and aren't truly Salesforce standards. However, classes that implement VlocityOpenInterface or VlocityOpenInterface2 also implement the Callable Interface, which is a Salesforce standard.

In addition, Remote Actions of OmniScripts and Integration Procedures can invoke any class that implements Callable, regardless of whether it implements VlocityOpenInterface or VlocityOpenInterface2. You specify the Remote Class, Remote Method, and Additional Input properties in the same way for classes that implement any of these interfaces.

This makes the CallableLogger both the solution for dynamically logging in Apex, as well the solution to OmniStudio logging. Internally, the CallableLogger class does some additional handling of OmniStudio's format for passing the args parameter in call() - but ultimately, the same actions & same inputs are available in both Apex & OmniStudio.

Input Handling for All Actions

The Callable interface's one method (shown below) expects an args map as the second parameter. This gives a way for developers to dynamically pass any needed parameters/data to the action being called.

  • Object call(String action, Map<String,Object> args)

But not all actions in CallableLogger require any input - and in those cases, you can simply pass null for the args parameter.

Example: newEntry requires input, passed via the args map parameter

// Dynamically create a instance Nebula Logger's Callable Apex class (if it's available)
Type nebulaLoggerCallableType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');
Callable nebulaLoggerCallable = (Callable) nebulaLoggerCallableType?.newInstance();
if (nebulaLoggerCallable == null) {
  return;
}

// Example: Add a basic "hello, world!" INFO entry
Map<String, Object> infoEntryInput = new Map<String, Object>{
  'loggingLevel' => System.LoggingLevel.INFO,
  'message' => 'hello, world!'
};
nebulaLoggerCallable.call('newEntry', infoEntryInput);

Example: saveLog has some optional inputs, but no input is required. In this case, you can pass null for args, instead of needing to pass an empty (and pointless) instance of Map<String, Object>

// Dynamically create a instance Nebula Logger's Callable Apex class (if it's available)
Type nebulaLoggerCallableType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');
Callable nebulaLoggerInstance = (Callable) nebulaLoggerCallableType?.newInstance();
if (nebulaLoggerInstance == null) {
  return;
}

// Example: ✅ Save logs, using default settings (no input required)
nebulaLoggerInstance.call('saveLog', null);

// Example: ❌ When saving logs, passing an empty map to the call() method (shown below)
// is fine & everything will work, but it's not necessary for actions that have no required inputs
nebulaLoggerInstance.call('saveLog', new Map<String, Object>());

// Example: ℹ️ Save logs, using some optional settings
Map<String, Object> saveLogInput = new Map<String, Object>{
  'saveMethodName' => 'SYNCHRONOUS_DML'
};
nebulaLoggerInstance.call('saveLog', saveLogInput);

Details about the specific inputs used for each action are documented below in the available actions section

Output Handling for All Actions

All actions return an instance of Map<String, Object> as the output. The call() method always returns an Object, so the returned value will have to be cast to Map<String, Object> if you wish to inspect the returned information.

  • ✅When the call() method finishes successfully, the map contains the key isSuccess, with a value of true
    • Some actions will add additional values to the map. The specific output values for each action is documented below.
  • ❌When the call() method fails due to some catchable exception, the map contains the key isSuccess, with a value of false. It also includes 3 String values for the exception that was thrown:
    1. exceptionMessage - the value of thrownException.getMessage()
    2. exceptionStackTrace - the value of thrownException.getStackTraceString()
    3. exceptionType - the value of thrownException.getTypeName()

Example Syntax for All Actions

Type nebulaLoggerCallableType = Type.forName('Nebula', 'CallableLogger') ?? Type.forName('CallableLogger');
Callable nebulaLoggerInstance = (Callable) nebulaLoggerCallableType?.newInstance();
if (nebulaLoggerInstance == null) {
  return;
}

// An example of checking the result of the action call
Map<String, Object> output = (Map<String, Object>) nebulaLoggerCallable.call('saveLog', null);
System.debug('Log entries were successfully saved in Nebula Logger: ' + output.get('isSuccess'));
System.debug('Save exception type: ' + output.get('exceptionMessage'));
System.debug('Save exception message: ' + output.get('exceptionType'));
System.debug('Save exception stack trace: ' + output.get('exceptionStackTrace'));

newEntry Action

This action is used to add new log entries in Nebula Logger. It is the equivalent of using these methods (and their overloads) available in Logger:

  • error() overloads, like Logger.error('hello, world');
  • warn() overloads, like Logger.warn('hello, world');
  • info() overloads, like Logger.info('hello, world');
  • debug() overloads, like Logger.debug('hello, world');
  • fine() overloads, like Logger.fine('hello, world');
  • finer() overloads, like Logger.fine('rhello, world');
  • finest() overloads, like Logger.finest('hello, world');
  • newEntry() overloads, like Logger.newEntry(LoggingLevel.INFO, 'hello, world');

Input Values

Input Map Key Required Expected Datatype Notes
loggingLevel Required String or System.LoggingLevel
message Required String
exception Optional System.Exception When provided, Nebula Logger automatically stores details about the provided exception the log entry to the specified SObject record ID.

Cannot be used at the same time as record, recordList, or recordMap
recordId Optional Id When provided, Nebula Logger automatically ties the log entry to the specified SObject record ID.

Cannot be used at the same time as record, recordList, or recordMap
record Optional SObject When provided, Nebula Logger automatically ties the log entry to the specified SObject record, and stores a JSON copy of the provided SObject record.

Cannot be used at the same time as recordId, recordList, or recordMap
recordList Optional List When provided, Nebula Logger automatically stores a JSON copy of the provided list of SObject records.

Cannot be used at the same time as recordId, record, or recordMap
recordMap Optional Map When provided, Nebula Logger automatically stores a JSON copy of the provided map of SObject records.

Cannot be used at the same time as recordId, record, or recordList
saveLog Optional Boolean When set to true, Nebula Logger automatically saves any pending log entries. By default, log entries are not automatically saved.
tags Optional List When provided, Nebula Logger stores the provided strings as tags associated with the log entry.

Output Values

No additional output values are returned for this action.

Example Usage
Account someAccount = [SELECT Id, Name ]
Exception someException = new DmlException('oops');

// Add a new entry with an account & an exception as supporting context/data
Map<String, Object> newEntryInput = new Map<String, Object>{
  'exception' => someException,
  'loggingLevel' => LoggingLevel.ERROR,
  'message' => 'An unexpected exception was thrown'
  'record' => someAccount
};
Callable nebulaLoggerCallable = (Callable) System.Type.forName('CallableLogger')?.newInstance();
nebulaLoggerCallable?.call('newEntry', newEntryInput);

saveLog Action

This action is used to save any pending new log entries in Nebula Logger. It is the equivalent to using Logger.saveLog()

Input Values

Input Map Key Required Expected Datatype Notes
saveMethodName Optional String When provided, the specified save method will be used by Nebula Logger to save any pending log entries. By default, log entries are saved using the save method configured in LoggerSettings__c.DefaultSaveMethod__c.

Output Values

No additional output values are returned for this action.

Example Usage

System.Callable nebulaLoggerCallable = (System.Callable) System.Type.forName('CallableLogger')?.newInstance();

// Save using the default save method
nebulaLoggerCallable?.call('saveLog', null);

// Save using a specific save method
nebulaLoggerCallable?.call('saveLog', new Map<String, Object>{ 'saveMethodName' => 'SYNCHRONOUS_DML' });

getTransactionId Action

This action is used to return Nebula Logger's unique identifier for the current transaction. It is the equivalent to using Logger.getTransactionId()

Input Values

No input values are used for this action.

Output Values
Output Map Key Datatype
transactionId String

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> output = (Map<String, Object>) nebulaLoggerCallable?.call('getTransactionId', null);
System.debug('Current transaction ID: ' + (String) output.get('transactionId'));

getParentLogTransactionId Action

This action is used to return Nebula Logger's unique identifier for the parent log of the current transaction (if one has been set). It is the equivalent to using Logger.getParentLogTransactionId()

Input Values

No input values are used for this action.

Output Values

Output Map Key Datatype
parentLogTransactionId String

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> output = (Map<String, Object>) nebulaLoggerCallable?.call('getParentLogTransactionId', null);
System.debug('Current parent log transaction ID: ' + (String) output.get('parentLogTransactionId'));

setParentLogTransactionId Action

This action is used to set Nebula Logger's the unique identifier for the parent log of the current transaction. It is the equivalent to using Logger.setParentLogTransactionId(String)

Input Values

Input Map Key Required Expected Datatype Notes
parentLogTransactionId Required String

Output Values

No output values are used for this action.

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> output = (Map<String, Object>) nebulaLoggerCallable?.call('getParentLogTransactionId', null);
System.debug('Current parent log transaction ID: ' + (String) output.get('parentLogTransactionId'));

getScenario Action

This action is used to return Nebula Logger's current scenario for scenario-based-logging (if one has been set). It is the equivalent to using Logger.getScenario().

Input Values

No input values are used for this action.

Output Values

Output Map Key Datatype
scenario String

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> output = (Map<String, Object>) nebulaLoggerCallable?.call('getScenario', null);
System.debug('Current scenario: ' + (String) output.get('scenario'));

setScenario Action

This action is used to set Nebula Logger's current scenario for scenario-based-logging. It is the equivalent to using Logger.setScenario(String)

Input Values

Input Map Key Required Expected Datatype Notes
scenario Required String

Output Values

No additional output values are used for this action.

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> input = new Map<String, Object>{ 'scenario' => 'some scenario' };
nebulaLoggerCallable?.call('setScenario', input);

endScenario Action

This action is used to set Nebula Logger's current scenario for scenario-based-logging. It is the equivalent to using Logger.endScenario(String)

Input Values

Input Map Key Required Expected Datatype Notes
scenario Required String

Output Values

No additional output values are used for this action.

Example Usage

Callable nebulaLoggerCallable = (Callable) Type.forName('CallableLogger')?.newInstance();

Map<String, Object> input = new Map<String, Object>{ 'scenario' => 'some scenario' };
nebulaLoggerCallable?.call('endScenario', input);
Clone this wiki locally