Logging - Guide
Framework: KernDX Package Type: Managed Package API Version: 67.0
> Note for Subscriber Implementations: When using KernDX with a custom namespace, prefix framework class references with your namespace (e.g., ClientNS.LOG_Builder). See > the AI Agent Instructions for details.
Target Audience:
- Developers - Implementing logging across Apex, LWC, and Flows
- Architects - Designing observability and traceability patterns
- DevOps - Monitoring and debugging production systems
Table of Contents
Expand
- Quick Navigation
- Overview
- Architecture
- Quick Start
- Apex Logging (LOG_Builder)
- Correlation Tracking
- Structured Context
- Performance Logging
- Log Buffering
- LWC Client-Side Logging
- Flow Logging (FLOW_LoggerStart, FLOW_LoggerLog, FLOW_LoggerEnd)
- Testing
- Querying Log Entries
- Configuration Reference
- Anti-Patterns
- Best Practices
- Troubleshooting
- Related Documentation
Quick Navigation
| I am a... | I need to... | Go to... |
|---|---|---|
| Architect | Design observability patterns | Architecture |
| Architect | Plan correlation tracking | Correlation Tracking |
| Developer | Log my first error | Quick Start |
| Developer | Add client-side logging | LWC Client-Side Logging |
| Developer | Test logging behavior | Testing |
| Developer | Query persisted log entries | Querying Log Entries |
| Analyst | Configure log filtering | Configuration Reference |
| Analyst | Integrate logging in Flows | Flow Logging |
Overview
The KernDX Logging Framework provides end-to-end observability across all Salesforce execution contexts. Unlike System.debug() which produces ephemeral debug logs, this framework persists logs to a queryable custom object (LogEntry__c) via platform events (LogEntryEvent__e).
> Responsibilities: The Logging Framework persists diagnostic and operational data to LogEntry__c. It does not enforce business rules, > perform DML on business objects, or control execution flow. Use it for observability only.
Key Capabilities:
| Feature | Description |
|---|---|
| Multi-Channel | Log from Apex, LWC, and Flows with consistent API |
| Correlation Tracking | Link related logs across async boundaries with correlation IDs |
| Context Stack | Capture nested operation context (query details, trigger info, etc.) |
| Performance Monitoring | Automatic timing with configurable thresholds |
| Structured Context | Attach key-value metadata to log entries |
| Log Buffering | Batch logs for efficient publishing |
> Logging Framework Scope: 4 LOG_* classes, 1 platform event (LogEntryEvent__e), and multi-channel support (Apex, LWC, Flow). Logging > is integrated across all 14 API outbound services, all trigger handlers, and the async processing framework.
Architecture
+---------------------------------------------------------------------------+
| Entry Points |
+--------------+--------------+--------------+---------------+--------------+
| LWC | Flow | Apex | Trigger | Webservice |
| utilityLogger| FLOW_Logger* | LOG_Builder | TRG_* | API_* |
+------+-------+------+-------+------+-------+------+--------+------+-------+
| | | | |
+--------------+--------------+--------------+----------------+
|
v
+------------------------------+
| LOG_Engine |
| - Correlation ID tracking |
| - Context stack management |
| - Log event publishing |
+-------------+----------------+
|
v
+------------------------------+
| LogEntryEvent__e |
| (Platform Event) |
+-------------+----------------+
|
v
+------------------------------+
| TRG_LogEntryEvent (Trigger) |
+-------------+----------------+
|
v
+------------------------------+
| LogEntry__c |
| (Persistent Storage) |
+------------------------------+Flow:
- Code calls logging methods (
LOG_Builder,utilityLogger,FLOW_LoggerStart/FLOW_LoggerLog/FLOW_LoggerEnd) LOG_Enginemanages correlation IDs and context- Log entries are published as platform events (
LogEntryEvent__e) (non-blocking) TRG_LogEntryEventtrigger inserts records intoLogEntry__c
Quick Start
> Step-by-step walkthrough: Fast Start - Logging covers implementation, > testing, and common pitfalls.
Apex - Log an Error
try
{
// Your code
}
catch(Exception e)
{
LOG_Builder.build().error(e).emitAt('MyClass.myMethod');
throw e;
}LWC - Log with Correlation
import {ComponentBuilder} from 'c/componentBuilder';
import {logger} from 'c/utilityLogger';
export default class MyComponent extends ComponentBuilder('notification')
{
connectedCallback()
{
logger.startCorrelation();
logger.info('Component loaded', 'MyComponent.connectedCallback');
}
handleError(error)
{
logger.error(error.message, 'MyComponent.handleError');
}
}Flow - Correlated Logging
Start Logger (flowName) → Log Message (correlationId, message) → End Logger (correlationId)For deeper coverage, continue reading the sections below.
Apex Logging (LOG_Builder)
LOG_Builder is the primary Apex interface for logging. All methods delegate to LOG_Engine for event publishing and context management.
Log Levels
| Level | Method | Use Case |
|---|---|---|
| DEBUG | debug() | Development tracing, detailed diagnostics |
| INFO | info() | Operational events, business milestones |
| WARN | warn() | Potential issues, degraded functionality |
| ERROR | error() | Failures requiring attention |
// DEBUG - Development tracing
LOG_Builder.build().debug('Processing started for account: ' + account.Name).emitAt('AccountService.process');
// INFO - Business events
LOG_Builder.build().info('Order #' + order.OrderNumber + ' totaling ' + order.TotalAmount)
.withSummary('Order Completed')
.emitAt('OrderService.completeOrder');
// WARN - Potential issues
LOG_Builder.build().warn('API calls at 80% of daily limit')
.withSummary('Rate limit approaching')
.emitAt('IntegrationService.callAPI');
// ERROR - Failures
LOG_Builder.build().error('Gateway returned: ' + errorMessage)
.withSummary('Payment failed')
.emitAt('PaymentService.processPayment');Exception Logging
// Log exception with full stack trace
try
{
processRecord(record);
}
catch(Exception e)
{
// Basic exception logging
LOG_Builder.build().error(e).emitAt('MyClass.myMethod');
// With record ID
LOG_Builder.build().error(e).at('MyClass.myMethod').forRecord(record.Id).emit();
throw e; // Re-throw if needed
}DML Error Logging
For partial DML operations, use errorDMLOperationResults() to extract and log all errors from Database.SaveResult, Database.DeleteResult, or Database.UpsertResult collections.
// Automatically extract and log all DML errors
List<Database.SaveResult> results = Database.insert(accounts, false);
Boolean hasErrors = LOG_Builder.errorDMLOperationResults(results, 'AccountService.bulkInsert');
if(hasErrors)
{
// Handle partial failure
}Batch Logging
// Log multiple related messages efficiently
List<String> messages = new List<String>();
for(Account account : failedAccounts)
{
messages.add('Failed to process: ' + account.Name + ' - ' + account.ErrorReason__c);
}
LOG_Builder.build().warn(messages).emitAt('BatchProcessor.execute');Log Grouping & Flood Control
When the same event recurs at high frequency — a retry loop, a flaky integration, a batch job logging per record — give it a fingerprint:
LOG_Builder.build()
.warn('Payment gateway retry failed')
.withFingerprint('payment-gateway-retry')
.emitAt('PaymentSync.run');The first occurrence persists as a full log entry — the row whose Fingerprint starts with detail:. Repeats roll up into one counter row per day (the rollup: prefix) carrying an Occurrence Count, so thousands of identical entries collapse to two rows.
Choosing a key: use a stable identity for the kind of event, never per-occurrence data. 'payment-gateway-retry' groups; a key containing a record Id or timestamp makes every entry unique and produces more rows than plain logging. Keys are trimmed; a key longer than 200 characters, or one starting with the reserved bypass: prefix, is hashed automatically. When a key is hashed, the original key is recorded on the detail row's context under fingerprintSource, so a hashed fingerprint always traces back to what you passed.
Reading grouped logs:
- Forensics: filter Fingerprint starting with
detail:— one full sample per event kind. - Volumes: SUM Occurrence Count over rows whose Fingerprint starts with
rollup:. The sampled occurrence is already included in the count, so rollup rows alone are the true total — counting detail and rollup rows together double-counts. - A detail row's Created Date means "oldest retained sample": if your log purge job removes it, the next occurrence simply re-creates it. Rollup rows are counts, not forensic records — their message reflects the window's first occurrence.
Framework bypass audit uses this mechanism automatically: every security-bypass identity (who, which surface, what target) keeps exactly one detail row in retained logs plus daily counters, so a bypass in a hot loop can no longer flood the log table. A new bypass identity appearing in production — new code path, new user — still lands loudly as a fresh detail row.
> Note: flood control collapses storage, not emission. Each occurrence still publishes a platform event and consumes event allocations; the BypassAudit_Enabled feature flag remains the emission-side off switch for bypass auditing.
Logging Inside Platform Event & Change Event Triggers
Log entries are normally published as a LogEntryEvent__e platform event and persisted asynchronously (see Architecture). That publish-then-persist path needs one adjustment in a specific place: code that logs while running inside a platform event (__e) or Change Data Capture (*ChangeEvent) trigger. Publishing a new platform event from inside an event trigger can cause the platform to redeliver the original event, and a "log on every delivery" pattern then becomes a redelivery loop.
The framework handles this for you. When a log is emitted while a platform-event or change-event trigger is on the stack, the engine suppresses the event publish and persists the entry synchronously, in the same transaction, via DML — the same LogEntry__c rows, with no second platform event and no loop. There is nothing to configure: logging from a Change Data Capture trigger action or a platform-event subscriber is safe by default.
Flood control applies on this path too. Fingerprinted entries collapse into one detail row plus per-day rollup counters exactly as they do on the asynchronous path (see Log Grouping & Flood Control), so a high-volume change-event stream cannot flood LogEntry__c.
Correlation Tracking
Correlation IDs link related log entries across transaction boundaries, making it possible to trace a user action from LWC through to async processing. The correlation ID is stored on each LogEntryEvent__e and subsequently on every LogEntry__c record, enabling filtering in SEL_LogEntry queries.
Starting Correlation
// Generate new correlation ID
String correlationId = LOG_Builder.startCorrelation();
// All subsequent logs in this transaction share this correlationId
LOG_Builder.build().info('Step 1').emitAt('MyClass.myMethod');
LOG_Builder.build().info('Step 2').emitAt('MyClass.myMethod');Async Context Propagation
When spawning async work (Queueable, Batch, Future), serialize and restore context:
// Parent transaction - serialize context
public void initiateAsync(Id recordId)
{
LOG_Builder.startCorrelation();
LOG_Builder.build().info('Initiating async processing').emitAt('MyClass.initiateAsync');
// Capture context for async job
String context = LOG_Builder.serializeContext();
System.enqueueJob(new MyQueueable(recordId, context));
}
// Child transaction - restore context
public with sharing class MyQueueable implements Queueable
{
private Id recordId;
private String loggerContext;
public MyQueueable(Id recordId, String loggerContext)
{
this.recordId = recordId;
this.loggerContext = loggerContext;
}
public void execute(QueueableContext ctx)
{
// Restore correlation - logs now linked to parent
LOG_Builder.hydrateContext(loggerContext);
LOG_Builder.build().info('Async processing started').emitAt('MyQueueable.execute');
// Process...
}
}> Automatic capture for async chains. The pattern above is the manual approach for a hand-rolled Queueable. The > async chain framework does this for you — it serializes and restores the logging context across every step, and attaches a > Transaction Finalizer that logs any unhandled > exception (including governor-limit crashes a step's own try/catch cannot trap) and marks the chain Failed rather than leaving it stuck in a Running state. > An async step that dies still produces a correlated error log.
External Correlation
When receiving requests from external systems with their own correlation IDs:
// REST endpoint receiving external correlation
@RestResource(urlMapping='/api/v1/orders/*')
global inherited sharing class OrderAPI
{
@HttpPost
global static void createOrder()
{
RestRequest req = RestContext.request;
String externalCorrelationId = req.headers.get('X-Correlation-ID');
if(String.isNotBlank(externalCorrelationId))
{
LOG_Builder.setCorrelationId(externalCorrelationId);
}
else
{
LOG_Builder.startCorrelation();
}
LOG_Builder.build().info('Order request received').emitAt('OrderAPI.createOrder');
}
}Structured Context
Global Context
Attach key-value pairs to all subsequent log entries in the transaction:
public void processAccount(Account account)
{
// Set context - appears in ContextData__c JSON
LOG_Builder.setGlobalContext('accountId', account.Id);
LOG_Builder.setGlobalContext('accountName', account.Name);
LOG_Builder.setGlobalContext('industry', account.Industry);
try
{
LOG_Builder.build().info('Processing started').emitAt('AccountService.processAccount');
// Log entry includes: {"accountId":"001...","accountName":"Acme","industry":"Technology"}
validateAccount(account);
enrichAccount(account);
LOG_Builder.build().info('Processing completed').emitAt('AccountService.processAccount');
}
finally
{
// Always clean up context
LOG_Builder.clearGlobalContext('accountId');
LOG_Builder.clearGlobalContext('accountName');
LOG_Builder.clearGlobalContext('industry');
// Or: LOG_Builder.clearAllGlobalContext();
}
}Operation Context Stack
The framework maintains an internal operation-context stack that is pushed and popped automatically at every dispatcher entry point — TRG_Dispatcher around each trigger action, API_Dispatcher around each inbound and outbound call, UTIL_AsyncChain around each chain step. Subscribers do not interact with the stack directly (LOG_Engine.pushOperationContext / popOperationContext are public framework-internal methods, not callable from subscriber Apex).
For per-call subscriber context, attach it directly to the log entry via .withContext(key, value):
kern.LOG_Builder.build()
.debug('Executing query')
.withContext('queryType', 'SOQL')
.withContext('objectName', 'Account')
.withContext('soql', 'SELECT Id FROM Account WHERE Industry = :industry')
.emitAt('MyClass.runQuery');The framework-managed operation types you will see in LogEntry__c records are API_CALL, API_BATCH, TRIGGER_ACTION, QUERY, FLOW, LWC, and VALIDATION — all set automatically by the dispatchers.
Performance Logging
The framework automatically times operations across queries, triggers, and API calls. No subscriber code is needed — timing is built into the framework infrastructure. All timers track governor limit deltas (CPU time, heap, SOQL queries, DML) and log only when configured thresholds are exceeded.
What Gets Automatically Timed
Query performance — Every QRY_Builder and SEL_* query is timed automatically. Log entries include the SOQL statement, row count, object name, and cache status (hit/miss/stored). This helps identify slow queries and N+1 patterns without instrumenting individual selectors.
Trigger action performance — Each action dispatched by TRG_Dispatcher is timed with full context: action class name, trigger operation (e.g., BEFORE_INSERT), object name, and record count. This surfaces which trigger actions contribute most to transaction time.
API operation performance — Outbound and inbound API calls processed through the web services framework are timed automatically, capturing HTTP method, endpoint, and response status.
All performance logging is threshold-based and disabled by default. Enable and configure thresholds via LogSetting__c (see Performance Configuration below).
Custom Timing in Subscriber Code
UTIL_StopWatch is the framework-internal base class used by the three specialised performance timers. It is declared public and is not intended for direct subscriber use. For ad-hoc timing around a custom batch step or callout, wrap the work in a LOG_Builder.scope() block — the scope captures start/end timestamps and emits a LogEntryEvent__e that joins the same correlation pipeline as the framework's automatic timers.
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();
try
{
// ... do work ...
}
finally
{
scope.close();
}Performance Configuration
Configure via LogSetting__c (Setup > Custom Settings > Log Setting):
| Field | Description | Default |
|---|---|---|
EnablePerformanceLogging__c | Enable general performance logging | true |
PerformanceThresholdMs__c | Threshold for general operations (ms) | 10000 |
EnableQueryPerformanceLogging__c | Enable query performance logging | true |
QueryPerformanceThresholdMs__c | Threshold for queries (ms) | 1000 |
EnableTriggerPerformanceLogging__c | Enable trigger performance logging | true |
TriggerPerformanceThresholdMs__c | Threshold for trigger actions (ms) | 500 |
EnableValidationPerformanceLogging__c | Enable validation performance logging | true |
ValidationPerformanceThresholdMs__c | Threshold for validation processing (ms) | 100 |
EnableMaskerPerformanceLogging__c | Enable data-masking performance logging (default OFF) | false |
MaskerPerformanceThresholdMs__c | Threshold for masking on a trigger batch (ms) | 100 |
Masker performance logging emits one aggregate LogEntryEvent__e per trigger batch when EnableMaskerPerformanceLogging__c is true and the masker's elapsed time meets the threshold. Default off so subscribers pay zero log volume by default — turn on during investigation of slow commits to attribute the time to masking. Entries carry the target SObject name in ClassMethod__c (e.g., UTIL_MaskerPerformanceTimer/Foobar__c) for per-object aggregation.
Log Buffering
For batch operations, buffer logs to reduce platform event publishes:
public void processBulkRecords(List<SObject> records)
{
// Suspend immediate publishing - logs are buffered
LOG_Builder.suspendSaving();
try
{
for(SObject record : records)
{
LOG_Builder.build().debug('Processing: ' + record.Id).emitAt('BulkProcessor.process');
processRecord(record);
// Optionally flush periodically to avoid memory issues
if(Math.mod(processedCount, 100) == 0)
{
LOG_Builder.flushBuffer();
}
}
}
finally
{
// Resume and flush all remaining logs
LOG_Builder.resumeSaving();
}
}> Note: ERROR-level logs bypass the buffer and trigger an immediate flush. This ensures error visibility even when buffering is active.
LWC Client-Side Logging
The utilityLogger LWC module provides client-side logging with automatic correlation tracking.
LWC Setup
Import the logger in your component:
import {logger} from 'c/utilityLogger';LWC Basic Usage
import {ComponentBuilder} from 'c/componentBuilder';
import {logger} from 'c/utilityLogger';
export default class MyComponent extends ComponentBuilder('notification')
{
connectedCallback()
{
logger.info('Component initialized', 'MyComponent.connectedCallback');
}
handleButtonClick()
{
logger.debug('Button clicked', 'MyComponent.handleButtonClick');
try
{
this.processData();
}
catch(error)
{
logger.error(error.message, 'MyComponent.handleButtonClick', {
errorName: error.name,
stack: error.stack
});
}
}
processData()
{
logger.info('Processing data', 'MyComponent.processData', {
recordCount: this.records.length
});
}
}LWC Correlation
import {ComponentBuilder} from 'c/componentBuilder';
import {logger} from 'c/utilityLogger';
import processRecords from '@salesforce/apex/MyController.processRecords';
export default class MyComponent extends ComponentBuilder('notification', 'controller')
{
async handleProcess()
{
// Start correlation for this user action
const correlationId = logger.startCorrelation();
logger.info('Starting process', 'MyComponent.handleProcess');
try
{
// Pass correlation to Apex - logs will be linked
const result = await processRecords({
recordIds: this.selectedIds,
correlationId: correlationId
});
logger.info('Process completed', 'MyComponent.handleProcess', {
successCount: result.successCount
});
}
catch(error)
{
logger.error('Process failed: ' + error.body?.message, 'MyComponent.handleProcess');
}
}
}LWC Performance Timing
import {logger} from 'c/utilityLogger';
async loadData()
{
const timer = logger.startTimer('LoadAccountData');
try
{
const data = await getAccountData({accountId: this.recordId});
timer.stop(); // Logs duration
this.accountData = data;
}
catch(error)
{
timer.stop();
logger.error('Failed to load data', 'MyComponent.loadData');
}
}Server Persistence
The utilityLogger module automatically persists buffered logs to the server. A framework controller bridges client-side and server-side logging, linking correlation IDs from the client with server-side log entries.
How It Works:
+----------------------+ +----------------------+ +------------------+
| utilityLogger.js |---->| Server Controller |---->| LOG_Engine |
| (LWC Module) | | | | |
| | | | | |
| - Buffers logs | | - Sets correlation | | - Publishes |
| - Auto-flush on end | | - Pushes LWC context | | LogEntryEvent__e |
+----------------------+ +----------------------+ +------------------+Client Log Entry Structure:
| Property | Type | Description |
|---|---|---|
timestamp | Datetime | When the log was created |
level | LoggingLevel | DEBUG, INFO, WARN, ERROR |
message | String | Log message |
correlationId | String | Links related logs |
context | Map<String, Object> | Additional context data |
Server-Side Processing:
When logs arrive from LWC, the server controller:
- Extracts the correlation ID from the first log entry
- Links it with any subsequent server-side logs
- Pushes an LWC operation context onto the context stack
- Logs each entry via
LOG_Builder - Pops the LWC context
This ensures LWC logs appear in LogEntry__c with proper correlation and context.
Console Fallback
If Apex persistence fails, logs automatically fall back to browser console:
[utilityLogger] Failed to persist logs to server: [error details]
[utilityLogger] Fallback - buffered logs:
[INFO] 2025-01-15T10:30:00.000Z | abc-123-def | Component initialized {...}
[ERROR] 2025-01-15T10:30:01.000Z | abc-123-def | Process failed {...}Flow Logging (FLOW_LoggerStart, FLOW_LoggerLog, FLOW_LoggerEnd)
Flow Simple Logging
For logging without correlation, use FLOW_WriteLog:
FLOW_WriteLog (Full control):
message(Required) - The message to loglogLevel(Optional) - DEBUG, INFO, WARN, ERRORshortMessage(Optional) - Brief summaryclassMethod(Optional) - Context identifierrecordId(Optional) - Associated recordcorrelationId(Optional) - Link to existing correlation
Flow Bookend Pattern
For correlated logging across a Flow, use the bookend pattern with three invocable actions:
1. FLOW_LoggerStart - Begin correlation
| Input | Description |
|---|---|
flowName (Required) | Name of the Flow |
flowVersion (Optional) | Flow version number |
recordId (Optional) | Associated record |
| Output | Description |
|---|---|
correlationId | Generated correlation ID |
2. FLOW_LoggerLog - Log messages
| Input | Description |
|---|---|
correlationId (Required) | From FLOW_LoggerStart |
message (Required) | Log message |
logLevel (Optional) | DEBUG, INFO, WARN, ERROR |
shortMessage (Optional) | Brief summary |
recordId (Optional) | Associated record |
stepName (Optional) | Current Flow step |
3. FLOW_LoggerEnd - End correlation
| Input | Description |
|---|---|
correlationId (Required) | From FLOW_LoggerStart |
status (Optional) | SUCCESS, FAILURE, etc. |
message (Optional) | Final message |
Example Flow:
+----------------------------------------+
| Start Flow Logger |
| flowName: "Account Approval Flow" |
| recordId: {!$Record.Id} |
| -> Store correlationId in {!varCorr} |
+-------------------+--------------------+
|
v
+----------------------------------------+
| Log Flow Message |
| correlationId: {!varCorr} |
| message: "Approval request created" |
| logLevel: "INFO" |
| stepName: "Create Request" |
+-------------------+--------------------+
|
v
+-----------------+
| Decision Node |
+--------+--------+
|
+----------+----------+
| |
v v
+---------------+ +---------------+
| Log: Approved| | Log: Rejected|
| logLevel:INFO| | logLevel:WARN|
+-------+-------+ +-------+-------+
| |
+----------+----------+
|
v
+----------------------------------------+
| End Flow Logger |
| correlationId: {!varCorr} |
| status: {!varFinalStatus} |
+----------------------------------------+Testing
By default, logging is suppressed in unit tests to avoid side effects. Enable logging when testing logging behavior:
@IsTest(SeeAllData=false IsParallel=true)
private class MyService_TEST
{
@IsTest
private static void shouldLogErrorOnFailure()
{
LOG_Builder.ignoreTestMode = true;
Test.startTest();
try
{
MyService.processInvalidData();
Assert.fail('Expected exception');
}
catch(Exception e)
{
// Expected
}
Test.stopTest();
List<LogEntry__c> logs = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
.fields(new List<SObjectField>{LogEntry__c.Id, LogEntry__c.LogLevel__c, LogEntry__c.Message__c})
.toList();
Assert.isFalse(logs.isEmpty(), 'Error should be logged');
Assert.areEqual('ERROR', logs[0].LogLevel__c);
}
}Querying Log Entries
When filtering LogEntry__c records by user — in reports, dashboards, cleanup scripts, or audit queries — filter by UserId__c, NOT by CreatedById.
Why
The framework persists log entries asynchronously: LOG_Builder.emit() publishes a LogEntryEvent__e Platform Event, and TRG_PersistLogEntry (a Platform Event subscriber trigger) inserts the LogEntry__c row. Salesforce executes Platform Event triggers as the Automated Process user (e.g. autoproc@<orgid>) regardless of who fired the event, so EVERY persisted LogEntry__c.CreatedById is the Automated Process user.
The framework captures the emitting user's Id at publish time on LogEntryEvent__e.UserId__c and TRG_PersistLogEntry propagates it to LogEntry__c.UserId__c. That field — not CreatedById — is the correct join key for "which user emitted this log".
Right vs wrong
// RIGHT — returns logs from the running user
List<LogEntry__c> myLogs = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
.condition(LogEntry__c.UserId__c).equals(UserInfo.getUserId())
.orderBy(LogEntry__c.CreatedDate).descending()
.toList();
// WRONG — silently returns ZERO rows on persisted entries
List<LogEntry__c> brokenQuery = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
.condition(LogEntry__c.CreatedById).equals(UserInfo.getUserId())
.toList();Reports and dashboards
The same applies to declarative reports, dashboards, and list views. To filter by emitting user, add a column or filter on User ID (kern__UserId__c), not Created By.
UserId__c is a Text(18) — not a Lookup — so UserId__r.Name traversal is NOT available. To filter or display by user name, build a report type that joins LogEntry__c.UserId__c to the User object via a Report Type Cross-Filter or a custom report formula. The companion field UserLink__c is a formula that renders an "Open" hyperlink to the user record (so reports can include a one-click jump from a log row to the User detail page); use it for navigation, and UserId__c for filtering.
Cleanup scripts
Cleanup queries should also filter by UserId__c. The platform allows WHERE CreatedDate < ... clauses against the Automated Process user's records — the cleanup user just needs delete access to LogEntry__c.
// Delete logs older than 30 days emitted by a specific user
List<LogEntry__c> stale = QRY_Builder.selectFrom(LogEntry__c.SObjectType)
.condition(LogEntry__c.UserId__c).equals(targetUserId)
.andCondition(LogEntry__c.CreatedDate).lessThan(Datetime.now().addDays(-30))
.toList();
DML_Builder.newTransaction().doDelete(stale).execute();Sharing considerations
TRG_PersistLogEntry runs inherited sharing, so the OWD model on LogEntry__c applies to the Automated Process user's record-level access — not the emitting user's. If subscribers want emitting users to see their own logs without granting "View All", a criteria-based sharing rule keyed on UserId__c closes that visibility gap.
Configuration Reference
LogSetting__c Fields
Hierarchical custom setting (Org > Profile > User). Configure via Setup > Custom Settings > Log Setting.
| Field | Type | Default | Description |
|---|---|---|---|
IsEnabled__c | Checkbox | true | Master kill switch. When false, all non-ERROR logs are dropped. |
LogLevelThreshold__c | Text(10) | DEBUG | Minimum level to log (DEBUG, INFO, WARN, ERROR) |
ClassFilter__c | Text(255) | blank | Comma-separated class name patterns with trailing * wildcard (e.g., API_*,SVC_Payment*). Blank = all classes. |
MaxContextDataSize__c | Number | 32768 | Max characters for ContextData__c |
EnablePerformanceLogging__c | Checkbox | true | Enable general performance logging |
PerformanceThresholdMs__c | Number | 10000 | Threshold for general timers (ms) |
EnableQueryPerformanceLogging__c | Checkbox | true | Enable query performance logging |
QueryPerformanceThresholdMs__c | Number | 1000 | Threshold for query timers (ms) |
EnableTriggerPerformanceLogging__c | Checkbox | true | Enable trigger performance logging |
TriggerPerformanceThresholdMs__c | Number | 500 | Threshold for trigger timers (ms) |
EnableValidationPerformanceLogging__c | Checkbox | true | Enable validation performance logging |
ValidationPerformanceThresholdMs__c | Number | 100 | Threshold for validation timers (ms) |
EnableMaskerPerformanceLogging__c | Checkbox | false | Enable masker performance logging (default OFF — opt-in) |
MaskerPerformanceThresholdMs__c | Number | 100 | Threshold for masking on a trigger batch (ms) |
TriggerSetting__mdt Fields (Trigger Performance)
| Field | Type | Description |
|---|---|---|
EnablePerformanceLogging__c | Checkbox | Override LogSetting__c for this object |
PerformanceThresholdMs__c | Number | Threshold for this object's triggers |
TriggerAction__mdt Fields (Action-Level Control)
| Field | Type | Description |
|---|---|---|
ForcePerformanceLogging__c | Checkbox | Always log this action |
SuppressPerformanceLogging__c | Checkbox | Never log this action |
PerformanceThresholdMs__c | Number | Custom threshold for this action |
Configuration Hierarchy (highest priority first):
- TriggerAction__mdt (action-specific)
- TriggerSetting__mdt (object-specific)
- LogSetting__c (global)
Anti-Patterns
| Anti-Pattern | Why It's Wrong | Instead |
|---|---|---|
Using System.debug() | Output is ephemeral, not queryable, and lost after debug log rotation | Use LOG_Builder.build().error(e).emitAt('Class.method') |
| Logging without class.method context | Impossible to trace which code generated a log entry | Always provide context via .emitAt('ClassName.methodName') or .at('ClassName.methodName') |
| Logging PII or secrets (passwords, tokens, SSNs) | Violates compliance requirements and creates security vulnerabilities | The data masking framework (MaskingRule__mdt + MaskingTarget__mdt) is on by default and redacts configured patterns on every LogEntryEvent__e. Ship custom rules if your payload shape needs additional patterns |
| Missing correlation in async operations | Cannot trace a logical operation across transaction boundaries | Use LOG_Builder.setCorrelationId(correlationId) or serializeContext()/hydrateContext() to propagate correlation IDs across async boundaries |
| Forgetting to close log scopes | Buffered log entries may not be emitted, causing silent data loss | Always call scope.close() in a finally block |
Best Practices
Always Include Context
// Good - includes class.method context and record correlation
LOG_Builder.build().error(e).at('OrderService.processOrder').forRecord(orderId).emit();
// Bad - no context
LOG_Builder.build().error(e.getMessage()).emit();Use Appropriate Log Levels
| Level | When to Use |
|---|---|
| DEBUG | Detailed tracing during development |
| INFO | Business events, milestones |
| WARN | Potential issues, recoverable errors |
| ERROR | Failures requiring attention |
Never Log Sensitive Data
// Bad - logs password
LOG_Builder.build().debug('Password: ' + password).emitAt('AuthService.login');
// Good - no sensitive data
LOG_Builder.build().debug('Login attempt for user: ' + username).emitAt('AuthService.login');Use Correlation for Async Operations
// Always propagate context to async jobs
public void initiateProcess(Id recordId)
{
LOG_Builder.startCorrelation();
String context = LOG_Builder.serializeContext();
System.enqueueJob(new ProcessQueueable(recordId, context));
}Clean Up Context
// Always use try/finally for context cleanup
LOG_Builder.setGlobalContext('accountId', account.Id);
try
{
// Process...
}
finally
{
LOG_Builder.clearGlobalContext('accountId');
}Enable Performance Logging for Critical Operations
Enable performance logging via LogSetting__c to surface slow operations. The framework automatically times queries, trigger actions, and API calls — no code changes required. For custom timing in subscriber code, wrap the work in a LOG_Builder.scope() block:
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();
try
{
kern.UTIL_HttpClient.post('MyGateway', '/charges').body(payload).send();
}
finally
{
scope.close();
}Troubleshooting
Logs Not Appearing
- Check test mode: Ensure
LOG_Builder.ignoreTestMode = truein tests - Check IsEnabled: Verify
LogSetting__c.IsEnabled__cistrue(Setup > Custom Settings > Log Setting) - Check threshold: Ensure
LogLevelThreshold__callows your log level (e.g.,DEBUGcaptures all) - Check class filter: If
ClassFilter__cis set, verify your class matches the pattern - Check platform event limits: Monitor event publishing limits
Missing Correlation
- Verify startCorrelation() called: Must be called at entry point
- Check async propagation: Ensure
serializeContext()/hydrateContext()used - Check LWC: Ensure correlationId passed to Apex methods
Performance Logs Missing
- Check enable flags: Verify
EnablePerformanceLogging__cis true - Check threshold: Operation may be faster than threshold
- Check timer API: Ensure
stop()is called (notstopSilent())
Context Not Appearing
- Check setGlobalContext(): Verify called before logging
- Check push/pop balance: Ensure
popOperationContext()matches pushes - Check MaxContextDataSize__c: Large context may be truncated
Related Documentation
- Objects & Metadata - Guide - LogEntry__c, LogSetting__c details
- Async Processing - Guide - Async context propagation patterns
- Web Services - Guide - Automatic API logging
- Triggers - Guide - Trigger action logging
- Fast Start - Logging - Quick-start primer for logging
Last Updated: April 2026 Guide Version: 2.1