A user clicks Save. The report points at a string operation deep in the RTL. Another report has the same BugID. Neither fact tells you which line to fix.
Treat the report as a witness, not a verdict: it records where a problem was detected, which may be far from where it began. Your job is to connect that observation to a broken invariant in your own code. Work in this order:
- Identify the exact build and the operation the user was performing.
- Read the exception and its thread's stack; distinguish reliable entries from guesses.
- Match the addresses to that build's source, not today's source.
- Decide whether you are investigating a bad access, missing release, earlier corruption, or a thread that cannot make progress.
- Test a specific explanation. If the report lacks the decisive fact, arrange to record that fact on the next run.
Chapter 4 covers opening and organizing incoming reports. This chapter starts once you have one in front of you. If the report never arrives or EurekaLog itself fails, use chapter 5 instead.
20.1 What the report says before you read any code
The general section
Every report opens with single-valued facts, four of which are mandatory and cannot be turned off: the exception's address, type, message and BugID.
The address (2.2) is the address of the machine instruction that raised the exception, and - see the
CPU section below - the logical address rather than the real one. The type (2.5) is the exception
object's class name and the message (2.6) its Message property; those two often settle whether you
are looking at a crash or at an expected failure. The ID (2.7), BugID, is a hash-based grouping
identifier, not proof of an identical root cause. Reports with the same ID belong in the same first
triage bucket, but compare their builds, stacks, operations and surrounding state before merging
the diagnosis. Different defects can reach the same failure site; grouping also depends on the
selected BugID inputs and customization. Section 8.10 explains CustomID, which can separate
otherwise grouped exceptions.
The issue workflow uses this grouping to create or update tracker issues; it does not validate your
diagnosis. Count records repetitions with this BugID before sending, not all exceptions in the
process. Sent = 0 means the report was not sent. Status and Note are obsolete compatibility
fields from the old plain-text bug database; use the actual tracker for current issue state
(chapter 4).
Start with the application build, then read Steps to reproduce and Active Controls: do they
describe the operation you think failed? The user, computer, operating system and network fields
help test environmental hypotheses; do not infer a cause merely because a machine differs from
yours. In a DLL project the Application block describes
the host process, not your DLL, and Custom information holds what your code supplied through the
OnCustomDataRequest handler, which still has to be enabled in the options. A report about a hang
or a leak carries dummy values in the Exception block. The Modules section's handle column turns a
module-relative code offset into an absolute address, which section 20.4 needs; the module and process lists
are automatically disabled for leak reports. A report may also carry files: static ones from the
options, files your code adds inside the packed report through OnZippedFilesRequest, files
attached beside it through OnAttachedFilesRequest, and a screenshot named Screenshot.png or
BugReport.png, which the viewer displays. In a screenshot the active control or window is outlined
in red, but the captured window may be covered by other applications - especially in the "active
window only" mode, which can capture a window belonging to another process - and areas outside all
monitors are not included.
Assembler and CPU
The Assembler section is a disassembly of the machine code around the exception, disabled for leaks
and holding nothing that is not already inside your executable - the documentation's words are that
it saves you the time of disassembling your own exe or DLL. It marks the exception line once with
<-- EXCEPTION - the listing spans about fifteen instructions in each direction, or stops at the
start or the end of the routine - and comment blocks above groups of instructions give routine name,
line number and byte offset, each only when it changed, so instructions are grouped by source line,
while a name or a line appears only where debug information is available for that address. A comment
that reads data as characters is often a false positive, since EurekaLog only checks that the data
is readable and zero-terminated. Two caveats are documented: everything below the exception address
is exact, but the listing above it may show wrong values, because x86 does not permit a reliable
backwards walk; and the disassembler covers Intel/AMD opcodes only up to SSE 4.1.
The CPU section is the processor's state at the moment of the exception: registers, a stack dump,
and a dump of the memory at the exception address, usually the code section the Assembler section
renders readably; capturing it requires low-level hooks. Its two most important values, EXP and
STK, exist because hardware and software exceptions differ: a hardware exception - access
violation, divide by zero - is raised by the failing instruction, so its address points straight at
your line, while a software exception is always raised from Kernel32.RaiseException, which
makes its real address identical for every raise in your program and useless. EurekaLog therefore
reports a logical exception address (EXP) pointing at the raise construction, and STK -
the address the stack dump starts from, normally ESP/RSP - is adjusted to exclude the service
routines; the address in the General section is that logical one, while EIP/RIP in the registers
block is the instruction that actually raised the exception.
20.2 Reading a call stack
Suppose the exception thread contains SaveDocument, a serializer, and finally an RTL string
routine. Read upwards to understand how Save reached the failing instruction; read downwards to
find the nearest caller whose assumptions you own. Do not stop at the first familiar unit name.
The same facts, in every format
A stack entry can contain code address, stack address, module, code offset, unit, class, routine, line number and line offset. In EurekaLog's raw-text format, one record occupies one line:
|7FFFFFFE|04|00000000|00615CD8|Project44.exe|00215CD8|Unit1.pas|Unit1|TMainForm|Button1Click|31[20]|
The code address is in the Address column, the stack address in Stack, the module in Module,
and the code offset in Offset - offset from module start, so it includes the $1000 of the code
section. Line information is packed: 31[20] is line 31, the bracketed number being the line
offset - lines from the routine's first line that generates code, not a position inside the line
(the viewer splits the two into Line and Rel.line). Other tools rearrange the same facts: Delphi
prefixes the code address with : only on lines without debug information and carries no line
number; FastMM brackets file, unit, qualified routine and line; JCL excludes the $1000 that
EurekaLog's Offset includes; madExcept is space-delimited. A Source column names the file the
line is really in, which is how an include file becomes visible: a location inside an .inc
produces two entries for the same address, one naming the .inc file and its line and one naming
the unit and the line of the {$I} that pulled it in (line 2 of code.inc, included at line 55 of
Unit1.pas). The master line cannot always be determined exactly, since it varies with the include
file's contents and the linker version; a generic method is listed with the instantiated type
substituted into the class name and the unit that declared the generic class in braces - Source
names the file of the declaration, Unit the unit whose code called it, as in
{Unit3}TMyClass<System.Integer> - so a bug that hits one instantiation is recognizable as such.
Direction, and what the ends of a stack are made of
One report file may hold several reports, and one report several stacks, so pick the one you are reading before interpreting anything. Read a stack from the bottom up for execution order - first-called routine at the bottom, current location at the top - or top-down to unwind from the error outwards. The bottom is usually system, RTL or framework code with only partial information, and when the stack has a depth or size limit it is this bottom part that is cut. The top may contain RTL helpers besides the error location and your code, and some tools insert one extra line at the very top for the error location, so that when RTL helpers come first the address appears twice.
Threads in the stack
By default an exception report contains a single stack, for the thread that raised the exception: the exception thread; leak reports are the exception, since they carry one stack per leak found
- the number is a leak-report setting, not a property of the threads. Other threads appear only when the options that collect stacks of RTL or Windows threads are enabled, and the documentation attaches a warning to that: obtaining another thread's stack requires suspending it, and a thread suspended inside a memory allocation can block the application forever.
|*Exception Thread: ID=2940; Parent=3696; Priority=0 |
|Class=TMyThread; Name= (Unit1.TMyThread.Execute) |
|DeadLock=0; Wait Chain= |
The exception thread raised the exception - exactly one, listed first. The calling thread is
its parent, listed next, and is absent if it terminated already or the option is off; everything
else, including the main thread, is a running thread. EurekaLog separates RTL threads
(TThread/TThreadEx, BeginThread/BeginThreadEx) from Windows threads (everything else,
typically CreateThread), and the header shows which it is: a TThread/TThreadEx thread yields
its class, while a BeginThread(Ex) or CreateThread thread yields its thread function in
parentheses, always Execute for a TThread. Since several threads can share a class and its
arguments, a name tells them apart: TThreadEx.Create and BeginThreadEx (both declared in
EBase) take one as an argument, and any other thread can name itself with NameThread
(EThreadsManager) as its first action; the main thread is always MAIN. The last entry of a
TThread's stack, normally below BaseThreadInitThunk, is the creator: a single entry naming
the code that called the constructor - only that immediate caller, not the creator's stack.
When an exception is re-raised into another thread - TThread.Synchronize is the common case -
EurekaLog merges the stacks of the main thread and the invoking thread where possible, and the
documented example of such a report holds three stacks for two threads: the exception stack, the
invoking thread's stack, and the main thread's ordinary stack. In it the stack labelled "Exception
thread" is the main thread - a past state of it, the state in which it ran the synchronized task -
although the exception was actually re-raised by the Synchronize wrapper in the background thread.
Manual synchronization such as SendMessage into the main thread is not merged.
20.3 Entries that are not really there
On x86-32 there is no reliable way to build an exact call stack: every method uses assumptions, heuristics and guessing, so any of them can produce a false-positive entry or miss a real one.
Frame-based tracing walks the chain of stack frames, each frame linking to the previous one; it looks only for registered calls instead of scanning the stack, so it is precise and fast. It stops working when frame pointers are omitted (FPO, x86-32 only) or the stack is used in a non-standard way, and the "Stack Frames" option, which forces a frame even where none is needed, raises its usefulness. A frame also appears when optimisation is off or the routine uses an auto-managed type (strings, dynamic arrays, variants), and the RTL and VCL can be recompiled with the option turned on. The important platform distinction is the unwinding information, not a guarantee that every routine uses an EBP/RBP chain: x86-64 uses compiler-generated unwind metadata stored in the executable, and the Delphi "Stack Frames" option has no effect there. A RAW fallback is documented for software exceptions in LLVM/Clang builds. Treat that as a compiler-specific limitation, not a promise covering every later compiler release.
RAW tracing scans the whole stack guessing which integers look like return addresses, and there
is no certain way to identify them, so it is a heuristic method. The classic noise source is stack
trash: a routine with a large uninitialized local buffer, such as Buffer: array [0..4095] of Char;, reserves four kilobytes - eight in a Unicode build, where Char is two bytes wide - that
still hold the return addresses of calls already completed; zeroing such buffers with
FillChar(Buffer, SizeOf(Buffer), 0) and enabling the "Wipe stack on idle" option reduce it. The
documented demonstration is a call tree that has already finished: Test calls P1, which calls
Internal, and then P2, which raises - the stale return addresses of P1 and Internal are
still on the stack, so RAW lists Internal although it never participated. EurekaLog tries to
remove such entries by analysing assembly code and other heuristics, but it is not possible to
remove false-positive entries with 100% efficiency: when using RAW tracing you always have to
analyse the call stack for false-positive entries.
The viewer helps you assess the evidence:
| Entry | What it tells you | What to do |
|---|---|---|
| Red | Exception location, not a return address. | Inspect the failing instruction and its operands. |
| Black | Obtained through frame-based unwinding; normally stronger evidence. | Follow the caller relationship, still checking build and symbols. |
| Gray | Candidate return address without a supporting frame. | Verify that the caller could actually call the next routine. |
Ordinary x86-64 frame-based entries are black. Leak stacks use one colour for the tracing method as a whole, because per-entry frame information is not retained with the allocation: black for frame-based tracing, gray for RAW. Colour is not proof that symbols match or memory is intact.
A missing frame can also subtract one from every line offset in an x86-32 routine: an offset
counts from the first line that generates code, and an unframed begin generates none. With RTL/VCL
units built without frames, frame-based tracing may omit short routines. A frame identifies the
caller rather than its own routine, so a leak through TStringList.Create may lose the
Button1Click caller you expected to see. Standard memory routines such as AllocMem receive
special handling to recover their caller.
20.4 From the report to the source line
With the error dialog or the viewer and a running IDE, double-clicking a stack line jumps to the
location - provided the compiled application matches the sources you have (open the target unit in
the IDE, use the editor's __history folder, which EurekaLog can use, or restore the matching
revision), the IDE is running and able to find the files, and the IDE and the reporting application -
error dialog or viewer - sit under the same security boundary, that is the same user account and the
same elevation level under UAC. Without a GUI, the fallbacks are:
- Line number - use the IDE's "go to line number". If no copy of the unit matches the build,
use the line offset: find the routine and move down that many lines from the line the offset
counts from, the routine's
beginline - the first one that generates code, marked by a blue dot in the editor's gutter - remembering that a missing stack frame shifts every offset in the routine by one. - Routine name only - reason about which of the routine's statements could be on the stack:
in a stack of
DoSomething/DoWork/Run, only theDoSomethingcalls ofDoWorkcan be meant, never thePrepareandDonecalls around them. - Code address, or module plus offset - run the project from the IDE, pause it and use "go
to address", prefixing a hexadecimal address with
$, which shows source if debug information is available. With a module and an offset, take the module's base address from the Modules window and add the offset, including$1000if it applies; the offset form is the better of the two, because an absolute address from a client's machine means nothing when that machine loaded the module elsewhere. EurekaLog's module-relativeOffsetalready includes the code-section offset; do not add$1000twice. The extra adjustment is for formats, such as the JCL convention described above, that omit it.
20.5 When the top line is not the culprit
At the first line of your code, write down the condition that should have been true: the object was alive, the index was inside the array, the lock was released, or the buffer held enough bytes. Then ask what evidence would disprove it.
| Report | Recorded location | Question that finds the defect |
|---|---|---|
| Ordinary exception / access violation | Raise site or failed instruction. | Which input or lifetime assumption failed here? |
| Leak | Allocation stack of a block still retained at the check. | Who owns it, and when should that owner release it? |
| Memory corruption | Checkpoint that discovered damaged state. | What wrote this memory between its last good state and this check? |
| Hang / deadlock | Thread state when lack of progress was detected. | What must happen for it to resume, and who can make that happen? |
Memory corruption is especially deceptive: checking the entire memory pool at every allocation would be prohibitively expensive, so checks examine relevant blocks at checkpoints. A later innocent allocation can expose an earlier overwrite. Leaks are deceptive in a different way: their allocation stack cannot show a release that never happened.
Also distinguish a defect from a deliberately expected exception. EurekaLog treats an exception
as expected when its expected-exception context ID is nonzero or its expected-exception URL is
nonempty. These TEurekaExceptionInfo properties are separate from BugID. Expected processing
shows the message but suppresses the report, sending, restart and technical details. A meaningful
help context or URL adds a Help button; -1 marks an expected exception without requesting a help
destination. It does not mean that ordinary exceptions are expected by default.
Use CPU registers and memory dumps to test the invariant. With assembler knowledge you may recover an operand's value; even without it, the contents of a leaked block can identify what retained it.
20.6 Access violations
An access violation means an instruction attempted a memory access that was not permitted. Common causes are a nil pointer, a stale object, an out-of-bounds index or execution through a bad code pointer. The reverse is not true: accessing a freed block may appear to work while its pages remain accessible. A typical message is:
Access violation at address XXX in module 'YYY'. Write/read of address ZZZ
Find the instruction at XXX, then work out how it obtained the address ZZZ. Check these patterns:
- Indexing outside an array, the archetype being
for X := 1 to Length(List) dowithList[X]in the body wherefor X := 0 to Length(List) - 1 dois meant. If the failing line contains[], validate what is inside it; such errors should be caught in development by the "Range Check Errors" option, and they can destroy the stack so thoroughly that the error's own location is lost. - Messing with arguments and buffers - untyped parameters and overflows. On the documented
S1: array of Integer,Stream.ReadBuffer(S1, 256)corrupts the array's pointer whereStream.ReadBuffer(S1[0], 256)writes into it, provided at least 256 writable bytes - 64 elements of that array - are allocated; andFillChar(S2, Length(S2), 0)damages a string's pointer whereFillChar(Pointer(S2)^, Length(S2), 0)does not - as long as the string is nonempty and uniquely owned, since a write through the pointer bypasses copy-on-write - andFillCharcounts bytes whileLengthcounts characters, so the count to pass in a Unicode build isLength(S2) * SizeOf(Char). - The same applies to passing data between modules - typically strings, ignoring the two
memory managers of an exe and a DLL - and to declaring an imported DLL function wrongly, most
often with a missing
stdcallorcdecl. - Missing synchronization. VCL objects generally belong to the main thread; marshal UI work
to it with the appropriate synchronization mechanism. Protect shared non-UI state too: one
thread changing data while another uses it can invalidate otherwise correct pointers. On the
affected RTLs, using raw
CreateThreadrather thanBeginThread/TThreadalso bypasses the setup that establishes the RTL'sIsMultiThreadstate. - Calling through an invalid procedural variable - load a library in one place, resolve a
routine in another without checking, call a nil pointer, and later call a pointer whose DLL has
been unloaded:
An unchecked
GetProcAddresscan yield nil; a cached non-nil procedure pointer can become invalid afterFreeLibrary. Check both successful resolution and the lifetime of the module that owns the code. A non-nil pointer alone proves neither. - Using an object that was never created or was already freed. Destroying an object does not
clear the variable, locals of unmanaged types are not zeroed, and several variables can point at
the same object.
FreeAndNilclears the particular reference passed to it, not every alias. Interfaces and other ownership wrappers help only when their lifetime model is used consistently; neither technique repairs stale aliases or unsynchronized access.
Local arrays are worse than objects, because they sit on the stack with live memory at their
borders: the documented example indexes a two-element local array with a wrong value, S[I] := 0
with I = 2, and overwrites the return address of the routine, so the access violation appears when
the routine returns. Depending on the index the same mistake yields a correct application, one with
wrong results, an exception, or a full crash, and with a sufficiently negative index the write
reaches the exception handler frame installed by try; raising anything then ends in a full crash
without a message, because the exception manager detects the damaged stack and terminates the
process immediately.
Reading the message without overclaiming. XXX is the code address. Resolve it against the
loaded module ranges. A missing module or an address outside executable code suggests a bad call
target or damaged stack, but is not conclusive. Older Win32 examples use $400000–$7FFFFFFF as a
sanity range; do not apply that range to ASLR, large-address-aware images or Win64.
write or read tells you which memory access failed, but a single source statement may compile
to several accesses: inspect the instruction rather than assuming that the left or right side of
the Delphi statement is guilty. Execution through an invalid function pointer can also appear as
an access violation; older messages may describe it as a read.
A small ZZZ, such as $00000000 or $00000010, often means a nil base plus a field offset.
A larger value may be uninitialized, freed or overwritten; the address alone cannot choose among
them. Recognizable fill patterns such as $DEADBEEF, $FEEEFEEE or $CCCCCCCC are clues from
particular allocators/debugging environments, not universal proof of a previous valid allocation.
If XXX and ZZZ coincide, inspect the caller for an invalid function or virtual-method target.
Do not assume the second displayed stack row is necessarily that caller: helpers and RAW false
positives may intervene. Memory debugging may supply a more specific deleted-object diagnosis.
20.7 Leaks
EurekaLog calls a leak in the Delphi memory manager - GetMem, AllocMem and the rest - a memory
leak, and everything else a resource leak: direct VirtualAlloc, HeapAlloc, kernel handles
from CreateFile/CreateEvent/CreateThread, and GDI/USER handles, of which each process has a
quota of ten thousand by default. Memory/resource leak checking has configuration-dependent
performance and memory costs; it is not the same as merely installing the memory filter. Chapter 22
covers the options and chapter 5 explains how to measure their cost. A leak report groups the
leaks found and omits modules, processes, assembler and CPU sections. Exception event handlers are
not a substitute for the separate leak event registration.
A leak report does not point at the missing release. Start at the allocation stack, identify the intended owner, then follow its release path: destructor, scope exit, or explicit free. Check whether the owner lost its reference, skipped cleanup on an exception, or deliberately kept the object longer than the check expected. Shutdown ordering matters: some RTL or dependency cleanup can run after the detector (chapters 22 and 26).
Repeat the relevant scenario outside the debugger to separate debugger-related observations from application behaviour, but preserve the leak-check settings: a debugger-only activation option can otherwise make the report disappear by turning detection off. Known RTL/VCL leaks can be registered as ignored once their origin and lifetime are understood. Registration hides a report; it does not release memory or demonstrate that all remaining allocations are legitimate.
Naming what leaks. A block allocated through the memory manager while leak checking is active
can appear as $06961F90 x 1 DATA [Data at $06961F90] 1024 bytes, followed by its allocation stack.
Use MemLeaksName from EMemLeaks to label an otherwise anonymous block. This complete routine
belongs in a unit that includes EMemLeaks; it demonstrates naming without deliberately leaking:
procedure ExerciseNamedBuffer;
var
P: Pointer;
begin
GetMem(P, 1024);
try
MemLeaksName(P, 'Document workspace');
FillChar(P^, 1024, 0);
finally
FreeMem(P);
end;
end;
The annotation would identify the block as Document workspace if it appeared in an allocation
dump or leak report while still allocated. This routine releases it normally; naming does not
change ownership or create a report by itself.
Any dynamically allocated type can be passed as a pointer except strings and dynamic arrays, whose
negative-offset headers force the separate MemLeaksNameStr and MemLeaksNameDynArray calls.
Ownership is declared with MemLeaksOwn(Pointer(fmDialog), Pointer(Stream)): the child disappears
from reports and its size is added to the parent, which is what the report uses when merging blocks
during leak checking or dumping (CheckHeap, DumpAllocationsToFile); MemLeaksOwn(nil, Pointer(Stream)) gives ownership back, a third argument names the child, and MemLeaksOwnStr and
MemLeaksOwnDynArrayChild are the variants for strings and dynamic arrays. These annotations
describe reporting relationships, not destruction. The practices the documentation lists for limiting
leaks: managed types with reference counting and wrappers for what is not managed, awareness of
circular references, one exit path per function, try/finally for finalization, care with untyped
RTL routines, and named blocks.
20.8 Memory corruption
A report about memory corruption says that a problem occurred somewhere and some time ago, because checks can only run at checkpoints and only special data can be validated automatically (the memory manager's own headers); there is usually no useful reference to the offending code, since the culprit can be far away in space and time. Reproduce the failure, then narrow the interval:
- Find a checkpoint where the suspect block is valid and a later one where it is damaged.
- Move explicit
CheckHeapcalls or applicationAsserts between them until the interval is small.CheckHeapchecks memory-manager invariants; your assertions must check your own data. - If the address is reproducible, stop while it is allocated and intact, set a debugger data breakpoint on the damaged location, and resume. Inspect the write that triggers it.
- If necessary, use a dedicated developer build with range checking, disabled inlining and optimization, or a full debugging memory manager such as FastMM full-debug mode or SafeMM. Configure one compatible memory-manager arrangement rather than stacking incompatible ones.
Heap instrumentation does not watch local arrays on the stack. Temporarily moving a suspect buffer to checked heap storage can make an overwrite observable, but changes timing and layout; it is an experiment, not a fix. Synchronization errors need the same caution. When reproduction is elusive, review untyped buffer operations, byte/character counts, module boundaries and ownership, and add logging around the last known-good boundary. A disappearing crash after a layout change is not evidence that the corrupting write is gone.
20.9 Hangs and deadlocks
A frozen window and a deadlock are not synonyms. The UI thread may be calculating, waiting for I/O, waiting for a lock, or waiting for work that must itself run on the UI thread.
Windows detects a window that does not service messages for roughly five seconds. EurekaLog's GUI
hang monitor checks main-thread responsiveness against its configured threshold; the documented
default is 60 seconds. Its description names WM_NULL, but historical support explanations disagree
on the probe mechanism. Do not use the exact message as an application contract or a workaround
(chapter 28). This automatic GUI
check is not a general progress detector for all background threads. An application-specific
watchdog can call RaiseFreezeException when it detects its own failure-to-progress condition.
On Vista and later, Wait Chain Traversal can help identify deadlocks; livelocks are not detected.
Hang/deadlock reporting uses a specially constructed exception and the ordinary reporting path.
Start with the main-thread stack, then inspect the thread or external operation it waits for:
- Busy work: move unbounded calculation, file I/O and network calls off the UI thread. Provide progress and cancellation rather than merely increasing the detection timeout.
- Circular wait: draw the ownership chain. A worker waiting for
Synchronizewhile the UI waits for that worker is as important as two critical sections acquired in opposite orders. - Unbounded external call: a DLL, COM server or another window can delay you indefinitely.
Prefer asynchronous messages (
PostMessage,SendNotifyMessage,SendMessageCallback) where their semantics fit. Never use blockingSendMessage(HWND_BROADCAST)as a responsiveness strategy. - Missing cleanup: audit lock release on every exception path; use
try/finally. An error handler that acquires more locks can make the original problem impossible to report.
Keep a consistent lock order, protect data rather than whole functions, and include implicit locks:
DllMain runs under the loader lock, so keep it to simple initialization and do not call
LoadLibraryEx or CoCreateInstance there. TerminateThread is not recovery: it can strand locks
and resources. Message-aware waiting (MsgWaitForMultipleObjects) may be appropriate when a UI
thread must wait, but it is not permission to ignore reentrancy.
A useful result is a concrete dependency that cannot complete, or an operation that exceeds the responsiveness budget. “The monitor fired” proves neither a deadlock nor a defect in the monitor.
20.10 Narrowing with a log
Return to the Save failure. If the stack shows where a stale object was used but not who released it, log the creation, hand-off and destruction of that object, with a stable operation identifier. Record the few facts that distinguish competing explanations, not every private document or buffer.
Your application's execution log
ELogging writes a mostly textual, CodeSite-compatible .csl file, readable in an advanced text
editor or CodeSite File Viewer. When a report is packed for sending, this log can travel with it.
This is basic local logging, not CodeSite's dispatcher, live viewer or network transport.
Your calls to ELog record application activity. Internal EurekaLog diagnostics are a separate
opt-in mode, described in chapter 5; opening a log alone does not enable them.
For a permanent file, make ELogOpen your first logging action. The following complete helper unit
opens one file in the running account's existing %APPDATA% directory. Add it to your diagnostic
build so it initializes before your application's first logging call; the account must be able to
write there. Archive the file before the next run if you need to preserve it. If you want the log
only as a report attachment, omit this helper: the default temporary file is deleted at exit.
unit ApplicationDiagnosticLog;
interface
implementation
uses
SysUtils, ESysInfo, ELogging;
var
LogFileName: string;
initialization
LogFileName := IncludeTrailingPathDelimiter(ExpandEnvVars('%APPDATA%')) +
ChangeFileExt(ExtractFileName(ParamStr(0)), '.csl');
ELogOpen(LogFileName);
end.
Entries are written with the ELog overloads - ELog('Say Hello'), ELog('I', I) - while
ELogMemoryAsHex('Stream', Stream.Memory, Stream.Size) dumps a buffer, and a chain shapes an entry
with a category and colours, as in ELog.Category('My Category').Color(clBlack).FontColor(clWhite).Log('Say Hello'). ELogEnter logs entering and
leaving a routine and indents the entries inside it, taking their parameters as in L := ELogEnter(Self, 'Test').P('AMsg', AMsg).P('AValue', AValue).D; - where L is an
IEurekaLoggerParams - and L.Ret(Result) records the returned value. D (an expanded entry) or
C (a collapsed one) must close the chain, or nested indentation is wrong; EDebugInfo supplies
__MODULE__, __UNIT__, __FILE__, __FUNCTION__ and __LINE__ for the text.
Third-party frameworks, in both directions
CodeSite, SmartInspect and TMS Logging are the frameworks the documentation points at when you have
them. One direction puts their log into your report: a handler such as the documented
PackLogFile(const ACustom: Pointer; AExceptionInfo: TEurekaExceptionInfo; const ATempFolder: String; AAttachedFiles: TStrings; var ACallNextHandler: Boolean), registered with
RegisterEventZippedFilesRequest, copies the log into ATempFolder as the report is packed and
adds the copy's name to AAttachedFiles; SmartInspect instead enables its own logging (Si.Enabled := True) and exports its in-memory log, through Si.Connections := 'mem()' and Si.Dispatch('mem', 0, LStream). The other direction puts the report into their log: a handler registered with
RegisterEventExceptionNotify (EEvents; the type itself comes from EException) receives const ACustom: Pointer; AExceptionInfo: TEurekaExceptionInfo; var AHandle: Boolean; var ACallNextHandler: Boolean and logs the exception's class, message and call stack from it with the framework's own
calls - CodeSite.SendException, SiMain.LogException, TMSLogger.Error and their siblings. TMS
Logging has a ready-made bridge - the TMSLoggingEurekaLogOutputHandler unit from the
installation's \Source\Extras\, plus
TMSLogger.RegisterOutputHandlerClass(TEurekaLogOutputHandler) - which is recommended over copying
its text output, because the formalized format can be manipulated client-side in the viewer; calling
ELogOpen first decides where that log goes. Three warnings: such a file helps only if sending
is configured, since it goes inside the report sent to you while a local report stores no additional
files; the exception object may be unavailable even for Delphi exceptions (the documented case is an
inner exception already deleted when an outer one was raised), so check ExceptionNative, nil and
InheritsFrom(Exception) before casting it and prefer TEurekaExceptionInfo's ExceptionClass and
ExceptionMessage; and these handlers serve exceptions - leaks have their own registration -
while leak checks run during shutdown, when parts of the RTL and your application may already be
finalized. Do not assume arbitrary SysUtils or application services remain usable, and do not
raise exceptions from these callbacks.