Skip to content

Latest commit

 

History

434 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@apexdevtools/apex-log-parser

npm version npm downloads CI License: BSD-3-Clause

Turn a Salesforce Apex debug log into a typed event tree with execution timings, governor limits and SOQL/DML counts.

It is the parser behind the Apex Log Analyzer VS Code extension and its MCP server.

Features

  • 171 event types parsed into their own classes, covering methods, SOQL, DML, flows, callouts and more. Other lines use a generic class, so none are dropped
  • An event tree with parent/child links, where each entry event is matched to its exit
  • Execution time per node, self and total, in nanoseconds
  • Governor limits per namespace, with a snapshot for each limit block in the log
  • Each limit line's reading as { metric, used, limit }
  • SOQL, DML and SOSL counts summed up the tree
  • Managed package namespaces, with metrics for each
  • No dependencies, ESM only

Install

npm install @apexdevtools/apex-log-parser

Quick start

Given this log:

64.0 APEX_CODE,FINE;APEX_PROFILING,FINEST;CALLOUT,NONE;DB,INFO;NBA,NONE;SYSTEM,NONE;VALIDATION,NONE;VISUALFORCE,NONE;WAVE,NONE;WORKFLOW,NONE
09:18:22.6 (6508409)|USER_INFO|[EXTERNAL]|005000000000AAA|user@example.com|Greenwich Mean Time|GMTZ
09:18:22.6 (6574780)|EXECUTION_STARTED
09:18:22.6 (6600000)|CODE_UNIT_STARTED|[EXTERNAL]|01p4J00000FpS6t|AccountService.refresh()
09:18:22.6 (7000000)|METHOD_ENTRY|[12]|01p4J00000FpS6t|AccountService.loadAccounts()
09:18:22.6 (7100000)|SOQL_EXECUTE_BEGIN|[14]|Aggregations:0|SELECT Id, Name FROM Account WHERE Industry = :industry
09:18:22.6 (9100000)|SOQL_EXECUTE_END|[14]|Rows:50
09:18:22.6 (9200000)|METHOD_EXIT|[12]|01p4J00000FpS6t|AccountService.loadAccounts()
09:18:22.6 (9300000)|DML_BEGIN|[20]|Op:Update|Type:Account|Rows:50
09:18:22.6 (9800000)|DML_END|[20]
09:18:22.6 (9900000)|CODE_UNIT_FINISHED|AccountService.refresh()
09:18:22.6 (10100000)|EXECUTION_FINISHED

This code:

import { type LogEvent, parse } from '@apexdevtools/apex-log-parser';

function printTree(node: LogEvent, depth = 0): void {
  const indent = '  '.repeat(depth);
  const ms = (node.duration.total / 1_000_000).toFixed(2);
  console.log(`${indent}${node.type ?? 'LOG_ROOT'} ${node.text} (${ms}ms)`);
  for (const child of node.children) {
    printTree(child, depth + 1);
  }
}

const log = parse(logData);

printTree(log);

Prints:

LOG_ROOT LOG_ROOT (3.53ms)
  EXECUTION_STARTED EXECUTION_STARTED (3.53ms)
    CODE_UNIT_STARTED AccountService.refresh() (3.30ms)
      METHOD_ENTRY AccountService.loadAccounts() (2.20ms)
        SOQL_EXECUTE_BEGIN SELECT Id, Name FROM Account WHERE Industry = :industry (2.00ms)
      DML_BEGIN DML Op:Update Type:Account (0.50ms)

Note the shape. parse() returns the root, which is itself a LogEvent, so the same walk works from any node. METHOD_EXIT, SOQL_EXECUTE_END and DML_END are not nodes of their own. Each one closes its matching begin event and sets that event's exitStamp and duration.

Summarise a transaction

The root sums the whole tree, so SOQL and DML totals need no walk. Governor limits are on the root too: final is what the transaction had used when the log ended, and peak is the highest each metric reached. Check peak against a limit, because counters can fall mid-log.

To rank methods, walk the tree and sort on duration.self. It excludes children, so it measures time spent in the method itself rather than in what it called.

import { type LogEvent, MethodEntryLine, parse } from '@apexdevtools/apex-log-parser';

const log = parse(logData);
const { final, peak } = log.governorLimits;

console.log(`SOQL: ${log.soqlCount.total} queries, ${log.soqlRowCount.total} rows`);
console.log(`DML:  ${log.dmlCount.total} statements, ${log.dmlRowCount.total} rows`);
console.log(`CPU:  ${final.cpuTime.used}/${final.cpuTime.limit}ms`);
console.log(`SOQL: ${peak.soqlQueries.used} at peak (${peak.soqlQueries.percentUsed}%)`);
console.log(`Heap: ${peak.heapSize.used}/${peak.heapSize.limit} bytes`);

const methods: MethodEntryLine[] = [];
const stack: LogEvent[] = [log];
for (let node = stack.pop(); node; node = stack.pop()) {
  if (node instanceof MethodEntryLine) {
    methods.push(node);
  }
  stack.push(...node.children);
}
methods.sort((a, b) => b.duration.self - a.duration.self);
console.table(methods.slice(0, 10).map((m) => ({ method: m.text, selfNs: m.duration.self })));

API

parse(logData: string): ApexLog is the whole entry point. There is no state to reset between calls, and ApexLogParser.parse gives each call its own parser. ApexLog is the root LogEvent, and adds governorLimits, namespaces, debugLevels, userInfo, entryPoint, truncation, logIssues, parsingErrors, exceptions and eventsById.

There are two entry points. The root exports runtime values only: parse, the ApexLogParser class, and the event classes you need for instanceof narrowing. Every type, and the const companions that go with them, come from @apexdevtools/apex-log-parser/types:

import { parse } from '@apexdevtools/apex-log-parser';
import { LOG_LEVEL, type GovernorLimits } from '@apexdevtools/apex-log-parser/types';

The type declarations in the package document every field, event class and type, so your editor shows them.

Tips

Capture the log at the right levels. The parser reports what the log contains. A log captured at a low level is missing whole categories of line, and the matching fields stay empty. This is the most common surprise:

You want The log needs
duration on method nodes APEX_CODE at FINE or above, plus APEX_PROFILING
governorLimits APEX_PROFILING at FINE or above, which emits the CUMULATIVE_LIMIT_USAGE block
SOQL and DML nodes DB at INFO or above
Flow and Process Builder limit lines WORKFLOW at FINER

All-zero limits mean "not reported". Without a LIMIT_USAGE_FOR_NS block, every governorLimits.final and governorLimits.peak metric stays at { used: 0, limit: 0, percentUsed: null }. That is not a transaction that used nothing.

Read totals from the root. It already sums the tree, so you don't need to walk it to count SOQL or DML.

Use eventIndex as an id. It is unique, increasing and stable across a parse.

Two collections, two meanings. parsingErrors holds lines the parser did not understand, which is a parser problem. logIssues holds problems in the transaction the log describes, such as a truncated log or an unexpected exit.

Requirements

  • Node.js 20 or later. The package targets ES2022 and runs in any runtime with ES modules: Node, Deno, Bun and modern browsers. It reads no files and makes no network calls.
  • ESM only. There is no CommonJS build, so require() does not work.
  • TypeScript declarations ship with the package. No @types install is needed.

Contributing

See CONTRIBUTING.md for development setup, coding standards, and the PR process.

License

BSD-3-Clause - Certinia Inc.

About

Turn a Salesforce Apex debug log into a typed event tree with execution timings, governor limits and SOQL/DML counts. No dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

3 watching

Forks

Releases

Packages

Used by

Contributors

Languages