EurekaLog
Menu

The EurekaLog Developer Guide

13. ELogging and ELogBuilder: your entries and the report text

When a report tells you where an exception happened but not what the application was doing, add a small application log: operation names, relevant inputs, completed stages and timings. Choose data that explains the failure without recording passwords, tokens or unnecessary personal data.

Keep four mechanisms separate:

Task Mechanism Result
Record your application's progress ELogging A .csl log, automatically attached to a packed .elp report
Change the report's textual representation ELogBuilder Text, XML or JSON returned by a builder
Record an exception in Windows Event Viewer System logging, chapter 23 An event referring to a registered message resource
Diagnose EurekaLog's own execution Internal diagnostics, chapter 5 Diagnostic tracing enabled separately from ordinary application entries

Opening a log and building report text do not deliver a report. Configure and verify delivery separately (chapter 3). Likewise, calling ELogError records an entry; it is not a substitute for passing an exception to a handler.

13.1 The log file

ELogOpen

procedure ELogOpen(const AFileName: String = '';
  const ABufferSize: Cardinal = 1024*1024);   // ELogging

Call this at application startup, before other logging routines. Supply an absolute full file name; .csl is the recommended extension. The default buffer is 1,048,576 bytes (1 MiB). With buffering enabled, entries are flushed periodically or explicitly by ELogFlush. Setting ABufferSize to 0 writes directly to disk and can significantly reduce performance.

Two startup details matter:

  • Entries written before ELogOpen are not lost. They go into an internal log, and that internal log is copied into the real log file once you call ELogOpen. If you do not want the older entries, call ELogClear before ELogOpen - the documented way to start a session with a clean file.
  • The call may be ignored if the application is already streaming into a log file owned by another module - another executable or a DLL - of the same application.

For a desktop application running under an interactive user, put this procedure in an application unit and call it once during startup. It uses SysUtils and ELogging. The example requires an existing, writable %APPDATA% folder; services should instead use a deliberately provisioned folder writable by their service account. It does not create directories.

uses
  SysUtils, ELogging;

procedure OpenApplicationLog;
var
  Folder, FileName: String;
begin
  Folder := GetEnvironmentVariable('APPDATA');
  if Folder = '' then
    raise Exception.Create('APPDATA is not available for application logging');
  FileName := IncludeTrailingPathDelimiter(Folder) +
    ChangeFileExt(ExtractFileName(ParamStr(0)), '.csl');
  ELogOpen(FileName, 1024 * 1024); // buffer size in bytes
  ELogNote('Application session started');
  ELogFlush;
end;

Check that the file contains the session entry, then provoke a controlled report in a development build and inspect the packed report's attachments. These are separate checks: a file on disk proves logging, not attachment delivery. If the file is missing, check the resolved path and account's permissions first; in a multi-module application, also check whether another module owns the log. Do not call ELogClear as a repair step: it deliberately erases evidence.

ELogFlush

procedure ELogFlush(const ACloseAllFunctions: Boolean = False);   // ELogging

Writes cached entries to disk; call it before an unexpected termination you deliberately initiate, while your code can still run. It does nothing when caching is disabled (ABufferSize = 0). It cannot recover buffered entries after the process has already died and does not promise survival of a power loss. The reference does not describe ACloseAllFunctions; leave it at its default unless your installed implementation provides the required contract.

ELogClear

procedure ELogClear;   // ELogging

Erases all content of the log file. Its documented purpose is to clear the log of old entries - and, as noted above, it is what you call before ELogOpen when the internal log from earlier in the process should not become part of the file.

13.2 Writing an entry

ELog

function ELog: IEurekaLogger; overload;                                 // ELogging
procedure ELog(const AMsg: String); overload;
procedure ELog(const AMsg: String; const AValue: String); overload;
// further two-argument overloads, per type of AValue:
//   PChar, Char, Integer, Word, Int64, Boolean, TPoint, TSmallPoint, TRect,
//   TGUID, TObject, TExceptionPointers, TExceptionRecord, TContext, TResponse
//   LongWord and UInt64, each with AAsHex: Boolean = False
//   Extended, with ADigits: Integer = 2

The three shapes the documentation calls out are the ones you will actually use: the overload without arguments logs nothing immediately and returns the IEurekaLogger interface so that you can shape the entry yourself; the overload with a single AMsg logs that text as is; and the two-argument overloads log the pair as AMsg = AValue.

Two formatting parameters exist for the numeric overloads: AAsHex asks for hexadecimal output, "like '0045FDA0'", and defaults to False; ADigits is how many digits to list after the decimal separator, so 2 means '0.00', and defaults to 2.

ELog is the only entry-writing function that covers the general case. Every other entry-writing routine either presets a meaning (note, error, checkpoint), covers a type the overload set does not, or returns the interface for shaping.

Entries with a stated meaning

procedure ELogNote(const AMsg: String);       // ELogging
procedure ELogWarning(const AMsg: String);    // ELogging
procedure ELogError(const AMsg: String);      // ELogging
procedure ELogReminder(const AMsg: String);   // ELogging

These add a note, warning, error or reminder instead of normal text. Prefer operational messages such as ELogWarning('Import completed with rejected rows') over implementation reminders that will be repeated in every production session.

Values that have a routine of their own

The overload set covers numbers, strings, booleans, points, rectangles, GUIDs, objects and exception records - but not every type worth logging, and not every one of those unambiguously. So the unit spells a few of them out, and for all but one of those the documentation gives the same reason: the separate function is required to avoid issues with overloads resolving. The exception is ELogMemoryAsHex, whose page documents a memory dump and names no resolution problem.

procedure ELogCurrency(const AMsg: String; const AValue: Currency);        // ELogging
procedure ELogDateTime(const AMsg: String; const AValue: TDateTime);       // ELogging
procedure ELogPointer(const AMsg: String; const AValue: Pointer);          // ELogging
procedure ELogWindowHandle(const AMsg: String; const AValue: HWND);        // ELogging
procedure ELogWinError(const AMsg: String;
  const ALastError: TSysErrorCode = TSysErrorCode(0));                     // ELogging
procedure ELogException(const E: TObject; const AMsg: String = '');        // ELogging
procedure ELogMemoryAsHex(const AMsg: String; const ABuffer: Pointer;
  ASize: Integer);                                                         // ELogging
  • ELogCurrency and ELogDateTime cover the Currency and TDateTime values a generic numeric overload would otherwise swallow, and ELogPointer and ELogWindowHandle cover a raw pointer and a window handle for the same documented reason.
  • ELogWinError is for system error codes; ALastError is a TSysErrorCode, defaulting to TSysErrorCode(0) - the usual call is ELogWinError('GetLastError', GetLastError);.
  • ELogException takes an exception object (not necessarily one descending from Exception) with an optional name for it as AMsg, for the documented pattern ELogException(E); HandleException(E); inside the except block.
  • ELogMemoryAsHex dumps ASize bytes of memory from ABuffer into the log; the documentation's example dumps a stream: ELogMemoryAsHex('Stream', Stream.Memory, Stream.Size);.

Each has an interface method that keeps its name minus the leading ELog - LogCurrency for ELogCurrency, and so on - so the same value can be logged on an entry that has already been shaped (section 13.3).

Checking before you log

function ELogAssigned(const AMsg: String; const AValue: TObject): Boolean;      // ELogging
function ELogAssignedPtr(const AMsg: String; const AValue: Pointer): Boolean;   // ELogging

Both add a status to the log - nil is logged as "not assigned", anything else as "assigned" - and both return whether the value was assigned; the documentation's example uses that return as a guard, if not ELogAssigned('SomeObject', SomeObject) then Exit;.

procedure ELogIf(const AExpression: Boolean; const AMsg: String); overload;   // ELogging
procedure ELogIf(const AExpression: Boolean; const AMsg: String;
  const AValue: String); overload;
// also overloaded with AValue: Integer, and with AValue: TObject
procedure ELogIfDateTime(const AExpression: Boolean; const AMsg: String;
  const AValue: TDateTime);                                               // ELogging

ELogIf is documented as two cases and nothing else: when AExpression is False the function does nothing, and when it is True it acts as ELog. ELogIfDateTime is the same for a TDateTime value, and again exists to keep overload resolution unambiguous. The point of the pair is that the condition and the entry stay on one line of source, which is what keeps log-heavy code readable.

Measuring time

function GetPerformanceCount: Int64;                                // ELogging
procedure ELogInterval(const AMsg: String; const AStart: Int64);    // ELogging

Treat the Int64 from GetPerformanceCount as an opaque timestamp, not seconds or milliseconds. Pass it unchanged to ELogInterval; the API description does not specify a unit for arithmetic on the raw value. The resulting entry describes elapsed time.

This complete helper belongs in a unit using SysUtils and ELogging. It times a real conversion and logs even a failed conversion. A timing entry proves how long the attempt took, not that it succeeded.

function ParseLoggedInteger(const Text: String): Integer;
var
  Started: Int64;
begin
  Started := GetPerformanceCount;
  try
    Result := StrToInt(Text);
  finally
    ELogInterval('Integer conversion elapsed', Started);
  end;
end;

This is a stopwatch, not a profiler: choose operation boundaries that help you compare runs.

Checkpoints and separators

procedure ELogAddCheckPoint;        // ELogging
procedure ELogResetCheckPoint;      // ELogging
procedure ELogAddSeparator;         // ELogging
procedure ELogAddResetSeparator;    // ELogging

ELogAddCheckPoint increments a checkpoint counter and adds a new checkpoint entry to the log, and ELogResetCheckPoint resets the counter so the following calls start over. The documentation's example is the reason they exist: reset the counter, then add a checkpoint after each of a series of dangerous operations. When a run dies, the highest checkpoint number that made it into the file is a lower bound on how far it got.

ELogAddSeparator adds a simple separation line to indicate visually where one block of the log ends and the next begins. ELogAddResetSeparator does the same and additionally resets all current indentation back to zero - and here the documentation carries a warning: calling it inside any logged function will break the flow of called functions, because each logged call increases the indentation. Use it between top-level blocks, not inside a routine you are tracing.

13.3 Shaping an entry: IEurekaLogger

IEurekaLogger = interface(IUnknown)
  ['{76A72C59-97B1-4561-AE67-67F8B9F67EE8}']
  // the methods listed in this section
end;

The interface which holds a single entry in the log file, and the value returned by the ELog-like functions. The documentation's description of what it is for is short: you use it to alter the properties of the created log entry.

Its methods are of two kinds. The first changes the entry and hands the same entry back, so the changes chain; the second is the Log* family, which mirrors the free functions of section 13.2 on an already-shaped entry. Almost all of them return the interface, which is what makes a chain such as ELog.Category('My Category').FontColor(clRed).Log('SomeVar', SomeVar) possible - the documentation's own example. The exceptions are the five that write a whole entry and end it: LogInterval, AddCheckPoint, ResetCheckPoint, AddSeparator and AddResetSeparator are procedures, so they terminate a chain instead of continuing it.

Category, Color, FontColor, Kind, Details

function Category(const ACategoryName: String): IEurekaLogger;   // ELogging
function Color(const AColor: DWORD): IEurekaLogger;             // ELogging
function FontColor(const AColor: DWORD): IEurekaLogger;         // ELogging
function Kind(const AMsgType: TLogMsgType): IEurekaLogger;      // ELogging
function Details(const ADetails: String): IEurekaLogger;        // ELogging

Category groups entries: the name is an arbitrary string, and the documentation adds the one fact you need in order not to collide with the product's own logging - EurekaLog's categories start with 'EurekaLog'. Color and FontColor set the entry's colour and its font colour as DWORD colour values, with documented defaults of clWhite for the first and clBlack for the second; FontColor(clRed) on an error entry is what makes the eye land on it. Kind sets the entry's kind as a TLogMsgType (section 13.5) and Details its details, and both carry the same documented remark: usually you do not need to call this - the free functions already set sensible values. In practice Category and the two colours are the ones a developer reaches for, and Kind and Details are there for the code that produces entries of its own.

The Log methods on the interface

function Log(const AMsg: String): IEurekaLogger; overload;
function Log(const AMsg: String; const AValue: String): IEurekaLogger; overload;
// and the rest of the two-argument overloads of ELog, with the same AAsHex and
// ADigits parameters - but no parameterless Log and no PChar overload

The interface repeats the free-function behavior described in section 13.2; use that section for parameters and interpretation. The methods are Log, LogAssigned, LogAssignedPtr, LogCurrency, LogDateTime, LogPointer, LogWindowHandle, LogWinError, LogException, LogMemoryAsHex, LogNote, LogWarning, LogError, LogReminder, LogIf, LogIfDateTime, LogInterval, Enter, AddCheckPoint, ResetCheckPoint, AddSeparator and AddResetSeparator. Each name is the free function's with the leading ELog dropped - LogCurrency for ELogCurrency, AddCheckPoint for ELogAddCheckPoint, Enter for ELogEnter - and the mapping goes no further than that, so parameters and meaning are the same as on the free function, with the one difference that LogWinError's error code has no default on the interface.

13.4 Sub-routine entries: ELogEnter and IEurekaLoggerParams

ELogEnter

function ELogEnter(const AFuncName: String = ''): IEurekaLoggerParams; overload;   // ELogging
function ELogEnter(const ASelf: TObject;
  const AMethodName: String = ''): IEurekaLoggerParams; overload;
function ELogEnter(const ASelf: TClass;
  const AMethodName: String = ''): IEurekaLoggerParams; overload;

Creates a new log entry about entering a method, function or procedure. The three overloads are the three ways of naming it: by function name, by object and method name, by class and method name - and in the last two the documentation requires that AMethodName is the name of a method that belongs to ASelf.

The call must be the first call in the sub-routine, and EurekaLog then logs the exit from it automatically. If you want to log an early exit or state the result, the returned interface has Ret for that, and the entry can be altered like any other through the IEurekaLogger methods it inherits.

The documentation marks one requirement as important: you have to call either D or C on the returned IEurekaLoggerParams. Otherwise nested log entries may have the wrong indentation in the resulting log file - which is precisely what breaks the trace of the call sequence.

The pattern is to build the entry in one expression - name first, then a P per argument, then the terminator, as in ELogEnter(Self, 'DoSomething').P('AValue', AValue).D - and, in a function, to keep that expression in a local IEurekaLoggerParams variable so that Ret(Result) can be called before leaving.

For example, this complete function uses ELogging. Keep the returned interface local, finish the parameter list with D, and record the return value after computing it:

function IsPositive(const AValue: Integer): Boolean;
var
  Entry: IEurekaLoggerParams;
begin
  Entry := ELogEnter('IsPositive').P('AValue', Int64(AValue)).D;
  Result := AValue > 0;
  Entry.Ret(Result);
end;

Use C instead of D if the call should initially appear collapsed. Broken indentation is not proof of a different call order: first check missing D/C and reset separators inside traced calls.

IEurekaLoggerParams: P, D, C, Ret

IEurekaLoggerParams = interface(IEurekaLogger)
  ['{05A12A38-D3DA-49B6-8935-4427677AD740}']
  // the methods listed in this section
end;

A specialised version of IEurekaLogger which allows you to specify additional parameters for the current log entry. The documentation names the three things you use it for: P to log an additional parameter, D or C to indicate the end of the parameter list, and Ret to indicate the result of the sub-routine.

function P(const AName, AValue: String): IEurekaLoggerParams; overload;   // ELogging
// also overloaded with AValue: Int64, Pointer, TObject, Boolean, TGUID,
// TDateTime and System.UInt64

AName is documented as the name of the argument as it appears in source code - that is what makes the resulting log line readable against the signature. Like the rest of the interface, P returns the entry so the parameters can be chained, and the same important warning applies to its result: call either D or C, or nested entries may have the wrong indentation.

function D: IEurekaLoggerParams;   // ELogging
function C: IEurekaLoggerParams;   // ELogging

D completes the logging of the function call; the documentation's description of when to call it is to indicate that there are no more arguments to the function. C is the same, except that the function call will appear collapsed in the log - the entry is there, but the reader sees it folded away rather than expanded. Both return the entry, which is documented as the value to use if you want to log the Result.

procedure Ret(const AResult: String); overload;   // ELogging
// also overloaded with Integer, Cardinal, Int64, System.UInt64, TObject,
// TResponse, Boolean and TDateTime - plus a parameterless overload

Ret logs the return value of the sub-routine. Its overload set is the set of values worth quoting as a result, and the parameterless overload is for the case where the fact of the return is the information and the value is not - the early exit the documentation mentions under ELogEnter.

13.5 Entry kinds and contents: TLogType

TLogType = (
  elmtInfo, elmtWarning, elmtError, elmtCheckPoint,
  elmcObject, elmcComponentAsText, elmcStringList, elmcText,
  elmtNote, elmtEnterMethod, elmtExitMethod,
  elmtSeparator, elmtInactiveSeparator, elmtClear, elmtScratchPad, elmtSaveLogFile,
  elmcBitmap, elmcTextFile, elmcWindow, elmcMemory, elmcMemoryStatus,
  elmcCustomText, elmcCustomProperties,
  elmtReminder, elmtLevel1, elmtLevel2, elmtLevel3, elmtLevel4, elmtLevel5,
  elmtLevel6, elmtLevel7,
  elmcColor, elmcRegistry, elmcSystemInfo, elmcVersionInfo, elmcXmlData,
  elmcStackTrace, elmtException, elmcCustomTable, elmtResetSeparator,
  elmtAppStartSeparator, elmcCollection, elmtCollapsedMethod,
  elmtExitMethodCollapse, elmcCustomHierarchy, elmtTiming,
  elmtRed, elmtOrange, elmtYellow, elmtGreen, elmtBlue, elmtIndigo, elmtViolet
);   // ELogging

The base type for all "types" and "kinds" in the log file. Its members come in two families, interleaved above: the elmt* names are the kinds of entry a human reads (info, warning, error, checkpoint, note, the markers of a traced call, the separators, the reminder, the seven levels, the timing entry, the colour markers red through violet), and the elmc* names are the kinds of content an entry can carry (object, text, bitmap, window, memory, registry, system and version information, XML data, stack trace, and the custom forms). One more documented fact: the type is compatible with CodeSite's csmXYZ constants from CodeSiteMsgTypes.inc.

TLogMsgType = TLogType;      // ELogging
TLogMsgContent = TLogType;   // ELogging

Both are aliases of TLogType, and each comes with a caution that narrows it. TLogMsgType specifies the kind of a single log entry, and you may only use items starting with elmt; it is also the type of IEurekaLogger.Kind's parameter. TLogMsgContent specifies the kind of data in the entry - the type of its content - and you may only use items starting with elmc.

The distinction matters because the two families share one enum: an entry has one kind (what it is) and, where it carries structured data, one content (what the data is), and the compiler will not stop you from passing the wrong family.

13.6 The report text: ELogBuilder

Everything above writes application entries. ELogBuilder instead builds the bug report's text. Multi-file reports (.elp/ZIP) are assembled by the dialog class, not by the builder, and sending belongs to the delivery pipeline (chapter 3). Changing the builder is not necessary to add an application breadcrumb.

BuildBugReport

function BuildBugReport(const AExceptionInfo: TEurekaExceptionInfo;
  const ALogBuilderClass: TBaseLogBuilderClass = nil): String;   // ELogBuilder

Builds the bug report for the given exception information, using the default builder or the one you name. ALogBuilderClass defaults to nil, and nil means the class in the LogBuilderClass variable - not a hard-coded class. The return value is the textual report, and the documentation says its exact value depends on the class used. Its documented uses are to re-create a report or to create one on demand, to store the text somewhere of your own choosing; the page also says plainly that you normally do not have to call it, because for exceptions EurekaLog caught the report is available at any time through the TBaseDialog.BugReport property.

LogBuilderClass

LogBuilderClass: TBaseLogBuilderClass = TLogBuilder;   // ELogBuilder

The global variable that specifies the log builder class EurekaLog uses to build reports. Its default value is TLogBuilder, and the documented effect of changing it is total: set this variable to your own class and you alter the content of all text bug reports - the bluntest of the levers, since the class you would be replacing is the subject of the rest of this chapter.

TBaseLogBuilder

TBaseLogBuilder = class(TObject)   // ELogBuilder
protected
  FBuilded: Boolean;
  function BuildReport: String; virtual; abstract;
  procedure Refresh; virtual;
public
  constructor Create(const AExceptionInfo: TEurekaExceptionInfo); virtual;
  function ToString: String; override;
  // Additional properties; full public declarations are not specified:
  //   Report, ExceptionInfo, CallStack, Options, CustomFields
end;

The abstract class for any specific log builder - and, in the documentation's words, any specific sub-class must implement BuildReport. The built-in sub-classes are TLogBuilder (the default), TXMLLogBuilder, TJSONLogBuilder and TDummyLogBuilder. The page also repeats that normally you do not need to create instances of this class by hand, and points at BuildBugReport. Of those five properties, ExceptionInfo holds the object passed to the constructor - a TEurekaExceptionInfo - and Report is a String in effect, because ToString returns its value.

  • Create takes the exception information the report is about and keeps it (see ExceptionInfo). Creating the instance does not generate the report: it is created on demand.
  • Report generates and returns the bug report for that exception information; accessing it is what triggers BuildReport. ToString returns the same value - the documentation states that calling it is the same as accessing Report.
  • BuildReport generates the actual report; it is the one member a custom builder must supply. It carries a caution: do not call it directly - call ToString or read Report instead, because the class caches what BuildReport produces for performance reasons and a direct call bypasses that.
  • FBuilded is that cache flag: True means Report returns the report already built, False means it generates a new one and caches it. Caching is automatic, and BuildReport does nothing special about it. Refresh sets FBuilded to False so the next access rebuilds - the documentation's example for needing it is a change in the exception or environment information, such as the user's e-mail being changed in the dialog. It is a protected method, so the call is the sub-class's.
  • ExceptionInfo is the object passed to the constructor. A note tells a sub-class how to read it: there are CallStack and Options properties which should be used instead of ExceptionInfo.CallStack and ExceptionInfo.Options - they default to those two, and they are the properties a sub-class overrides to customize report content without affecting the exception information. Options is the EurekaLog options object.
  • CustomFields is a set of name-value pairs used as custom data in the report, corresponding to the soCustomData option. Fill it right after creating the builder and before touching Report - the tip is that TStrings.Values makes the pairs easy to set. Changing it does not refresh the report automatically: Refresh is what regenerates it, and being a protected method it belongs in the sub-class.

The builders you can pick

TLogBuilder = class(TBaseLogBuilder);       // ELogBuilder
TXMLLogBuilder = class(TBaseLogBuilder);    // ELogBuilder
TJSONLogBuilder = class(TBaseLogBuilder);   // ELogBuilder
TDummyLogBuilder = class(TBaseLogBuilder);  // ELogBuilder

TLogBuilder is the default log builder for EurekaLog 7 and above, and the default value of LogBuilderClass. Its page shows the shape of what it produces: a header with the version, then labelled sections - Exception: with the address, type, message, ID and send count, User: with name and e-mail, Steps to reproduce: with the text, and Call Stack Information: as the framed table of methods, details, stack, address, module, offset, unit, class, procedure and line. The labels in the example are numbered (2.2, 3.2, 8.1).

TXMLLogBuilder saves XML bug reports and TJSONLogBuilder saves JSON ones, both described as log builder classes for EurekaLog 7 and above; they are the reason BuildBugReport's result is documented as builder-dependent rather than as one fixed format. TDummyLogBuilder can generate a pre-made bug report - pass any string to its constructor and that string is returned as the bug report - but it cannot be used as the default builder, because there is no way to pass the report string through the alternative constructor the global variable would use; what it is for is reusing an already-generated report in dialogs.

Intercepting report generation

TELEvReportGenMeth = procedure(AExceptionInfo: TEurekaExceptionInfo;
  ALogBuilder: TBaseLogBuilder; var CallNextHandler: Boolean) of object;   // ELogBuilder
TELEvReportGenProc = procedure(const ACustom: Pointer;
  AExceptionInfo: TEurekaExceptionInfo; ALogBuilder: TBaseLogBuilder;
  var CallNextHandler: Boolean);                                          // ELogBuilder

The method and procedural forms of the event handler for the OnReportBeginGen and OnReportEndGen events - the same type is used for both, and both are called before creating the bug report. The documentation's summary is that the event is where you intercept the creation of bug reports, and the handler receives the exception information and the log builder instance about to do the work. In the procedural form, ACustom is the same pointer that was passed when the handler was registered. CallNextHandler controls the rest of the chain: True is the documented default and means the next registered handler is called, False means it is not. That is the difference from replacing LogBuilderClass: the hook sees the report being made and can decline to pass the call on, rather than changing the class for the whole application.

The unit's other routines

function DisasmFunction(const Address: Pointer; const AExceptionAddress: Pointer = nil;
  const ASize: Integer = 0; const ALines: Integer = 0;
  const AOptions: TEurekaModuleOptions = nil; const AEncrypt: Boolean = False;
  const AContext: PContext = nil): String;                                  // ELogBuilder
function FindLowAddress(const AExceptionAddress: Pointer;
  const ALines: Integer): Pointer;                                          // ELogBuilder

ELogBuilder also declares the two routines behind the disassembly view in a report. DisasmFunction returns a textual disassembly - comments, marks, addresses, raw bytes and decoded instructions - and is documented as the function that generates that view, using the EDisAsm unit. Address is where the disassembly starts, and despite the parameter's own description the documentation's note is that it does not have to be the beginning of a function - it can point at any CPU instruction. How far the disassembly goes comes from the other three: ASize is a size in bytes, and when it is set the disassembly stops only after that many bytes rather than at the first RET; AExceptionAddress marks the line at that address as "< Exception Here" and enlarges the range to include it; ALines caps the number of instructions disassembled after AExceptionAddress, and has no effect when that address is not given. With none of the three given - AExceptionAddress, ASize or ALines - the disassembly runs to the end of the allocated memory region or to the first RET. FindLowAddress is its auxiliary: given an address inside a function (and a number of instructions), it returns a pointer to the instruction exactly that many instructions before it, so the disassembly can be started there. The documentation recommends the same ALines value for both calls.