Skip to content

Optional dependency via Callable

If you’re building a Salesforce package and want to emit rich telemetry in customer orgs when Nebula Logger is installed, without forcing customers to install it, use CallableLogger. Your package compiles and installs cleanly whether Nebula Logger is present or not.

Hard-depending on Nebula Logger means:

  • Every customer must install Nebula Logger before they can install your package.
  • Your package’s install failure surface expands.
  • Customers who don’t want logging can’t opt out cleanly.

The optional-dependency pattern lets:

  • Customers with Nebula Logger installed get rich, structured logs.
  • Customers without get a no-op - no runtime error, no compile-time dependency, no post-install step.

CallableLogger is a global class in both the unlocked and managed packages. It implements System.Callable:

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

Your package resolves the class at runtime via System.Type.forName(...) - not via a compile-time reference. If neither package is installed, the type resolution returns null, and your code no-ops.

public with sharing class OptionalLogger {
private static final System.Type CALLABLE_LOGGER_TYPE = System.Type.forName('Nebula', 'CallableLogger') ?? System.Type.forName('CallableLogger');
public static void info(String message) {
logEntry(System.LoggingLevel.INFO, message);
}
public static void error(String message, System.Exception apexException) {
invoke(
'newEntry',
new Map<String, Object>{ 'loggingLevel' => System.LoggingLevel.ERROR.name(), 'message' => message, 'exception' => apexException, 'saveLog' => true }
);
}
private static void logEntry(System.LoggingLevel level, String message) {
invoke('newEntry', new Map<String, Object>{ 'loggingLevel' => level.name(), 'message' => message, 'saveLog' => true });
}
private static void invoke(String action, Map<String, Object> input) {
if (CALLABLE_LOGGER_TYPE == null) {
return;
}
System.Callable logger = (System.Callable) CALLABLE_LOGGER_TYPE.newInstance();
logger.call(action, input);
}
}

Callers of OptionalLogger.info(...) / OptionalLogger.error(...) get logging when Nebula Logger is installed, no-ops otherwise.

Every action accepts a Map<String, Object> input and returns a Map<String, Object> output (with isSuccess, transactionId, parentLogTransactionId, requestId, and error details on failure).

Action Purpose
newEntry Add a log entry. Supports every enrichment key in the table below.
saveLog Persist buffered entries. Optionally accepts saveMethodName.
getTransactionId Return the current transaction ID (for async parent/child linking).
getParentLogTransactionId / setParentLogTransactionId Read / set the parent transaction ID.
getScenario / setScenario / endScenario Read / set / end the transaction’s scenario.
tryCatch Shorthand for newEntry with loggingLevel=ERROR and a serialized-input message; useful in generic catch blocks.
Key Purpose
loggingLevel Level name (INFO, ERROR, WARN, DEBUG, FINE, FINER, FINEST).
message Entry message.
saveLog Set to true to persist immediately after adding the entry.
saveMethodName Override the save method for this call (EVENT_BUS, QUEUEABLE, REST, SYNCHRONOUS_DML).
tags List<String> of tag names.
exception System.Exception instance to attach.
recordId / record / recordList / recordMap Record association, same shape as the Apex setRecord(...) overloads.
scenario Scenario for the transaction.
parentLogTransactionId Parent transaction ID for async linking.

CallableLogger requires Nebula Logger v4.14.10 or newer. Customers on older versions won’t have the class - the type resolution returns null and the wrapper no-ops, which is the same behavior as not having Nebula Logger installed at all.