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
ELogOpenare not lost. They go into an internal log, and that internal log is copied into the real log file once you callELogOpen. If you do not want the older entries, callELogClearbeforeELogOpen- 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
ELogCurrencyandELogDateTimecover theCurrencyandTDateTimevalues a generic numeric overload would otherwise swallow, andELogPointerandELogWindowHandlecover a raw pointer and a window handle for the same documented reason.ELogWinErroris for system error codes;ALastErroris aTSysErrorCode, defaulting toTSysErrorCode(0)- the usual call isELogWinError('GetLastError', GetLastError);.ELogExceptiontakes an exception object (not necessarily one descending fromException) with an optional name for it asAMsg, for the documented patternELogException(E); HandleException(E);inside theexceptblock.ELogMemoryAsHexdumpsASizebytes of memory fromABufferinto 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.
Createtakes the exception information the report is about and keeps it (seeExceptionInfo). Creating the instance does not generate the report: it is created on demand.Reportgenerates and returns the bug report for that exception information; accessing it is what triggersBuildReport.ToStringreturns the same value - the documentation states that calling it is the same as accessingReport.BuildReportgenerates the actual report; it is the one member a custom builder must supply. It carries a caution: do not call it directly - callToStringor readReportinstead, because the class caches whatBuildReportproduces for performance reasons and a direct call bypasses that.FBuildedis that cache flag:TruemeansReportreturns the report already built,Falsemeans it generates a new one and caches it. Caching is automatic, andBuildReportdoes nothing special about it.RefreshsetsFBuildedtoFalseso 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.ExceptionInfois the object passed to the constructor. A note tells a sub-class how to read it: there areCallStackandOptionsproperties which should be used instead ofExceptionInfo.CallStackandExceptionInfo.Options- they default to those two, and they are the properties a sub-class overrides to customize report content without affecting the exception information.Optionsis the EurekaLog options object.CustomFieldsis a set of name-value pairs used as custom data in the report, corresponding to thesoCustomDataoption. Fill it right after creating the builder and before touchingReport- the tip is thatTStrings.Valuesmakes the pairs easy to set. Changing it does not refresh the report automatically:Refreshis 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.