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.

Why this pattern

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.

How it works

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.

Minimal wrapper

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.

Supported actions

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).

ActionPurpose
newEntryAdd a log entry. Supports every enrichment key in the table below.
saveLogPersist buffered entries. Optionally accepts saveMethodName.
getTransactionIdReturn the current transaction ID (for async parent/child linking).
getParentLogTransactionId / setParentLogTransactionIdRead / set the parent transaction ID.
getScenario / setScenario / endScenarioRead / set / end the transaction’s scenario.
tryCatchShorthand for newEntry with loggingLevel=ERROR and a serialized-input message; useful in generic catch blocks.

Supported input keys for newEntry

KeyPurpose
loggingLevelLevel name (INFO, ERROR, WARN, DEBUG, FINE, FINER, FINEST).
messageEntry message.
saveLogSet to true to persist immediately after adding the entry.
saveMethodNameOverride the save method for this call (EVENT_BUS, QUEUEABLE, REST, SYNCHRONOUS_DML).
tagsList<String> of tag names.
exceptionSystem.Exception instance to attach.
recordId / record / recordList / recordMapRecord association, same shape as the Apex setRecord(...) overloads.
scenarioScenario for the transaction.
parentLogTransactionIdParent transaction ID for async linking.

Version requirement

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.

Where next