Skip to content

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
  1. Quick Navigation
  2. Overview
  3. Architecture
  4. Quick Start
  5. Apex Logging (LOG_Builder)
  6. Correlation Tracking
  7. Structured Context
  8. Performance Logging
  9. Log Buffering
  10. LWC Client-Side Logging
  11. Flow Logging (FLOW_LoggerStart, FLOW_LoggerLog, FLOW_LoggerEnd)
  12. Testing
  13. Querying Log Entries
  14. Configuration Reference
  15. Anti-Patterns
  16. Best Practices
  17. Troubleshooting
  18. Related Documentation

Quick Navigation

I am a...I need to...Go to...
ArchitectDesign observability patternsArchitecture
ArchitectPlan correlation trackingCorrelation Tracking
DeveloperLog my first errorQuick Start
DeveloperAdd client-side loggingLWC Client-Side Logging
DeveloperTest logging behaviorTesting
DeveloperQuery persisted log entriesQuerying Log Entries
AnalystConfigure log filteringConfiguration Reference
AnalystIntegrate logging in FlowsFlow 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:

FeatureDescription
Multi-ChannelLog from Apex, LWC, and Flows with consistent API
Correlation TrackingLink related logs across async boundaries with correlation IDs
Context StackCapture nested operation context (query details, trigger info, etc.)
Performance MonitoringAutomatic timing with configurable thresholds
Structured ContextAttach key-value metadata to log entries
Log BufferingBatch 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

text
+---------------------------------------------------------------------------+
|                           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:

  1. Code calls logging methods (LOG_Builder, utilityLogger, FLOW_LoggerStart / FLOW_LoggerLog / FLOW_LoggerEnd)
  2. LOG_Engine manages correlation IDs and context
  3. Log entries are published as platform events (LogEntryEvent__e) (non-blocking)
  4. TRG_LogEntryEvent trigger inserts records into LogEntry__c

Quick Start

> Step-by-step walkthrough: Fast Start - Logging covers implementation, > testing, and common pitfalls.

Apex - Log an Error

apex
try
{
	// Your code
}
catch(Exception e)
{
	LOG_Builder.build().error(e).emitAt('MyClass.myMethod');
	throw e;
}

LWC - Log with Correlation

javascript
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

text
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

LevelMethodUse Case
DEBUGdebug()Development tracing, detailed diagnostics
INFOinfo()Operational events, business milestones
WARNwarn()Potential issues, degraded functionality
ERRORerror()Failures requiring attention
apex
// 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

apex
// 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.

apex
// 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

apex
// 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:

apex
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

apex
// 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:

apex
// 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:

apex
// 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:

apex
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):

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

apex
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):

FieldDescriptionDefault
EnablePerformanceLogging__cEnable general performance loggingtrue
PerformanceThresholdMs__cThreshold for general operations (ms)10000
EnableQueryPerformanceLogging__cEnable query performance loggingtrue
QueryPerformanceThresholdMs__cThreshold for queries (ms)1000
EnableTriggerPerformanceLogging__cEnable trigger performance loggingtrue
TriggerPerformanceThresholdMs__cThreshold for trigger actions (ms)500
EnableValidationPerformanceLogging__cEnable validation performance loggingtrue
ValidationPerformanceThresholdMs__cThreshold for validation processing (ms)100
EnableMaskerPerformanceLogging__cEnable data-masking performance logging (default OFF)false
MaskerPerformanceThresholdMs__cThreshold 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:

apex
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:

javascript
import {logger} from 'c/utilityLogger';

LWC Basic Usage

javascript
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

javascript
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

javascript
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:

text
+----------------------+     +----------------------+     +------------------+
|   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:

PropertyTypeDescription
timestampDatetimeWhen the log was created
levelLoggingLevelDEBUG, INFO, WARN, ERROR
messageStringLog message
correlationIdStringLinks related logs
contextMap<String, Object>Additional context data

Server-Side Processing:

When logs arrive from LWC, the server controller:

  1. Extracts the correlation ID from the first log entry
  2. Links it with any subsequent server-side logs
  3. Pushes an LWC operation context onto the context stack
  4. Logs each entry via LOG_Builder
  5. 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:

text
[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 log
  • logLevel (Optional) - DEBUG, INFO, WARN, ERROR
  • shortMessage (Optional) - Brief summary
  • classMethod (Optional) - Context identifier
  • recordId (Optional) - Associated record
  • correlationId (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

InputDescription
flowName (Required)Name of the Flow
flowVersion (Optional)Flow version number
recordId (Optional)Associated record
OutputDescription
correlationIdGenerated correlation ID

2. FLOW_LoggerLog - Log messages

InputDescription
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

InputDescription
correlationId (Required)From FLOW_LoggerStart
status (Optional)SUCCESS, FAILURE, etc.
message (Optional)Final message

Example Flow:

text
+----------------------------------------+
|  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:

apex
@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

apex
// 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.

apex
// 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.

FieldTypeDefaultDescription
IsEnabled__cCheckboxtrueMaster kill switch. When false, all non-ERROR logs are dropped.
LogLevelThreshold__cText(10)DEBUGMinimum level to log (DEBUG, INFO, WARN, ERROR)
ClassFilter__cText(255)blankComma-separated class name patterns with trailing * wildcard (e.g., API_*,SVC_Payment*). Blank = all classes.
MaxContextDataSize__cNumber32768Max characters for ContextData__c
EnablePerformanceLogging__cCheckboxtrueEnable general performance logging
PerformanceThresholdMs__cNumber10000Threshold for general timers (ms)
EnableQueryPerformanceLogging__cCheckboxtrueEnable query performance logging
QueryPerformanceThresholdMs__cNumber1000Threshold for query timers (ms)
EnableTriggerPerformanceLogging__cCheckboxtrueEnable trigger performance logging
TriggerPerformanceThresholdMs__cNumber500Threshold for trigger timers (ms)
EnableValidationPerformanceLogging__cCheckboxtrueEnable validation performance logging
ValidationPerformanceThresholdMs__cNumber100Threshold for validation timers (ms)
EnableMaskerPerformanceLogging__cCheckboxfalseEnable masker performance logging (default OFF — opt-in)
MaskerPerformanceThresholdMs__cNumber100Threshold for masking on a trigger batch (ms)

TriggerSetting__mdt Fields (Trigger Performance)

FieldTypeDescription
EnablePerformanceLogging__cCheckboxOverride LogSetting__c for this object
PerformanceThresholdMs__cNumberThreshold for this object's triggers

TriggerAction__mdt Fields (Action-Level Control)

FieldTypeDescription
ForcePerformanceLogging__cCheckboxAlways log this action
SuppressPerformanceLogging__cCheckboxNever log this action
PerformanceThresholdMs__cNumberCustom threshold for this action

Configuration Hierarchy (highest priority first):

  1. TriggerAction__mdt (action-specific)
  2. TriggerSetting__mdt (object-specific)
  3. LogSetting__c (global)

Anti-Patterns

Anti-PatternWhy It's WrongInstead
Using System.debug()Output is ephemeral, not queryable, and lost after debug log rotationUse LOG_Builder.build().error(e).emitAt('Class.method')
Logging without class.method contextImpossible to trace which code generated a log entryAlways provide context via .emitAt('ClassName.methodName') or .at('ClassName.methodName')
Logging PII or secrets (passwords, tokens, SSNs)Violates compliance requirements and creates security vulnerabilitiesThe 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 operationsCannot trace a logical operation across transaction boundariesUse LOG_Builder.setCorrelationId(correlationId) or serializeContext()/hydrateContext() to propagate correlation IDs across async boundaries
Forgetting to close log scopesBuffered log entries may not be emitted, causing silent data lossAlways call scope.close() in a finally block

Best Practices

Always Include Context

apex
// 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

LevelWhen to Use
DEBUGDetailed tracing during development
INFOBusiness events, milestones
WARNPotential issues, recoverable errors
ERRORFailures requiring attention

Never Log Sensitive Data

apex
// 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

apex
// 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

apex
// 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:

apex
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

  1. Check test mode: Ensure LOG_Builder.ignoreTestMode = true in tests
  2. Check IsEnabled: Verify LogSetting__c.IsEnabled__c is true (Setup > Custom Settings > Log Setting)
  3. Check threshold: Ensure LogLevelThreshold__c allows your log level (e.g., DEBUG captures all)
  4. Check class filter: If ClassFilter__c is set, verify your class matches the pattern
  5. Check platform event limits: Monitor event publishing limits

Missing Correlation

  1. Verify startCorrelation() called: Must be called at entry point
  2. Check async propagation: Ensure serializeContext()/hydrateContext() used
  3. Check LWC: Ensure correlationId passed to Apex methods

Performance Logs Missing

  1. Check enable flags: Verify EnablePerformanceLogging__c is true
  2. Check threshold: Operation may be faster than threshold
  3. Check timer API: Ensure stop() is called (not stopSilent())

Context Not Appearing

  1. Check setGlobalContext(): Verify called before logging
  2. Check push/pop balance: Ensure popOperationContext() matches pushes
  3. Check MaxContextDataSize__c: Large context may be truncated


Last Updated: April 2026 Guide Version: 2.1