EurekaLog
Menu

The EurekaLog Developer Guide

20. Solving bugs in your code

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:

  1. Identify the exact build and the operation the user was performing.
  2. Read the exception and its thread's stack; distinguish reliable entries from guesses.
  3. Match the addresses to that build's source, not today's source.
  4. Decide whether you are investigating a bad access, missing release, earlier corruption, or a thread that cannot make progress.
  5. 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:

  1. 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 begin line - 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.
  2. Routine name only - reason about which of the routine's statements could be on the stack: in a stack of DoSomething / DoWork / Run, only the DoSomething calls of DoWork can be meant, never the Prepare and Done calls around them.
  3. 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 $1000 if 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-relative Offset already includes the code-section offset; do not add $1000 twice. 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:

  1. Indexing outside an array, the archetype being for X := 1 to Length(List) do with List[X] in the body where for X := 0 to Length(List) - 1 do is 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.
  2. 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 where Stream.ReadBuffer(S1[0], 256) writes into it, provided at least 256 writable bytes - 64 elements of that array - are allocated; and FillChar(S2, Length(S2), 0) damages a string's pointer where FillChar(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 - and FillChar counts bytes while Length counts characters, so the count to pass in a Unicode build is Length(S2) * SizeOf(Char).
  3. 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 stdcall or cdecl.
  4. 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 CreateThread rather than BeginThread/TThread also bypasses the setup that establishes the RTL's IsMultiThread state.
  5. 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 GetProcAddress can yield nil; a cached non-nil procedure pointer can become invalid after FreeLibrary. Check both successful resolution and the lifetime of the module that owns the code. A non-nil pointer alone proves neither.
  6. 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. FreeAndNil clears 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:

  1. Find a checkpoint where the suspect block is valid and a later one where it is damaged.
  2. Move explicit CheckHeap calls or application Asserts between them until the interval is small. CheckHeap checks memory-manager invariants; your assertions must check your own data.
  3. 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.
  4. 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 Synchronize while 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 blocking SendMessage(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.