Skip to content

Fast Start - Logging

Framework: KernDX | Total time: ~30 minutes

> Persistent, queryable logging via platform events — zero DML in your transaction, survives rollbacks.

Before you start:

  • [ ] KernDX package installed in your org
  • [ ] Org configured post-install — verify with the Kern app's Health Check (see Installation guide)
  • [ ] CLI authenticated (sf org open -o YourOrgAlias to verify) — or just use the Developer Console (Gear Icon > Developer Console) for all Apex work
  • [ ] Working in a sandbox or scratch org (not production)

What you'll build: A service class that logs every step of order processing — start, per-record progress, errors, and completion — with all entries correlated under a single ID.

Success looks like: Log entries visible in App Launcher > Kern > Log Entries, showing correlated info/debug/error messages linked to specific Account records. Your test class shows 2/2 pass with 100% coverage.

In one line: kern.LOG_Builder.build().info('Payment processed').forRecord(accountId).emitAt('SVC.charge'); — persisted via platform event, survives rollbacks.


Table of Contents

Expand
  1. Tier 1: See It Work (~2 minutes)
  2. Tier 2: Build Your Own (~20 minutes)
  3. Tier 3: Production Patterns (~5-10 minutes)
  4. Sensitive data is masked by default
  5. Common Issues
  6. What You Now Know
  7. Next Steps

Tier 1: See It Work (~2 minutes)

Run these blocks in Execute Anonymous (Developer Console or VS Code) to see logging in action.

Log at every level

apex
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();

try
{
	kern.LOG_Builder.build().info('Application started').emitAt('MyApp.initialize');
	kern.LOG_Builder.build().debug('Loading configuration').emitAt('MyApp.loadConfig');
	kern.LOG_Builder.build().warn('Configuration missing, using defaults').emitAt('MyApp.loadConfig');
	kern.LOG_Builder.build().error('Failed to connect').emitAt('MyApp.connect');
}
finally
{
	scope.close();
}

Open App Launcher > Kern > Log Entries to see your four entries. Each has a log level, message, and class/method context — and all four share the same Correlation ID so you can filter them as a group.

> Why the scope wrapper? Each .emit() publishes a platform event. Without batching, emitting more > than 150 events in one transaction hits the PublishImmediate governor. LOG_Builder.scope() batches > all emissions inside the try/finally into a single flush when scope.close() is called.

Log an exception with full stack trace

apex
try
{
	Integer result = 1 / 0;
}
catch(Exception error)
{
	kern.LOG_Builder.build().error(error).emitAt('MyApp.calculate');
}

Open the log entry — you'll see the exception type (MathException) and the full stack trace captured automatically.

Correlate logs to a record

apex
Account newAccount = new Account(Name = 'Logging Demo Corp', Phone = '555-0100');
insert newAccount;

kern.LOG_Builder.build().info('Account created successfully')
	.at('MyApp.createAccount')
	.forRecord(newAccount.Id)
	.emit();

kern.LOG_Builder.build().info('Ready for processing')
	.at('MyApp.createAccount')
	.forRecord(newAccount.Id)
	.withContext('industry', 'Technology')
	.emit();

Both log entries link to the Account record. Click View Record on any log entry to jump directly to it.

> When to move to Tier 2: When you want structured logging inside your own classes with automated test > coverage.


Tier 2: Build Your Own (~20 minutes)

> No local project? You can create classes directly in the Developer Console (Gear Icon > Developer Console > > File > New > Apex Class) and run tests from there too (Test > New Run). Paste the code, save, and skip the > sf project deploy start and sf apex run test commands.

Step 1: Create the service class

This service processes Account records and logs every step — start, per-record progress, errors, and completion — correlated under one Scope.

Copy this code exactly as is into force-app/main/default/classes/SVC_OrderProcessor.cls:

apex
/**
 * @description Processes Account records with correlated, structured logging at every step.
 *
 * @see SVC_OrderProcessor_TEST
 *
 * @author your.name@company.com
 *
 * @group Order Management
 *
 * @date February 2026
 */
public with sharing class SVC_OrderProcessor
{
	/** @description Context key for the record count. */
	@TestVisible private static final String CONTEXT_COUNT = 'count';

	/**
	 * @description Processes a list of Account records with full correlated logging.
	 *
	 * @param accounts The Account records to process.
	 */
	public void processOrders(List<Account> accounts)
	{
		kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();

		try
		{
			kern.LOG_Builder.build()
				.info('Order processing started')
				.withContext(CONTEXT_COUNT, accounts.size())
				.emitAt('SVC_OrderProcessor.processOrders');

			for(Account account : accounts)
			{
				try
				{
					kern.LOG_Builder.build()
						.debug('Processing account')
						.forRecord(account.Id)
						.emitAt('SVC_OrderProcessor.processOrders');
				}
				catch(Exception error)
				{
					kern.LOG_Builder.build()
						.error(error)
						.forRecord(account.Id)
						.emitAt('SVC_OrderProcessor.processOrders');
				}
			}

			kern.LOG_Builder.build()
				.info('Order processing completed')
				.withContext(CONTEXT_COUNT, accounts.size())
				.emitAt('SVC_OrderProcessor.processOrders');
		}
		finally
		{
			scope.close();
		}
	}

	/**
	 * @description Emits a single ERROR-level alert.
	 *
	 * @param message The alert message to persist.
	 */
	public void criticalAlert(String message)
	{
		kern.LOG_Builder.build()
			.error(message)
			.emitAt('SVC_OrderProcessor.criticalAlert');
	}
}

What this code does:

  • LOG_Builder.scope() opens a batch scope — all .emit() calls inside the try/finally share one Correlation ID and are flushed together at scope.close(), staying well under the 150-event limit
  • .info() / .debug() / .warn() / .error() set the log level
  • .at('ClassName.methodName') records where the log came from
  • .forRecord(account.Id) links the log entry to the Account being processed
  • .withContext(key, value) adds structured key-value data to the entry
  • .emitAt('Class.method') shorthand for .at(...).emit()

Step 2: Deploy and execute

bash
sf project deploy start -o YourOrgAlias -m "ApexClass:SVC_OrderProcessor" --ignore-conflicts

Test it from Execute Anonymous:

apex
Account newAccount = new Account(Name = 'Order Demo Corp', Phone = '555-0200', BillingCity = 'Melbourne');
insert newAccount;

new SVC_OrderProcessor().processOrders(new List<Account>{ newAccount });
System.debug('Done — check App Launcher > Kern > Log Entries');

> See it in the org: Open App Launcher > Kern > Log Entries. You'll see three entries — an INFO > (started), a DEBUG (per-record), and an INFO (completed) — all sharing the same Correlation ID.

Step 3: Write the test class

Copy this code exactly as is into force-app/main/default/classes/SVC_OrderProcessor_TEST.cls:

apex
/**
 * @description Unit tests for SVC_OrderProcessor.
 *
 * @see SVC_OrderProcessor
 *
 * @author your.name@company.com
 *
 * @group Order Management
 *
 * @date February 2026
 */
@SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs')
@IsTest(SeeAllData=false IsParallel=true)
private class SVC_OrderProcessor_TEST
{
	/** @description Log level string for INFO entries. */
	private static final String LOG_LEVEL_INFO = 'INFO';

	/** @description Log level string for ERROR entries. */
	private static final String LOG_LEVEL_ERROR = 'ERROR';

	/** @description Alert message used in the error path test. */
	private static final String ALERT_MESSAGE = 'Critical system failure';

	/**
	 * @description Verifies that processOrders() creates correlated log entries
	 * queryable by kern__UserId__c after platform event delivery.
	 */
	@IsTest
	private static void shouldCreateScopedLogEntriesForProcessedAccounts()
	{
		Account account = (Account)kern.TST_Builder.of(Account.SObjectType).build();

		kern.LOG_Builder.ignoreTestMode = true;

		Test.startTest();
		new SVC_OrderProcessor().processOrders(new List<Account>{ account });
		Test.getEventBus().deliver();
		Test.stopTest();

		// Query by kern__UserId__c — NOT CreatedById.
		// TRG_PersistLogEntry runs as the Automated Process user, so CreatedById returns zero rows.
		Id currentUserId = UserInfo.getUserId();
		List<kern__LogEntry__c> entries = [
			SELECT kern__LogLevel__c, kern__CorrelationId__c
			FROM kern__LogEntry__c
			WHERE kern__UserId__c = :currentUserId
		];

		Assert.isFalse(entries.isEmpty(), 'Expected correlated log entries after event delivery');

		Boolean foundInfo = false;

		for(kern__LogEntry__c entry : entries)
		{
			if(entry.kern__LogLevel__c == LOG_LEVEL_INFO)
			{
				foundInfo = true;
			}
		}

		Assert.isTrue(foundInfo, 'Expected at least one INFO entry from processOrders()');
	}

	/**
	 * @description Verifies that criticalAlert() persists an ERROR entry with the correct message.
	 */
	@IsTest
	private static void shouldPersistErrorEntryForCriticalAlert()
	{
		kern.LOG_Builder.ignoreTestMode = true;

		Test.startTest();
		new SVC_OrderProcessor().criticalAlert(ALERT_MESSAGE);
		Test.getEventBus().deliver();
		Test.stopTest();

		// Query by kern__UserId__c — NOT CreatedById (see note in test above)
		Id currentUserId = UserInfo.getUserId();
		List<kern__LogEntry__c> entries = [
			SELECT kern__Message__c, kern__LogLevel__c
			FROM kern__LogEntry__c
			WHERE kern__UserId__c = :currentUserId AND kern__LogLevel__c = :LOG_LEVEL_ERROR
		];

		Assert.isFalse(entries.isEmpty(), 'Expected one ERROR entry from criticalAlert()');
		Assert.areEqual(ALERT_MESSAGE, entries[0].kern__Message__c, 'Message should match the alert text');
	}
}

> About the query filter: Tests must filter kern__LogEntry__c by kern__UserId__c, not > CreatedById. The platform trigger that persists log entries (TRG_PersistLogEntry) runs as the > Automated Process user, so CreatedById returns zero rows. kern__UserId__c is set to the ID of the > user who emitted the log.

> About the annotations: @IsTest(SeeAllData=false IsParallel=true) keeps tests isolated and > enables parallel execution. @SuppressWarnings('PMD.ApexUnitTestClassShouldHaveRunAs') suppresses a > code analysis rule — in production tests, consider wrapping logic in System.runAs(testUser) to > verify profile and permission set access.

Step 4: Deploy and run tests

bash
sf project deploy start -o YourOrgAlias -m "ApexClass:SVC_OrderProcessor_TEST" --ignore-conflicts
sf apex run test -o YourOrgAlias -t SVC_OrderProcessor_TEST --code-coverage --synchronous --result-format human

Expected output:

text
=== Test Results
Tests Ran        2
Passing          2
Failing          0

Key Patterns

PatternExampleWhy
Scope batchingLOG_Builder.scope() + try/finallyPrevents PE governor hit; groups entries under one Correlation ID
Log at method boundaries.info('started') + .info('completed')Trace execution flow in production
Per-record context.forRecord(account.Id)All logs for one record queryable together
Debug inside loops.debug('Processing account')Filtered in production; visible in sandbox
Catch-log-continue.error(error) in catch, no rethrowPersistent record; processing continues for remaining items
Single-shot alert.error('message').emitAt(...)One-liner for config/startup errors
Query by UserIdkern__UserId__c = UserInfo.getUserId()NOT CreatedById — TRG_PersistLogEntry runs as Automated Process user

Tier 3: Production Patterns (~5-10 minutes)

Log levels

See the Logging Guide for the complete log level reference and filtering configuration.

LevelMethodWhen to use
ERROR.error(exception) or .error('message')Failures, exceptions, data corruption
WARN.warn('message')Business rule violations, missing optional data, degraded performance
INFO.info('message')Key business events, state transitions, completion messages
DEBUG.debug('message')Implementation details, variable values (filtered in production)

Flood control for repeated events

When the same event fires in a hot loop, tag it with a fingerprint so it doesn't flood the log table — the first occurrence keeps a full entry and repeats roll up into a daily counter:

apex
kern.LOG_Builder.build()
		.warn('Payment gateway retry failed')
		.withFingerprint('payment-gateway-retry')
		.emitAt('PaymentSync.run');

Use a stable identity for the kind of event (never a record Id or timestamp). See Log Grouping & Flood Control in the Logging Guide for reading grouped logs and reporting on occurrence counts.

Shorthand: .emitAt()

When you don't need .forRecord() or .withContext(), use .emitAt() as shorthand for .at().emit():

apex
// These are equivalent:
kern.LOG_Builder.build().info('Done').at('MyClass.myMethod').emit();
kern.LOG_Builder.build().info('Done').emitAt('MyClass.myMethod');

Use .emitAt() for simple messages. Use .at() + chain + .emit() when adding .forRecord() or .withContext().

Exception logging

Always pass the Exception object — never .getMessage():

apex
catch(Exception error)
{
	// Captures exception type, message, AND full stack trace
	kern.LOG_Builder.build().error(error).at('MyClass.myMethod').forRecord(recordId).emit();
	throw error;
}

Testing with logs

By default, logs don't emit in @IsTest context. Set kern.LOG_Builder.ignoreTestMode = true before your log call to enable logging in tests, then call Test.getEventBus().deliver() to deliver the platform events synchronously before asserting.

Here's the complete working pattern:

apex
kern.LOG_Builder.ignoreTestMode = true;

Test.startTest();
new SVC_OrderProcessor().processOrders(accounts);
Test.getEventBus().deliver();
Test.stopTest();

// Query by kern__UserId__c — NOT CreatedById
Id currentUserId = UserInfo.getUserId();
List<kern__LogEntry__c> entries = [
	SELECT kern__Message__c
	FROM kern__LogEntry__c
	WHERE kern__UserId__c = :currentUserId
];

Assert.isFalse(entries.isEmpty(), 'Should have log entries');

> Key steps: ignoreTestMode = true → emit logs → Test.getEventBus().deliver() → query by > kern__UserId__c → assert.

> Why inline SOQL for kern__LogEntry__c? When a managed-package object is referenced through > kern.QRY_Builder.selectFrom(<kern__sobject>.SObjectType) and then chained with .condition(<kern__sobject>.<kern__field>), > Apex's namespace tokenizer struggles with the doubly-prefixed field token. Inline SOQL is the simplest > idiomatic pattern for subscriber tests against managed-package objects when no SEL_* selector exists. > For your own subscriber objects, kern.QRY_Builder works without issue.

Scoped logging

LOG_Builder.scope() groups all log entries emitted inside a try/finally block under a single Correlation ID, and batches their platform event publications into one flush at scope.close(). This keeps you well clear of the 150-event PublishImmediate governor limit.

apex
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();

try
{
	kern.LOG_Builder.build().info('Batch started').emitAt('BATCH_ProcessAccounts.execute');
	// ... process records ...
	kern.LOG_Builder.build().info('Batch completed').emitAt('BATCH_ProcessAccounts.execute');
}
finally
{
	scope.close();
}

Run this from Execute Anonymous to see correlated scoped entries:

apex
kern.LOG_Builder.LogScope scope = kern.LOG_Builder.scope();

try
{
	kern.LOG_Builder.build().info('Step 1: Loading data').emitAt('MyApp.processRecords');
	kern.LOG_Builder.build().info('Step 2: Validating').emitAt('MyApp.processRecords');
	kern.LOG_Builder.build().info('Step 3: Complete').emitAt('MyApp.processRecords');
}
finally
{
	scope.close();
}

System.debug('Check App Launcher > Kern > Log Entries — all three entries share the same Correlation ID.');

Logging from Flows

Use invocable actions to log from Flows. This creates correlated log entries visible in the same Log Entries tab as Apex logs.

  1. Open Setup > Flows and edit your Flow (or create a new one)
  2. Add an Action element:
    • Search for Start Flow Correlation
    • Set Label to Start Logging
    • Set flowName to the name of your Flow (e.g., Account_Onboarding)
    • Set recordId to the record ID variable (e.g., {!recordId})
    • Store the output correlationId in a text variable (e.g., {!correlationId})
  3. At key steps, add Action elements:
    • Search for Log Flow Event
    • Set correlationId to {!correlationId}
    • Set message to a description (e.g., Account created successfully)
    • Set Log Level (input name logLevel) to INFO, WARN, or ERROR
  4. At the end of the Flow, add an Action element:
    • Search for End Flow Correlation
    • Set flowName to the same Flow name used in step 2
    • Set correlationId to {!correlationId}
    • Set Success (input name success) to {!$GlobalConstant.True} (or False for failure paths)
  5. Save and Activate

All log entries from the Flow share the same Correlation ID, so you can filter them together in App Launcher > Kern > Log Entries.

Logging from LWC

LWC logging uses the built-in consoleLog() and consoleError() methods from ComponentBuilder. These write to the browser console (not to kern__LogEntry__c). For server-side persistent logging, use LOG_Builder in your Apex controller methods.

javascript
// In any ComponentBuilder LWC:
this.consoleLog('User clicked save');
this.consoleError('Save failed', error);

See the LWC Guide for the full ComponentBuilder reference, or the Logging Guide for server-side logging patterns.


Sensitive data is masked by default

Log entries pass through the data masking framework before persistence. Out of the box, two rules fire:

  • MaskSecretKeys — redacts common secret JSON keys (password, token, apiKey, authorization, bearer, client_secret, private_key, access_token, refresh_token) anywhere they appear in a field value.
  • MaskPaymentCard — redacts 13–19 digit sequences that pass the Luhn (mod-10) checksum, covering all major card brands; digits may be separated by spaces or hyphens. The Luhn check filters out most transaction IDs, order numbers, and other long digit runs that would otherwise false-positive as card data. (Replaces the original MaskCreditCard rule, which still ships for compatibility.)

So LOG_Builder.build().info('Payload: ' + JSON.serialize(payload)).emitAt(...) is safe even if payload contains a password or card number — the persisted LogEntry__c.Message__c will have them redacted. Fifteen more rules (SSN, IBAN, SWIFT/BIC, MBI, health keywords, email, US phone, JWT, AWS access key, URL basic auth, authorization header, private IPv4, postal address, free text, international phone) ship as inactive templates — flip kern__MaskingRule__mdt.IsActive__c = true and add a kern__MaskingTarget__mdt record wiring the rule to the fields that need it.

> Wiring gotcha. Rules carry two optional filters that override explicit target wiring: > ApplicableFieldTypes__c (which DisplayTypes the rule fires on) and MinInputLength__c (minimum value > length). If a MaskingTarget__mdt points at a specific Field__c whose type or value length is rejected > by the rule's filter, the rule will not fire on that field. For ApplicableFieldTypes__c mismatches > the framework logs a one-time warn LogEntry to surface the misconfiguration; MinInputLength__c > mismatches are silent. Either widen the rule filter or remove the target.

> Masking performance telemetry. Masking runs inside trigger dispatch. When a batch's masking duration > is material, enable LogSetting__c.EnableMaskerPerformanceLogging__c (default off) to emit one aggregate > LogEntry__c per trigger batch (not per record). The log fires only when total masking time for that > batch meets or exceeds LogSetting__c.MaskerPerformanceThresholdMs__c (default 100ms).


Common Issues

ProblemCauseFix
No log entries appear in testsLogs suppressed in test contextSet kern.LOG_Builder.ignoreTestMode = true before logging
Log entries empty after ignoreTestModePlatform events not deliveredCall Test.getEventBus().deliver() after emitting and before asserting
Query returns zero rows in testsFiltering by CreatedById instead of kern__UserId__cUse kern__LogEntry__c.kern__UserId__c = UserInfo.getUserId()TRG_PersistLogEntry runs as the Automated Process user, so CreatedById never matches the running user
Variable does not exist: kern__LogEntry__cMissing namespace prefixUse kern__LogEntry__c and kern__FieldName__c (double underscore) for SObjects and fields
.error(error.getMessage()) loses stack tracePassing String instead of ExceptionUse .error(error) to pass the Exception object directly
Logs have no method contextMissing .at() or .emitAt()Always include .emitAt('Class.method') or .at('Class.method')
PE governor error after many emits>150 PublishImmediate events in one transactionWrap emit calls in kern.LOG_Builder.scope() + try/finally to batch the flush
Test.getEventBus() not foundRunning outside @IsTest contextTest.getEventBus().deliver() only works inside test methods
Sensitive value appears raw in a logField not covered by a masking target, or rule is inactiveAdd a kern__MaskingTarget__mdt record with the rule, SObjectType, and field (blank Field__c for a wildcard) — see Logging Guide

What You Now Know

ConceptWhat it does
kern.LOG_Builder.build()Creates a fluent log entry builder
.info() / .warn() / .error() / .debug()Sets the log level
.at('Class.method')Records where the log came from
.forRecord(Id)Links the log entry to a specific record
.withContext(key, value)Adds structured key-value data
.withSummary(message)Sets a short searchable summary on the entry
.emitAt('Class.method')Shorthand for .at().emit()
.emit()Publishes the log as a platform event
LOG_Builder.scope()Batches all emits under one Correlation ID; prevents PE governor hit
ignoreTestMode = trueEnables logging in test context
Test.getEventBus().deliver()Delivers platform events synchronously in tests
kern__UserId__cThe field to filter on when querying log entries — NOT CreatedById

Key patterns:

  • Wrap bulk logging in LOG_Builder.scope() + try/finally (batches emits, prevents governor hit)
  • Log at method boundaries (entry + exit) for execution tracing
  • Always pass Exception objects, not .getMessage() strings
  • Use .forRecord() on every log call to enable record-level filtering
  • Add .withContext() for structured data instead of string concatenation
  • In tests: ignoreTestMode = true → emit → Test.getEventBus().deliver() → query by kern__UserId__c → assert

Next Steps

TopicLink
Fast Start - Feature FlagsFast Start - Feature Flags
Fast Start - Test DataFast Start - Test Data
Code Scanning (catch System.debug anti-patterns)Fast Start - Code Scanning
Logging Developer GuideLogging - Guide
LOG_Builder API Referencereference/apex/LOG_Builder.md
LogEntry__c Objectreference/objects/LogEntry__c.md
Flow Loggingreference/apex/FLOW_LoggerStart.md