/// logging functions used by Synopse projects
// - this unit is a part of the freeware Synopse mORMot framework,
// licensed under a MPL/GPL/LGPL tri-license; version 1.18
unit SynLog;

(*
    This file is part of Synopse framework.

    Synopse framework. Copyright (C) 2022 Arnaud Bouchez
      Synopse Informatique - https://synopse.info

  *** BEGIN LICENSE BLOCK *****
  Version: MPL 1.1/GPL 2.0/LGPL 2.1

  The contents of this file are subject to the Mozilla Public License Version
  1.1 (the "License"); you may not use this file except in compliance with
  the License. You may obtain a copy of the License at
  http://www.mozilla.org/MPL

  Software distributed under the License is distributed on an "AS IS" basis,
  WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  for the specific language governing rights and limitations under the License.

  The Original Code is Synopse framework.

  The Initial Developer of the Original Code is Arnaud Bouchez.

  Portions created by the Initial Developer are Copyright (C) 2022
  the Initial Developer. All Rights Reserved.

  Contributor(s):

  Alternatively, the contents of this file may be used under the terms of
  either the GNU General Public License Version 2 or later (the "GPL"), or
  the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  in which case the provisions of the GPL or the LGPL are applicable instead
  of those above. If you wish to allow use of your version of this file only
  under the terms of either the GPL or the LGPL, and not to allow others to
  use your version of this file under the terms of the MPL, indicate your
  decision by deleting the provisions above and replace them with the notice
  and other provisions required by the GPL or the LGPL. If you do not delete
  the provisions above, a recipient may use your version of this file under
  the terms of any one of the MPL, the GPL or the LGPL.

  ***** END LICENSE BLOCK *****

*)


{$I Synopse.inc} // define HASINLINE CPU32 CPU64 OWNNORMTOUPPER

interface

uses
{$ifdef MSWINDOWS}
  Windows,
  Messages,
{$endif}
{$ifdef KYLIX3}
  Types,
  LibC,
  SynKylix,
{$endif}
  Classes,
{$ifndef LVCL}
  SyncObjs, // for TEvent
  Contnrs,  // for TObjectList
  {$ifdef HASINLINENOTX86}
  Types,
  {$endif}
{$endif}
{$ifndef NOVARIANTS}
  Variants,
{$endif}
  SysUtils,
  SynLZ, // needed e.g. for TSynMapFile .mab format
  SynCommons,
  SynTable;


{ ************ Logging classes and functions }

type
  /// a debugger symbol, as decoded by TSynMapFile from a .map file
  TSynMapSymbol = packed record
    /// symbol internal name
    Name: RawUTF8;
    /// starting offset of this symbol in the executable
    // - addresses are integer, since map be <0 in Kylix .map files
    Start: integer;
    /// end offset of this symbol in the executable
    // - addresses are integer, since map be <0 in Kylix .map files
    Stop: integer;
  end;
  PSynMapSymbol = ^TSynMapSymbol;
  /// a dynamic array of symbols, as decoded by TSynMapFile from a .map file
  TSynMapSymbolDynArray = array of TSynMapSymbol;

  /// a debugger unit, as decoded by TSynMapFile from a .map file
  TSynMapUnit = packed record
    /// Name, Start and Stop of this Unit
    Symbol: TSynMapSymbol;
    /// associated source file name
    FileName: RawUTF8;
    /// list of all mapped source code lines of this unit
    Line: TIntegerDynArray;
    /// start code address of each source code line
    Addr: TIntegerDynArray;
  end;
  /// a dynamic array of units, as decoded by TSynMapFile from a .map file
  TSynMapUnitDynArray = array of TSynMapUnit;

  {$M+}
  /// retrieve a .map file content, to be used e.g. with TSynLog to provide
  // additional debugging information
  // - original .map content can be saved as .mab file in a more optimized format
  TSynMapFile = class
  protected
    fMapFile: TFileName;
    fSymbol: TSynMapSymbolDynArray;
    fUnit: TSynMapUnitDynArray;
    fSymbols: TDynArray;
    fUnits: TDynArrayHashed;
    fSymCount, fUnitCount, fUnitSynLogIndex, fUnitSystemIndex: integer;
    fCodeOffset: PtrUInt;
    fHasDebugInfo: boolean;
  public
    /// get the available debugging information
    // - if aExeName is specified, will use it in its search for .map/.mab
    // - if aExeName is not specified, will use the currently running .exe/.dll
    // - it will first search for a .map matching the file name: if found,
    // will be read to retrieve all necessary debugging information - a .mab
    // file will be also created in the same directory (if MabCreate is TRUE)
    // - if .map is not not available, will search for the .mab file
    // - if no .mab is available, will search for a .mab appended to the .exe/.dll
    // - if nothing is available, will log as hexadecimal pointers, without
    // debugging information
    constructor Create(const aExeName: TFileName=''; MabCreate: boolean=true);
    /// save all debugging information in the .mab custom binary format
    // - if no file name is specified, it will be saved as ExeName.mab or DllName.mab
    // - this file content can be appended to the executable via SaveToExe method
    // - this function returns the created file name
    function SaveToFile(const aFileName: TFileName=''): TFileName;
    /// save all debugging informat in our custom binary format
    procedure SaveToStream(aStream: TStream);
    /// append all debugging information to an executable (or library)
    // - the executable name must be specified, because it's impossible to
    // write to the executable of a running process
    // - this method will work for .exe and for .dll (or .ocx)
    procedure SaveToExe(const aExeName: TFileName);
    /// save all debugging information as JSON content
    // - may be useful from debugging purposes
    procedure SaveToJson(W: TTextWriter); overload;
    /// save all debugging information as a JSON file
    // - may be useful from debugging purposes
    procedure SaveToJson(const aJsonFile: TFileName; aHumanReadable: Boolean=false); overload;
    /// add some debugging information about the supplied absolute memory address
    // - will create a global TSynMapFile instance for the current process, if
    // necessary
    // - if no debugging information is available (.map or .mab), will write
    // the raw address pointer as hexadecimal
    // - under FPC, currently calls BacktraceStrFunc() which may be very slow
    class procedure Log(W: TTextWriter; aAddressAbsolute: PtrUInt;
      AllowNotCodeAddr: boolean);
    /// compute the relative memory address from its absolute (pointer) value
    function AbsoluteToOffset(aAddressAbsolute: PtrUInt): integer;
    /// retrieve a symbol according to a relative code address
    // - use fast O(log n) binary search
    function FindSymbol(aAddressOffset: integer): integer;
    /// retrieve an unit and source line, according to a relative code address
    // - use fast O(log n) binary search
    function FindUnit(aAddressOffset: integer; out LineNumber: integer): integer; overload;
    /// retrieve an unit information, according to the unit name
    // - will search within Units array
    function FindUnit(const aUnitName: RawUTF8): integer; overload;
    /// return the symbol location according to the supplied absolute address
    // - i.e. unit name, symbol name and line number (if any), as plain text
    // - returns '' if no match found
    function FindLocation(aAddressAbsolute: PtrUInt): RawUTF8; overload;
    /// return the symbol location according to the supplied ESynException
    // - i.e. unit name, symbol name and line number (if any), as plain text
    // - under FPC, currently calls BacktraceStrFunc() which may be very slow
    class function FindLocation(exc: ESynException): RawUTF8; overload;
    /// return the low-level stack trace exception information into human-friendly text
    class function FindStackTrace(const Ctxt: TSynLogExceptionContext): TRawUTF8DynArray;
    /// returns the file name of
    // - if unitname = '', returns the main file name of the current executable
    class function FindFileName(const unitname: RawUTF8): TFileName;
    /// returns the global TSynMapFile instance associated with the current
    // executable
    class function FromCurrentExecutable: TSynMapFile;
    /// all symbols associated to the executable
    property Symbols: TSynMapSymbolDynArray read fSymbol;
    /// all units, including line numbers, associated to the executable
    property Units: TSynMapUnitDynArray read fUnit;
  published
    /// the associated file name
    property FileName: TFileName read fMapFile;
    /// equals true if a .map or .mab debugging information has been loaded
    property HasDebugInfo: boolean read fHasDebugInfo;
  end;
  {$M-}

  /// an exception which wouldn't be logged and intercepted by this unit
  // - only this exact class will be recognized by TSynLog: inheriting it
  // will trigger the interception, as any other regular exception
  ESynLogSilent = class(ESynException);

  {$M+} { we need the RTTI for the published methods of the logging classes }

  TSynLog = class;

  /// class-reference type (metaclass) of a TSynLog family
  // - since TSynLog classes store their information per type, you usually
  // will store a reference to a logging family (i.e. logging settings) using
  // a TSynLogClass variable, whereas TSynLog would point to the active logging
  // instance
  TSynLogClass = class of TSynLog;

  TSynLogFamily = class;
  TSynLogFile = class;

  {$M-}

  /// a generic interface used for logging a method
  // - you should create one TSynLog instance at the beginning of a block code
  // using TSynLog.Enter: the ISynLog will be released automaticaly by the
  // compiler at the end of the method block, marking it's executation end
  // - all logging expect UTF-8 encoded text, i.e. usualy English text
  ISynLog = interface(IUnknown)
    ['{527AC81F-BC41-4717-B089-3F74DE56F1AE}']
    /// call this method to add some information to the log at a specified level
    // - will use TTextWriter.Add(...,twOnSameLine) to append its content
    // - % = #37 indicates a string, integer, floating-point, class parameter
    // to be appended as text (e.g. class name), any variant as JSON...
    // - note that cardinal values should be type-casted to Int64() (otherwise
    // the integer mapped value will be transmitted, therefore wrongly)
    // - if Instance is set, it will log the corresponding class name and address
    // (to be used if you didn't call TSynLog.Enter() method first)
    procedure Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArgs: array of const;
      Instance: TObject=nil); overload;
    /// call this method to add some information to the log at a specified level
    // - if Instance is set and Text is not '', it will log the corresponding
    // class name and address (to be used e.g. if you didn't call TSynLog.Enter()
    // method first)
    // - if Instance is set and Text is '', will behave the same as
    // Log(Level,Instance), i.e. write the Instance as JSON content
    procedure Log(Level: TSynLogInfo; const Text: RawUTF8;
      Instance: TObject=nil; TextTruncateAtLength: integer=maxInt); overload;
    {$ifdef UNICODE}
    /// call this method to add some VCL string to the log at a specified level
    // - this overloaded version will avoid a call to StringToUTF8()
    procedure Log(Level: TSynLogInfo; const Text: string; Instance: TObject=nil); overload;
    {$endif}
    /// call this method to add the content of an object to the log at a
    // specified level
    // - TSynLog will write the class and hexa address - TSQLLog will write the
    // object JSON content
    procedure Log(Level: TSynLogInfo; Instance: TObject); overload;
    /// call this method to add the content of most low-level types to the log
    // at a specified level
    // - TSynLog will handle enumerations and dynamic array; TSQLLog will be
    // able to write TObject/TSQLRecord and sets content as JSON
    procedure Log(Level: TSynLogInfo; const aName: RawUTF8;
      aTypeInfo: pointer; const aValue; Instance: TObject); overload;
    /// call this method to add the caller address to the log at the specified level
    // - if the debugging info is available from TSynMapFile, will log the
    // unit name, associated symbol and source code line
    procedure Log(Level: TSynLogInfo=sllTrace); overload;
    /// call this method to add some multi-line information to the log at a
    // specified level
    // - LinesToLog content will be added, one line per one line, delimited
    // by #13#10 (CRLF)
    // - if a line starts with IgnoreWhenStartWith (already uppercase), it won't
    // be added to the log content (to be used e.g. with '--' for SQL statements)
    procedure LogLines(Level: TSynLogInfo; LinesToLog: PUTF8Char;
      aInstance: TObject=nil; const IgnoreWhenStartWith: PAnsiChar=nil);
    /// retrieve the associated logging instance
    function Instance: TSynLog;
  end;

  {$ifndef DELPHI5OROLDER}
  /// a mORMot-compatible calback definition
  // - used to notify a remote mORMot server via interface-based serivces
  // for any incoming event, using e.g. TSynLogCallbacks.Subscribe
  ISynLogCallback = interface(IInvokable)
    ['{9BC218CD-A7CD-47EC-9893-97B7392C37CF}']
    /// each line of the TTextWriter internal instance will trigger this method
    // - the format is similar to TOnTextWriterEcho, as defined in SynCommons
    // - an initial call with Level=sllNone and the whole previous Text may be
    // transmitted, if ReceiveExistingKB is set for TSynLogCallbacks.Subscribe()
    procedure Log(Level: TSynLogInfo; const Text: RawUTF8);
  end;

  /// store a subscribe to ISynLogCallback
  TSynLogCallback = record
    Levels: TSynLogInfos;
    Callback: ISynLogCallback;
  end;
  /// store the all subscribed ISynLogCallback
  TSynLogCallbackDynArray = array of TSynLogCallback;

  /// can manage a list of ISynLogCallback registrations
  TSynLogCallbacks = class(TSynPersistentLock)
  protected
    fCount: integer;
    fCurrentlyEchoing: boolean;
  public
    /// direct access to the registration storage
    Registration: TSynLogCallbackDynArray;
    /// high-level access to the registration storage
    Registrations: TDynArray;
    /// the TSynLog family actually associated with those callbacks
    TrackedLog: TSynLogFamily;
    /// initialize the registration storage for a given TSynLogFamily instance
    constructor Create(aTrackedLog: TSynLogFamily); reintroduce;
    /// finalize the registration storage for a given TSynLogFamily instance
    destructor Destroy; override;
    /// register a callback for a given set of log levels
    // - you can specify a number of KB of existing log content to send to the
    // monitoring tool, before the actual real-time process
    function Subscribe(const Levels: TSynLogInfos;
      const Callback: ISynLogCallback; ReceiveExistingKB: cardinal=0): integer; virtual;
    /// unregister a callback previously registered by Subscribe()
    procedure Unsubscribe(const Callback: ISynLogCallback); virtual;
    /// notify a given log event
    // - matches the TOnTextWriterEcho signature
    function OnEcho(Sender: TTextWriter; Level: TSynLogInfo;
      const Text: RawUTF8): boolean;
  published
    /// how many registrations are currently defined
    property Count: integer read fCount;
  end;
  {$endif}

  /// this event can be set for a TSynLogFamily to archive any deprecated log
  // into a custom compressed format
  // - will be called by TSynLogFamily when TSynLogFamily.Destroy identify
  // some outdated files
  // - the aOldLogFileName will contain the .log file with full path
  // - the aDestinationPath parameter will contain 'ArchivePath\log\YYYYMM\'
  // - should return true on success, false on error
  // - example of matching event handler are EventArchiveDelete/EventArchiveSynLZ
  // or EventArchiveZip in SynZip.pas
  // - this event handler will be called one time per .log file to archive,
  // then one last time with aOldLogFileName='' in order to close any pending
  // archive (used e.g. by EventArchiveZip to open the .zip only once)
  TSynLogArchiveEvent = function(const aOldLogFileName, aDestinationPath: TFileName): boolean;

  /// this event can be set for a TSynLogFamily to customize the file rotation
  // - will be called by TSynLog.PerformRotation
  // - should return TRUE if the function did process the file name
  // - should return FALSE if the function did not do anything, so that the
  // caller should perform the rotation as usual
  TSynLogRotateEvent = function(aLog: TSynLog; const aOldLogFileName: TFileName): boolean;

  /// how threading is handled by the TSynLogFamily
  // - proper threading expects the TSynLog.NotifyThreadEnded method to be called
  // when a thread is about to terminate, e.g. from TSQLRest.EndCurrentThread
  // - by default, ptMergedInOneFile will indicate that all threads are logged
  // in the same file, in occurence order
  // - if set to ptOneFilePerThread, it will create one .log file per thread
  // - if set to ptIdentifiedInOnFile, a new column will be added for each
  // log row, with the corresponding ThreadID - LogView tool will be able to
  // display per-thread logging, if needed - note that your application shall
  // use a thread pool (just like all mORMot servers classes do), otherwise
  // some random hash collision may occur if Thread IDs are not recycled enough
  // - if set to ptNoThreadProcess, no thread information is gathered, and all
  // Enter/Leave would be merged into a single call - but it may be mandatory
  // to use this option if TSynLog.NotifyThreadEnded is not called (e.g. from
  // legacy code), and that your process experiment instability issues
  TSynLogPerThreadMode = (
    ptMergedInOneFile, ptOneFilePerThread, ptIdentifiedInOnFile, ptNoThreadProcess);

  /// how stack trace shall be computed during logging
  TSynLogStackTraceUse = (stManualAndAPI,stOnlyAPI,stOnlyManual);

  /// how file existing shall be handled during logging
  TSynLogExistsAction = (acOverwrite, acAppend);

  /// callback signature used by TSynLogFamilly.OnBeforeException
  // - should return false to log the exception, or true to ignore it
  TSynLogOnBeforeException = function(const aExceptionContext: TSynLogExceptionContext;
    const aThreadName: RawUTF8): boolean of object;

  /// store simple log-related settings
  // - see also TDDDLogSettings in dddInfraSettings.pas and TSynDaemonSettings
  // in mORMotService.pas, which may be more integrated
  TSynLogSettings = class(TSynPersistent)
  protected
    fLevels: TSynLogInfos;
    fDestinationPath: TFileName;
    fRotateFileCount: integer;
    fLogClass: TSynLogClass;
  public
    /// set some default values
    constructor Create; override;
    /// define the log information into the supplied TSynLog class
    // - if you don't call this method, the logging won't be initiated
    procedure SetLog(aLogClass: TSynLogClass = nil);
    /// read-only access to the TSynLog class, if SetLog() has been called
    property LogClass: TSynLogClass read fLogClass;
  published
    /// the log levels to be used for the log file
    // - i.e. a combination of none or several logging event
    // - if "*" is serialized, unneeded sllNone won't be part of the set
    // - default is LOG_STACKTRACE
    property Levels: TSynLogInfos read fLevels write fLevels;
    /// allow to customize where the logs should be written
    // - default is the system log folder (e.g. /var/log on Linux)
    property DestinationPath: TFileName read fDestinationPath write fDestinationPath;
    /// how many files will be rotated (default is 2)
    property RotateFileCount: integer read fRotateFileCount write fRotateFileCount;
  end;

  /// regroup several logs under an unique family name
  // - you should usualy use one family per application or per architectural
  // module: e.g. a server application may want to log in separate files the
  // low-level Communication, the DB access, and the high-level process
  // - initialize the family settings before using them, like in this code:
  // ! with TSynLogDB.Family do begin
  // !   Level := LOG_VERBOSE;
  // !   PerThreadLog := ptOneFilePerThread;
  // !   DestinationPath := 'C:\Logs';
  // ! end;
  //- then use the logging system inside a method:
  // ! procedure TMyDB.MyMethod;
  // ! var log: ISynLog;
  // ! begin
  // !   log := TSynLogDB.Enter(self,'MyMethod');
  // !   // do some stuff
  // !   log.Log(sllInfo,'method run with no problem and value=%',[value]);
  // ! end; // here log will be released and method leaving will be logged
  TSynLogFamily = class
  protected
    fLevel, fLevelStackTrace: TSynLogInfos;
    fArchiveAfterDays: Integer;
    fArchivePath: TFileName;
    fOnArchive: TSynLogArchiveEvent;
    fOnRotate: TSynLogRotateEvent;
    fPerThreadLog: TSynLogPerThreadMode;
    fIncludeComputerNameInFileName: boolean;
    fCustomFileName: TFileName;
    fGlobalLog: TSynLog;
    fSynLogClass: TSynLogClass;
    fIdent: integer;
    fDestinationPath: TFileName;
    fDefaultExtension: TFileName;
    fBufferSize: integer;
    fHRTimestamp: boolean;
    fLocalTimestamp: boolean;
    fWithUnitName: boolean;
    fWithInstancePointer: boolean;
    fNoFile: boolean;
    fAutoFlush: cardinal;
    {$ifdef MSWINDOWS}
    fNoEnvironmentVariable: boolean;
    {$endif}
    {$ifndef NOEXCEPTIONINTERCEPT}
    fHandleExceptions: boolean;
    {$endif}
    fStackTraceLevel: byte;
    fStackTraceUse: TSynLogStackTraceUse;
    fFileExistsAction: TSynLogExistsAction;
    fExceptionIgnore: TList;
    fOnBeforeException: TSynLogOnBeforeException;
    fEchoToConsole: TSynLogInfos;
    fEchoToConsoleUseJournal: boolean;
    fEchoCustom: TOnTextWriterEcho;
    fEchoRemoteClient: TObject;
    fEchoRemoteClientOwned: boolean;
    fEchoRemoteEvent: TOnTextWriterEcho;
    fEndOfLineCRLF: boolean;
    fDestroying: boolean;
    fRotateFileCurrent: cardinal;
    fRotateFileCount: cardinal;
    fRotateFileSize: cardinal;
    fRotateFileAtHour: integer;
    fRotateFileNoCompression: boolean;
    function CreateSynLog: TSynLog;
    procedure StartAutoFlush;
    procedure SetDestinationPath(const value: TFileName);
    procedure SetLevel(aLevel: TSynLogInfos);
    procedure SynLogFileListEcho(const aEvent: TOnTextWriterEcho; aEventAdd: boolean);
    procedure SetEchoToConsole(aEnabled: TSynLogInfos);
    procedure SetEchoToConsoleUseJournal(aValue: boolean);
    procedure SetEchoCustom(const aEvent: TOnTextWriterEcho);
    function GetSynLogClassName: string;
    function GetExceptionIgnoreCurrentThread: boolean;
    procedure SetExceptionIgnoreCurrentThread(
      aExceptionIgnoreCurrentThread: boolean);
  public
    /// intialize for a TSynLog class family
    // - add it in the global SynLogFileFamily[] list
    constructor Create(aSynLog: TSynLogClass);
    /// release associated memory
    // - will archive older DestinationPath\*.log files, according to
    // ArchiveAfterDays value and ArchivePath
    destructor Destroy; override;

    /// retrieve the corresponding log file of this thread and family
    // - creates the TSynLog if not already existing for this current thread
    function SynLog: TSynLog;
    /// register one object and one echo callback for remote logging
    // - aClient is typically a mORMot's TSQLHttpClient or a TSynLogCallbacks
    // instance as defined in this unit
    // - if aClientOwnedByFamily is TRUE, its life time will be manage by this
    // TSynLogFamily: it will stay alive until this TSynLogFamily is destroyed,
    // or the EchoRemoteStop() method called
    // - aClientEvent should be able to send the log row to the remote server
    procedure EchoRemoteStart(aClient: TObject; const aClientEvent: TOnTextWriterEcho;
      aClientOwnedByFamily: boolean);
    /// stop echo remote logging
    // - will free the aClient instance supplied to EchoRemoteStart
    procedure EchoRemoteStop;
    /// can be used to retrieve up to a specified amount of KB of existing log
    // - expects a single file to be opened for this family
    // - will retrieve the log content for the current file, truncating the
    // text up to the specified number of KB (an up to 128 MB at most)
    function GetExistingLog(MaximumKB: cardinal): RawUTF8;
    /// callback to notify the current logger that its thread is finished
    // - method follows TNotifyThreadEvent signature, which can be assigned to
    // TSynBackgroundThreadAbstract.OnAfterExecute
    // - is called e.g. by TSQLRest.EndCurrentThread
    procedure OnThreadEnded(Sender: TThread);

    /// you can add some exceptions to be ignored to this list
    // - for instance, EConvertError may be added to the list, as such:
    // ! TSQLLog.Family.ExceptionIgnore.Add(EConvertError);
    // - you may also trigger ESynLogSilent exceptions for silent process
    // - see also ExceptionIgnoreCurrentThread property, if you want a per-thread
    // filtering of all exceptions
    property ExceptionIgnore: TList read fExceptionIgnore;
    /// allow to (temporarly) ignore exceptions in the current thread
    // - this property will affect all TSynLogFamily instances, for the
    // current thread
    // - may be used in a try...finally block e.g. when notifying the exception
    // to a third-party service, or during a particular process
    // - see also ExceptionIgnore property - which is also checked in addition
    // to this flag
    property ExceptionIgnoreCurrentThread: boolean
      read GetExceptionIgnoreCurrentThread write SetExceptionIgnoreCurrentThread;
    /// you can let exceptions be ignored from a callback
    // - if set and returns true, the given exception won't be logged
    // - execution of this event handler is protected via the logs global lock
    // - may be handy e.g. when working with code triggerring a lot of
    // exceptions (e.g. Indy), where ExceptionIgnore could be refined
    property OnBeforeException: TSynLogOnBeforeException
      read fOnBeforeException write fOnBeforeException;
    /// event called to archive the .log content after a defined delay
    // - Destroy will parse DestinationPath folder for *.log files matching
    // ArchiveAfterDays property value
    // - you can set this property to EventArchiveDelete in order to delete deprecated
    // files, or EventArchiveSynLZ to compress the .log file into our propertary
    // SynLZ format: resulting file name will be ArchivePath\log\YYYYMM\*.log.synlz
    // (use FileUnSynLZ function to uncompress it)
    // - if you use SynZip.EventArchiveZip, the log files will be archived in
    // ArchivePath\log\YYYYMM.zip
    // - the aDestinationPath parameter will contain 'ArchivePath\log\YYYYMM\'
    // - this event handler will be called one time per .log file to archive,
    // then one last time with aOldLogFileName='' in order to close any pending
    // archive (used e.g. by EventArchiveZip to open the .zip only once)
    property OnArchive: TSynLogArchiveEvent read fOnArchive write fOnArchive;
    /// event called to perform a custom file rotation
    // - will be checked by TSynLog.PerformRotation to customize the rotation
    // process and do not perform the default step, if the callback returns TRUE
    property OnRotate: TSynLogRotateEvent read fOnRotate write fOnRotate;
    /// if the some kind of events shall be echoed to the console
    // - note that it will slow down the logging process a lot (console output
    // is slow by nature under Windows, but may be convenient for interactive
    // debugging of services, for instance
    // - this property shall be set before any actual logging, otherwise it
    // will have no effect
    // - can be set e.g. to LOG_VERBOSE in order to echo every kind of events
    // - EchoCustom or EchoToConsole can be activated separately
    property EchoToConsole: TSynLogInfos read fEchoToConsole write SetEchoToConsole;
    /// For Linux with journald
    // - if true: redirect all EchoToConsole logging into journald service
    // - such logs can be exported into a format whichcan be viewed by our
    // LogView tool using a command (replacing UNIT with your unit name and
    // PROCESS with the executable name):
    // $ "journalctl -u UNIT --no-hostname -o short-iso-precise --since today | grep "PROCESS\[.*\]:  . " > todaysLog.log"
    property EchoToConsoleUseJournal: boolean read fEchoToConsoleUseJournal
      write SetEchoToConsoleUseJournal;
    /// can be set to a callback which will be called for each log line
    // - could be used with a third-party logging system
    // - EchoToConsole or EchoCustom can be activated separately
    // - you may even disable the integrated file output, via NoFile := true
    property EchoCustom: TOnTextWriterEcho read fEchoCustom write SetEchoCustom;
    /// the associated TSynLog class
    property SynLogClass: TSynLogClass read fSynLogClass;
  published
    /// the associated TSynLog class
    property SynLogClassName: string read GetSynLogClassName;
    /// index in global SynLogFileFamily[] and SynLogFileIndexThreadVar[] lists
    property Ident: integer read fIdent;
    /// the current level of logging information for this family
    // - can be set e.g. to LOG_VERBOSE in order to log every kind of events
    property Level: TSynLogInfos read fLevel write SetLevel;
    /// the levels which will include a stack trace of the caller
    // - by default, contains sllStackTrace,sllException,sllExceptionOS plus
    // sllError,sllFail,sllLastError,sllDDDError for Delphi only  - since FPC
    // BacktraceStrFunc() function is very slow
    // - exceptions will always trace the stack
    property LevelStackTrace: TSynLogInfos read fLevelStackTrace write fLevelStackTrace;
    /// the folder where the log must be stored
    // - by default, is in the executable folder
    property DestinationPath: TFileName read fDestinationPath write SetDestinationPath;
    /// the file extension to be used
    // - is '.log' by default
    property DefaultExtension: TFileName read fDefaultExtension write fDefaultExtension;
    /// if TRUE, the log file name will contain the Computer name - as '(MyComputer)'
    property IncludeComputerNameInFileName: boolean read fIncludeComputerNameInFileName write fIncludeComputerNameInFileName;
    /// can be used to customized the default file name
    // - by default, the log file name is computed from the executable name
    // (and the computer name if IncludeComputerNameInFileName is true)
    // - you can specify your own file name here, to be used instead
    // - this file name should not contain any folder, nor file extension (which
    // are set by DestinationPath and DefaultExtension properties)
    property CustomFileName: TFileName read fCustomFileName write fCustomFileName;
    /// the folder where old log files must be compressed
    // - by default, is in the executable folder, i.e. the same as DestinationPath
    // - the 'log\' sub folder name will always be appended to this value
    // - will then be used by OnArchive event handler to produce, with the
    // current file date year and month, the final path (e.g.
    // 'ArchivePath\Log\YYYYMM\*.log.synlz' or 'ArchivePath\Log\YYYYMM.zip')
    property ArchivePath: TFileName read fArchivePath write fArchivePath;
    /// number of days before OnArchive event will be called to compress
    // or delete deprecated files
    // - will be set by default to 7 days
    // - will be used by Destroy to call OnArchive event handler on time
    property ArchiveAfterDays: Integer read fArchiveAfterDays write fArchiveAfterDays;
    /// the internal in-memory buffer size, in bytes
    // - this is the number of bytes kept in memory before flushing to the hard
    // drive; you can call TSynLog.Flush method or set AutoFlushTimeOut to true
    // in order to force the writting to disk
    // - is set to 4096 by default (4 KB is the standard hard drive cluster size)
    property BufferSize: integer read fBufferSize write fBufferSize;
    /// define how thread will be identified during logging process
    // - by default, ptMergedInOneFile will indicate that all threads are logged
    // in the same file, in occurence order (so multi-thread process on server
    // side may be difficult to interpret)
    // - if RotateFileCount and RotateFileSizeKB/RotateFileDailyAtHour are set,
    // will be ignored (internal thread list shall be defined for one process)
    property PerThreadLog: TSynLogPerThreadMode read fPerThreadLog write fPerThreadLog;
    /// if TRUE, will log high-resolution time stamp instead of ISO 8601 date and time
    // - this is less human readable, but allows performance profiling of your
    // application on the customer side (using TSynLog.Enter methods)
    // - set to FALSE by default, or if RotateFileCount and RotateFileSizeKB /
    // RotateFileDailyAtHour are set (the high resolution frequency is set
    // in the log file header, so expects a single file)
    property HighResolutionTimestamp: boolean read fHRTimestamp write fHRTimestamp;
    /// by default, time logging will use error-safe UTC values as reference
    // - you may set this property to TRUE to store local time instead
    property LocalTimestamp: boolean read fLocalTimestamp write fLocalTimestamp;
    /// if TRUE, will log the unit name with an object instance if available
    // - unit name is available from RTTI if the class has published properties
    // - set to TRUE by default, for better debugging experience
    property WithUnitName: boolean read fWithUnitName write fWithUnitName;
    /// if TRUE, will log the pointer with an object instance class if available
    // - set to TRUE by default, for better debugging experience
    property WithInstancePointer: boolean read fWithInstancePointer write fWithInstancePointer;
    /// the time (in seconds) after which the log content must be written on
    // disk, whatever the current content size is
    // - by default, the log file will be written for every 4 KB of log (see
    // BufferSize property) - this will ensure that the main application won't
    // be slow down by logging
    // - in order not to loose any log, a background thread can be created
    // and will be responsible of flushing all pending log content every
    // period of time (e.g. every 10 seconds)
    property AutoFlushTimeOut: cardinal read fAutoFlush write fAutoFlush;
    {$ifdef MSWINDOWS}
    /// force no environment variables to be written to the log file
    // - may be usefull if they contain some sensitive information
    property NoEnvironmentVariable: boolean read fNoEnvironmentVariable write fNoEnvironmentVariable;
    {$endif}
    /// force no log to be written to any file
    // - may be usefull in conjunction e.g. with EchoToConsole or any other
    // third-party logging component
    property NoFile: boolean read fNoFile write fNoFile;
    /// auto-rotation of logging files
    // - set to 0 by default, meaning no rotation
    // - can be set to a number of rotating files: rotation and compression will
    // happen, and main file size will be up to RotateFileSizeKB number of bytes,
    // or when RotateFileDailyAtHour time is reached
    // - if set to 1, no .synlz backup will be created, so the main log file will
    // be restarted from scratch when it reaches RotateFileSizeKB size or when
    // RotateFileDailyAtHour time is reached
    // - if set to a number > 1, some rotated files will be compressed using the
    // SynLZ algorithm, and will be named e.g. as MainLogFileName.0.synlz ..
    // MainLogFileName.7.synlz for RotateFileCount=9 (total count = 9, including
    // 1 main log file and 8 .synlz files)
    property RotateFileCount: cardinal read fRotateFileCount write fRotateFileCount;
    /// maximum size of auto-rotated logging files, in kilo-bytes (per 1024 bytes)
    // - specify the maximum file size upon which .synlz rotation takes place
    // - is not used if RotateFileCount is left to its default 0
    property RotateFileSizeKB: cardinal read fRotateFileSize write fRotateFileSize;
    /// fixed hour of the day where logging files rotation should be performed
    // - by default, equals -1, meaning no rotation
    // - you can set a time value between 0 and 23 to force the rotation at this
    // specified hour
    // - is not used if RotateFileCount is left to its default 0
    property RotateFileDailyAtHour: integer read fRotateFileAtHour write fRotateFileAtHour;
    /// if set to TRUE, no #.synlz will be created at rotation but plain #.log file
    property RotateFileNoCompression: boolean read fRotateFileNoCompression write fRotateFileNoCompression;
    /// the recursive depth of stack trace symbol to write
    // - used only if exceptions are handled, or by sllStackTrace level
    // - default value is 30, maximum is 255
    // - if stOnlyAPI is defined as StackTraceUse under Windows XP, maximum
    // value may be around 60, due to RtlCaptureStackBackTrace() API limitations
    property StackTraceLevel: byte read fStackTraceLevel write fStackTraceLevel;
    /// how the stack trace shall use only the Windows API
    // - the class will use low-level RtlCaptureStackBackTrace() API to retrieve
    // the call stack: in some cases, it is not able to retrieve it, therefore
    // a manual walk of the stack can be processed - since this manual call can
    // trigger some unexpected access violations or return wrong positions,
    // you can disable this optional manual walk by setting it to stOnlyAPI
    // - default is stManualAndAPI, i.e. use RtlCaptureStackBackTrace() API and
    // perform a manual stack walk if the API returned no address (or <3); but
    // within the IDE, it will use stOnlyAPI, to ensure no annoyning AV occurs
    property StackTraceUse: TSynLogStackTraceUse read fStackTraceUse write fStackTraceUse;
    /// how existing log file shall be handled
    property FileExistsAction: TSynLogExistsAction read fFileExistsAction write fFileExistsAction;
    /// define how the logger will emit its line feed
    // - by default (FALSE), a single LF (#10) char will be written, to save
    // storage space
    // - you can set this property to TRUE, so that CR+LF (#13#10) chars will
    // be appended instead
    // - TSynLogFile class and our LogView tool will handle both patterns
    property EndOfLineCRLF: boolean read fEndOfLineCRLF write fEndOfLineCRLF;
  end;

  /// TSynLogThreadContext will define a dynamic array of such information
  // - used by TSynLog.Enter methods to handle recursivity calls tracing
  TSynLogThreadRecursion = record
    /// associated class instance to be displayed
    Instance: TObject;
    /// method name (or message) to be displayed
    // - may be a RawUTF8 if MethodNameLocal=mnEnterOwnMethodName
    MethodName: PUTF8Char;
    /// internal reference count used at this recursion level by TSynLog._AddRef
    RefCount: integer;
    /// the caller address, ready to display stack trace dump if needed
    Caller: PtrUInt;
    /// the time stamp at enter time
    EnterTimestamp: Int64;
    /// if the method name is local, i.e. shall not be displayed at Leave()
    MethodNameLocal: (mnAlways, mnEnter, mnLeave, mnEnterOwnMethodName);
  end;
  PSynLogThreadRecursion = ^TSynLogThreadRecursion;

  /// thread-specific internal context used during logging
  // - this structure is a hashed-per-thread variable
  TSynLogThreadContext = record
    /// the corresponding Thread ID
    ID: TThreadID;
    /// number of items stored in Recursion[]
    RecursionCount: integer;
    /// number of items available in Recursion[]
    // - faster than length(Recursion)
    RecursionCapacity: integer;
    /// used by TSynLog.Enter methods to handle recursivity calls tracing
    Recursion: array of TSynLogThreadRecursion;
    /// the associated thread name
    ThreadName: RawUTF8;
  end;
  // pointer to thread-specific context information
  PSynLogThreadContext = ^TSynLogThreadContext;

  /// file stream which ignores I/O write errors
  // - in case disk space is exhausted, TFileStreamWithoutWriteError.WriteBuffer
  // won't throw any exception, so application will continue to work
  // - used by TSynLog to let the application continue with no exception,
  // even in case of a disk/partition full of logs
  TFileStreamWithoutWriteError = class(TFileStream)
  public
    /// this overriden function returns Count, as if it was always sucessfull
    function Write(const Buffer; Count: Longint): Longint; override;
  end;

  /// a per-family and/or per-thread log file content
  // - you should create a sub class per kind of log file
  // ! TSynLogDB = class(TSynLog);
  // - the TSynLog instance won't be allocated in heap, but will share a
  // per-thread (if Family.PerThreadLog=ptOneFilePerThread) or global private
  // log file instance
  // - was very optimized for speed, if no logging is written, and even during
  // log write (using an internal TTextWriter)
  // - can use available debugging information via the TSynMapFile class, for
  // stack trace logging for exceptions, sllStackTrace, and Enter/Leave labelling
  TSynLog = class(TObject, ISynLog)
  protected
    fFamily: TSynLogFamily;
    fWriter: TTextWriterWithEcho;
    fWriterClass: TTextWriterClass;
    fWriterStream: TStream;
    fThreadContext: PSynLogThreadContext;
    fThreadID: TThreadID;
    fThreadLastHash: integer;
    fThreadIndex: integer;
    fStartTimestamp: Int64;
    fCurrentTimestamp: Int64;
    {$ifndef LINUX}
    fFrequencyTimestamp: Int64;
    {$endif}
    fStartTimestampDateTime: TDateTime;
    fStreamPositionAfterHeader: cardinal;
    fFileName: TFileName;
    fFileRotationSize: cardinal;
    fFileRotationNextHour: Int64;
    fThreadHash: TWordDynArray; // 8 KB buffer
    fThreadIndexReleased: TWordDynArray;
    fThreadIndexReleasedCount: integer;
    fThreadContexts: array of TSynLogThreadContext;
    fThreadContextCount: integer;
    fCurrentLevel: TSynLogInfo;
    fInternalFlags: set of (logHeaderWritten, logInitDone);
    fDisableRemoteLog: boolean;
    {$ifndef NOEXCEPTIONINTERCEPT} // for IsBadCodePtr() or any internal exception
    fThreadHandleExceptionBackup: TSynLog;
    {$endif}
    {$ifdef FPC}
    function QueryInterface(
      {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    function _AddRef: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    function _Release: longint; {$IFNDEF WINDOWS}cdecl{$ELSE}stdcall{$ENDIF};
    {$else}
    function QueryInterface(const IID: TGUID; out Obj): HResult; stdcall;
    function _AddRef: Integer; stdcall;
    function _Release: Integer; stdcall;
    {$endif}
    class function FamilyCreate: TSynLogFamily;
    procedure CreateLogWriter; virtual;
    procedure LogInternal(Level: TSynLogInfo; const TextFmt: RawUTF8;
      const TextArgs: array of const; Instance: TObject); overload;
    procedure LogInternal(Level: TSynLogInfo; const Text: RawUTF8;
      Instance: TObject; TextTruncateAtLength: integer); overload;
    procedure LogInternal(Level: TSynLogInfo; const aName: RawUTF8;
      aTypeInfo: pointer; const aValue; Instance: TObject); overload;
    // any call to this method MUST call LogTrailerUnLock
    function LogHeaderLock(Level: TSynLogInfo; AlreadyLocked: boolean): boolean;
    procedure LogTrailerUnLock(Level: TSynLogInfo); {$ifdef HASINLINENOTX86}inline;{$endif}
    procedure LogCurrentTime; virtual;
    procedure LogFileInit; virtual;
    procedure LogFileHeader; virtual;
    {$ifndef DELPHI5OROLDER}
    procedure AddMemoryStats; virtual;
    {$endif}
    procedure AddErrorMessage(Error: cardinal);
    procedure AddStackTrace(Stack: PPtrUInt);
    procedure ComputeFileName; virtual;
    function GetFileSize: Int64; virtual;
    procedure PerformRotation; virtual;
    procedure AddRecursion(aIndex: integer; aLevel: TSynLogInfo);
    procedure LockAndGetThreadContext; {$ifdef HASINLINENOTX86}inline;{$endif}
    procedure GetThreadContextInternal;
    function NewRecursion: PSynLogThreadRecursion;
    procedure ThreadContextRehash;
    function Instance: TSynLog;
    function ConsoleEcho(Sender: TTextWriter; Level: TSynLogInfo;
      const Text: RawUTF8): boolean; virtual;
  public
    /// intialize for a TSynLog class instance
    // - WARNING: not to be called directly! Use Enter or Add class function instead
    constructor Create(aFamily: TSynLogFamily=nil); virtual;
    /// release all memory and internal handles
    destructor Destroy; override;
    /// flush all log content to file
    // - if ForceDiskWrite is TRUE, will wait until written on disk (slow)
    procedure Flush(ForceDiskWrite: boolean);
    /// flush all log content to file and close the file
    procedure CloseLogFile;
    /// flush all log content to file, close the file, and release the instance
    // - you should never call the Free method directly, since the instance
    // is registered in a global TObjectList and an access violation may
    // occur at application closing: you can use this Release method if you
    // are sure that you won't need this TSynLog instance any more
    // - ensure there is no pending Leave element in a stack-allocated ISynLog
    // (see below)
    // - can be used e.g. to release the instance when finishing a thread when
    // Family.PerThreadLog=ptOneFilePerThread:
    // ! var
    // !   TThreadLogger : TSynLogClass = TSynLog;
    // !
    // ! procedure TMyThread.Execute;
    // ! var log : ISynLog;
    // ! begin
    // !   log := TThreadLogger.Enter(self);
    // ! ...
    // !   log := nil; // to force logging end of method
    // !   TThreadLogger.SynLog.Release;
    // ! end;
    procedure Release;
    /// you may call this method when a thread is ended
    // - should be called in the thread context which is about to terminate,
    // in a situation where no other logging may occur from this thread any more
    // - it will release all thread-specific resource used by this TSynLog
    // - is called e.g. by TSQLRest.EndCurrentThread, via TSynLogFamily.OnThreadEnded
    procedure NotifyThreadEnded;
    /// handle generic method enter / auto-leave tracing
    // - this is the main method to be called within a procedure/function to trace:
    // ! procedure TMyDB.SQLExecute(const SQL: RawUTF8);
    // ! var log: ISynLog;
    // ! begin
    // !   log := TSynLogDB.Enter(self,'SQLExecute');
    // !   // do some stuff
    // !   log.Log(sllInfo,'SQL=%',[SQL]);
    // ! end; // here log will be released, and method leaving will be logged
    // - returning a ISynLog interface will allow you to have an automated
    // sllLeave log created when the method is left (thanks to the hidden
    // try..finally block generated by the compiler to protect the ISynLog var)
    // - WARNING: due to a limitation (feature?) of the FPC compiler and
    // Delphi 10.4 and later, you NEED to hold the returned value into a
    // local ISynLog variable; as a benefit, it is always convenient to define
    // a local variable to store the returned ISynLog and use it for any
    // specific logging within the method execution
    // - on Delphi earlier to 10.4 (and not FPC), you could just call Enter()
    // inside the method block, without any ISynLog interface variable - but
    // it is not very future-proof to write the following code:
    // ! procedure TMyDB.SQLFlush;
    // ! begin
    // !   TSynLogDB.Enter(self,'SQLFlush');
    // !   // do some stuff
    // ! end;
    // - if no Method name is supplied, it will use the caller address, and
    // will write it as hexa and with full unit and symbol name, if the debugging
    // information is available (i.e. if TSynMapFile retrieved the .map content;
    // note that this is not available yet on FPC):
    // ! procedure TMyDB.SQLFlush;
    // ! var log: ISynLog;
    // ! begin
    // !   log := TSynLogDB.Enter(self);
    // !   // do some stuff
    // ! end;
    // - note that supplying a method name is faster than using the .map content:
    // if you want accurate profiling, it's better to use a method name or not to
    // use a .map file - note that this method name shall be a constant, and not
    // a locally computed variable, since it may trigger some random GPF at
    // runtime - if it is a local variable, you can set aMethodNameLocal=true
    // - if TSynLogFamily.HighResolutionTimestamp is TRUE, high-resolution
    // time stamp will be written instead of ISO 8601 date and time: this will
    // allow performance profiling of the application on the customer side
    // - Enter() will write the class name (and the unit name for classes with
    // published properties, if TSynLogFamily.WithUnitName is true) for both
    // enter (+) and leave (-) events:
    //  $ 20110325 19325801  +    MyDBUnit.TMyDB(004E11F4).SQLExecute
    //  $ 20110325 19325801 info   SQL=SELECT * FROM Table;
    //  $ 20110325 19325801  -    01.512.320
    class function Enter(aInstance: TObject=nil; aMethodName: PUTF8Char=nil;
      aMethodNameLocal: boolean=false): ISynLog; overload;
    /// handle method enter / auto-leave tracing, with some custom text
    // - this overloaded method would not write the method name, but the supplied
    // text content, after expanding the parameters like FormatUTF8()
    // - it will append the corresponding sllLeave log entry when the method ends
    class function Enter(const TextFmt: RawUTF8; const TextArgs: array of const;
      aInstance: TObject=nil): ISynLog; overload;
    /// retrieve the current instance of this TSynLog class
    // - to be used for direct logging, without any Enter/Leave:
    // ! TSynLogDB.Add.Log(llError,'The % statement didn''t work',[SQL]);
    // - to be used for direct logging, without any Enter/Leave (one parameter
    // version - just the same as previous):
    // ! TSynLogDB.Add.Log(llError,'The % statement didn''t work',SQL);
    // - is just a wrapper around Family.SynLog - the same code will work:
    // ! TSynLogDB.Family.SynLog.Log(llError,'The % statement didn''t work',[SQL]);
    class function Add: TSynLog; {$ifdef HASINLINENOTX86}inline;{$endif}
    /// retrieve the family of this TSynLog class type
    class function Family: TSynLogFamily; overload; {$ifdef HASINLINENOTX86}inline;{$endif}
    /// returns a logging class which will never log anything
    // - i.e. a TSynLog sub-class with Family.Level := []
    class function Void: TSynLogClass;
    /// low-level method helper which can be called to make debugging easier
    // - log some warning message to the TSynLog family
    // - will force a manual breakpoint if tests are run from the IDE
    class procedure DebuggerNotify(Level: TSynLogInfo;
      const Format: RawUTF8; const Args: array of const);
    /// call this method to add some information to the log at the specified level
    // - will use TTextWriter.Add(...,twOnSameLine) to append its content
    // - % = #37 indicates a string, integer, floating-point, class parameter
    // to be appended as text (e.g. class name), any variant as JSON...
    // - note that cardinal values should be type-casted to Int64() (otherwise
    // the integer mapped value will be transmitted, therefore wrongly)
    procedure Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArgs: array of const;
      aInstance: TObject=nil); overload;
    /// same as Log(Level,TextFmt,[]) but with one RawUTF8 parameter
    procedure Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArg: RawUTF8;
      aInstance: TObject=nil); overload;
    /// same as Log(Level,TextFmt,[]) but with one Int64 parameter
    procedure Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArg: Int64;
      aInstance: TObject=nil); overload;
    /// call this method to add some information to the log at the specified level
    // - if Instance is set and Text is not '', it will log the corresponding
    // class name and address (to be used e.g. if you didn't call TSynLog.Enter()
    // method first) - for instance
    // ! TSQLLog.Add.Log(sllDebug,'GarbageCollector',GarbageCollector);
    // will append this line to the log:
    // $ 0000000000002DB9 debug TObjectList(00425E68) GarbageCollector
    // - if Instance is set and Text is '', will behave the same as
    // Log(Level,Instance), i.e. write the Instance as JSON content
    procedure Log(Level: TSynLogInfo; const Text: RawUTF8;
      aInstance: TObject=nil; TextTruncateAtLength: integer=maxInt); overload;
    {$ifdef UNICODE}
    /// call this method to add some VCL string to the log at a specified level
    // - this overloaded version will avoid a call to StringToUTF8()
    procedure Log(Level: TSynLogInfo; const Text: string; aInstance: TObject=nil); overload;
    {$endif}
    /// call this method to add the content of an object to the log at a
    // specified level
    // - this default implementation will just write the class name and its hexa
    // pointer value, and handle TList, TCollections and TStrings - for instance:
    // ! TSynLog.Add.Log(sllDebug,GarbageCollector);
    // will append this line to the log:
    // $ 20110330 10010005 debug {"TObjectList(00B1AD60)":["TObjectList(00B1AE20)","TObjectList(00B1AE80)"]}
    // - if aInstance is an Exception, it will handle its class name and Message:
    // $ 20110330 10010005 debug "EClassName(00C2129A)":"Exception message"
    // - use TSQLLog from mORMot.pas unit to add the record content, written
    // as human readable JSON
    procedure Log(Level: TSynLogInfo; aInstance: TObject); overload;
    /// call this method to add the content of most low-level types to the log
    // at a specified level
    // - this overridden implementation will write the value content,
    // written as human readable JSON: handle dynamic arrays and enumerations
    // - TSQLLog from mORMot.pas unit will be able to write
    // TObject/TSQLRecord and sets content as JSON
    procedure Log(Level: TSynLogInfo; const aName: RawUTF8;
      aTypeInfo: pointer; const aValue; Instance: TObject); overload;
    /// call this method to add the caller address to the log at the specified level
    // - if the debugging info is available from TSynMapFile, will log the
    // unit name, associated symbol and source code line
    procedure Log(Level: TSynLogInfo); overload;
    /// allows to identify the current thread with a textual representation
    // - would append an sllInfo entry with "SetThreadName ThreadID=Name" text
    // - entry would also be replicated at the begining of any rotated log file
    procedure LogThreadName(const Name: RawUTF8; IgnoreIfAlreadySet: boolean=false);
    /// call this method to add some multi-line information to the log at a
    // specified level
    // - LinesToLog content will be added, one line per one line, delimited by
    // #13#10 (CRLF)
    // - if a line starts with IgnoreWhenStartWith (already uppercase), it won't
    // be added to the log content (to be used e.g. with '--' for SQL statements)
    procedure LogLines(Level: TSynLogInfo; LinesToLog: PUTF8Char; aInstance: TObject=nil;
      const IgnoreWhenStartWith: PAnsiChar=nil);
    /// manual low-level TSynLog.Enter execution without the ISynLog
    // - may be used to log Enter/Leave stack from non-pascal code
    // - each call to ManualEnter should be followed by a matching ManualLeave
    // - aMethodName should be a not nil constant text
    procedure ManualEnter(aMethodName: PUtf8Char; aInstance: TObject = nil);
    /// manual low-level ISynLog release after TSynLog.Enter execution
    // - each call to ManualEnter should be followed by a matching ManualLeave
    procedure ManualLeave;
    /// allow to temporary disable remote logging
    // - to be used within a try ... finally section:
    // ! log.DisableRemoteLog(true);
    // ! try
    // !   log.Log(....); // won't be propagated to the remote log
    // ! finally
    // !   log.DisableRemoteLog(false);
    // ! end;
    procedure DisableRemoteLog(value: boolean);
    /// the associated TSynLog class
    function LogClass: TSynLogClass; {$ifdef HASINLINENOTX86}inline;{$endif}
    /// Force log rotation; Can be used for example inside SUGHUP signal handler
    procedure ForceRotation;
    /// direct access to the low-level writing content
    // - should usually not be used directly, unless you ensure it is safe
    property Writer: TTextWriterWithEcho read fWriter;
  published
    /// the associated file name containing the log
    // - this is accurate only with the default implementation of the class:
    // any child may override it with a custom logging mechanism
    property FileName: TFileName read fFileName;
    /// the current size, in bytes, of the associated file containing the log
    property FileSize: Int64 read GetFileSize;
    /// the current number of thread contexts associated with this instance
    // - doesn't match necessary the number of threads of the process, but the
    // threads which are still marked as active for this TSynLog
    // - a huge number may therefore not indicate a potential "out of memory"
    // error, but a broken logic with missing NotifyThreadEnded calls
    property ThreadContextCount: integer read fThreadContextCount;
    /// the associated logging family
    property GenericFamily: TSynLogFamily read fFamily;
  end;

  /// reference-counted block code critical section with context logging
  // - race conditions are difficult to track: you could use this TAutoLockerDebug
  // instead of plain TAutoLocker class, to log some information at each
  // Enter/Leave process, and track unexpected blocking issues
  // - see also the global USELOCKERDEBUG conditional, defined in Synopse.inc,
  // which may be used to enable verbose logging at compile time:
  // ! fSafe: IAutoLocker;
  // ! ...
  // ! {$ifdef USELOCKERDEBUG}
  // ! fSafe := TAutoLockerDebug.Create(fLogClass,aModel.Root); // more verbose
  // ! {$else}
  // ! fSafe := TAutoLocker.Create;
  // ! {$endif}
  TAutoLockerDebug = class(TAutoLocker)
  protected
    fLog: TSynLogClass;
    fIdentifier: RawUTF8;
    fCounter: integer;
  public
    /// initialize the mutex, which would log its Enter/Leave process
    // - the supplied identifier should be a short text, able to specify the
    // lock execution context, e.g. the resource which is actually protected
    // - an associated TSQLLog instance should be specified as logging target
    constructor Create(aLog: TSynLogClass; const aIdentifier: RawUTF8); reintroduce;
    /// enter the mutex
    procedure Enter; override;
    /// leave the mutex
    procedure Leave; override;
  end;

  /// used by TSynLogFile to refer to a method profiling in a .log file
  // - i.e. map a sllEnter/sllLeave event in the .log file
  TSynLogFileProc = record
    /// the index of the sllEnter event in the TSynLogFile.fLevels[] array
    Index: cardinal;
    /// the associated time elapsed in this method (in micro seconds)
    // - computed from the sllLeave time difference (high resolution timer)
    Time: cardinal;
    /// the time elapsed in this method and not in nested methods
    // - computed from Time property, minus the nested calls
    ProperTime: cardinal;
  end;

  /// used by TSynLogFile to refer to global method profiling in a .log file
  // - i.e. map all sllEnter/sllLeave event in the .log file
  TSynLogFileProcDynArray = array of TSynLogFileProc;

  TSynLogFileProcArray = array[0..(MaxInt div sizeof(TSynLogFileProc))-1] of TSynLogFileProc;
  PSynLogFileProcArray = ^TSynLogFileProcArray;

  /// used by TSynLogFile.LogProcSort method
  TLogProcSortOrder = (
    soNone, soByName, soByOccurrence, soByTime, soByProperTime);

  /// used to parse a .log file, as created by TSynLog, into high-level data
  // - this particular TMemoryMapText class will retrieve only valid event lines
  // (i.e. will fill EventLevel[] for each line <> sllNone)
  // - Count is not the global text line numbers, but the number of valid events
  // within the file (LinePointers/Line/Strings will contain only event lines) -
  // it will not be a concern, since the .log header is parsed explicitely
  TSynLogFile = class(TMemoryMapText)
  protected
    /// map the events occurring in the .log file content
    fLevels: TSynLogInfoDynArray;
    fThreads: TWordDynArray;
    fThreadInfo: array of record
      Rows: cardinal;
      SetThreadName: TPUTF8CharDynArray;
    end;
    fThreadInfoMax: cardinal;
    fThreadsCount: integer;
    fThreadMax: cardinal;
    fLineLevelOffset: cardinal;
    fLineTextOffset: cardinal;
    fLineHeaderCountToIgnore: integer;
    /// as extracted from the .log header
    fExeName, fExeVersion, fInstanceName: RawUTF8;
    fHost, fUser, fCPU, fOSDetailed, fFramework: RawUTF8;
    fExeDate: TDateTime;
    fIntelCPU: TIntelCpuFeatures;
    fOS: TWindowsVersion;
    fOSServicePack: integer;
    fWow64: boolean;
    fStartDateTime: TDateTime;
    fDayCurrent: Int64; // as PInt64('20160607')^
    fDayChangeIndex: TIntegerDynArray;
    fDayCount: TIntegerDynArray;
    /// retrieve all used event levels
    fLevelUsed: TSynLogInfos;
    /// =0 if date time resolution, >0 if high-resolution time stamp
    fFreq: Int64;
    /// used by EventDateTime() to compute date from time stamp
    fFreqPerDay: double;
    /// custom headers, to be searched as .ini content
    fHeaderLinesCount: integer;
    fHeaders: RawUTF8;
    /// method profiling data
    fLogProcCurrent: PSynLogFileProcArray;
    fLogProcCurrentCount: integer;
    fLogProcNatural: TSynLogFileProcDynArray;
    fLogProcNaturalCount: integer;
    fLogProcMerged: TSynLogFileProcDynArray;
    fLogProcMergedCount: integer;
    fLogProcIsMerged: boolean;
    fLogProcStack: array of array of cardinal;
    fLogProcStackCount: array of integer;
    fLogProcSortInternalOrder: TLogProcSortOrder;
    /// used by ProcessOneLine//GetLogLevelTextMap
    fLogLevelsTextMap: array[TSynLogInfo] of cardinal;
    fIsJournald: boolean;
    procedure SetLogProcMerged(const Value: boolean);
    function GetEventText(index: integer): RawUTF8;
    function GetLogLevelFromText(LineBeg: PUTF8Char): TSynLogInfo;
    /// retrieve headers + fLevels[] + fLogProcNatural[], and delete invalid fLines[]
    procedure LoadFromMap(AverageLineLength: integer=32); override;
    /// compute fLevels[] + fLogProcNatural[] for each .log line during initial reading
    procedure ProcessOneLine(LineBeg, LineEnd: PUTF8Char); override;
    /// called by LogProcSort method
    function LogProcSortComp(A,B: PtrInt): PtrInt;
    procedure LogProcSortInternal(L,R: PtrInt);
  public
    /// initialize internal structure
    constructor Create; override;
    /// returns TRUE if the supplied text is contained in the corresponding line
    function LineContains(const aUpperSearch: RawUTF8; aIndex: Integer): Boolean; override;
    /// retrieve the date and time of an event
    // - returns 0 in case of an invalid supplied index
    function EventDateTime(aIndex: integer): TDateTime;
    /// retrieve the description text of an event, as native VCL string
    // - returns '' if supplied index is out of range
    // - if the text is not truly UTF-8 encoded, would use the current system
    // codepage to create a valid string
    // - you may specify a text to replace all #9 characters occurences
    // - is used e.g. in TMainLogView.ListDrawCell
    function EventString(index: integer; const replaceTabs: RawUTF8='';
      maxutf8len: Integer=0; includeFirstColumns: boolean=false): string;
    /// sort the LogProc[] array according to the supplied order
    procedure LogProcSort(Order: TLogProcSortOrder);
    /// return the number of matching events in the log
    function EventCount(const aSet: TSynLogInfos): integer;
    /// add a new line to the already parsed content
    // - overriden method which would identify the freq=%,%,% pseudo-header
    procedure AddInMemoryLine(const aNewLine: RawUTF8); override;
    /// returns the name of a given thread, according to the position in the log
    function ThreadName(ThreadID, CurrentLogIndex: integer): RawUTF8;
    /// returns the name of all threads, according to the position in the log
    // - result[0] stores the name of ThreadID = 1
    function ThreadNames(CurrentLogIndex: integer): TRawUTF8DynArray;
    /// returns all days of this log file
    // - only available for low-resolution timestamp, i.e. Freq=0
    procedure GetDays(out Days: TDateTimeDynArray);
    /// returns the number of occurences of a given thread
    function ThreadRows(ThreadID: integer): cardinal;
    /// retrieve the level of an event
    // - is calculated by Create() constructor
    // - EventLevel[] array index is from 0 to Count-1
    property EventLevel: TSynLogInfoDynArray read fLevels;
    /// retrieve all used event levels
    // - is calculated by Create() constructor
    property EventLevelUsed: TSynLogInfos read fLevelUsed;
    /// retrieve the description text of an event
    // - returns '' if supplied index is out of range
    // - see also EventString() function, for direct VCL use
    property EventText[index: integer]: RawUTF8 read GetEventText;
    /// retrieve all event thread IDs
    // - contains something if TSynLogFamily.PerThreadLog was ptIdentifiedInOnFile
    // - for ptMergedInOneFile (default) or ptOneFilePerThread logging process,
    // the array will be void (EventThread=nil)
    property EventThread: TWordDynArray read fThreads;
    /// the number of threads
    property ThreadsCount: cardinal read fThreadMax;
    /// profiled methods information
    // - is calculated by Create() constructor
    // - will contain the sllEnter index, with the associated elapsed time
    // - number of items in the array is retrieved by the LogProcCount property
    property LogProc: PSynLogFileProcArray read fLogProcCurrent;
    /// the current sort order
    property LogProcOrder: TLogProcSortOrder read fLogProcSortInternalOrder;
    /// if the method information must be merged for the same method name
    property LogProcMerged: boolean read fLogProcIsMerged write SetLogProcMerged;
    /// all used event levels, as retrieved at log file content parsing
    property LevelUsed: TSynLogInfos read fLevelUsed;
    /// high-resolution time stamp frequence, as retrieved from log file header
    // - equals 0 if date time resolution, >0 if high-resolution time stamp
    property Freq: Int64 read fFreq;
    /// the row indexes where the day changed
    // - only available for low-resolution timestamp, i.e. Freq=0
    // - if set, contains at least [0] if the whole log is over a single day
    property DayChangeIndex: TIntegerDynArray read fDayChangeIndex;
    /// the number of rows for each DayChangeIndex[] value
    property DayCount: TIntegerDynArray read fDayCount;
    /// custom headers, to be searched as .ini content
    property Headers: RawUTF8 read fHeaders;
    /// the available CPU features, as recognized at program startup
    // - is extracted from the last part of the CPU property text
    // - you could use the overloaded ToText() function to show it in an
    // human-friendly way
    property IntelCPU: TIntelCpuFeatures read fIntelCPU;
  published
    /// the associated executable name (with path)
    // - returns e.g. 'C:\Dev\lib\SQLite3\exe\TestSQL3.exe'
    property ExecutableName: RawUTF8 read fExeName;
    /// the associated executable version
    // - returns e.g. '0.0.0.0'
    property ExecutableVersion: RawUTF8 read fExeVersion;
    /// the associated executable build date and time
    property ExecutableDate: TDateTime read fExeDate;
    /// for a library, the associated instance name (with path)
    // - returns e.g. 'C:\Dev\lib\SQLite3\exe\TestLibrary.dll'
    // - for an executable, will be left void
    property InstanceName: RawUTF8 read fInstanceName;
    /// the computer host name in which the process was running on
    property ComputerHost: RawUTF8 read fHost;
    /// the computer user name who launched the process
    property RunningUser: RawUTF8 read fUser;
    /// the computer CPU in which the process was running on
    // - returns e.g. '1*0-15-1027'
    property CPU: RawUTF8 read fCPU;
    /// the computer Operating System in which the process was running on
    // - equals wUnknown on Linux or BSD - use DetailedOS instead
    property OS: TWindowsVersion read fOS;
    /// the Operating System Service Pack number
    // - not defined on Linux or BSD - use DetailedOS instead
    property ServicePack: integer read fOSServicePack;
    /// if the 32 bit process was running under WOW 64 virtual emulation
    property Wow64: boolean read fWow64;
    /// the computer Operating System in which the process was running on
    // - returns e.g. '2.3=5.1.2600' for Windows XP
    // - under Linux, it will return the full system version, e.g.
    // 'Ubuntu=Linux-3.13.0-43-generic#72-Ubuntu-SMP-Mon-Dec-8-19:35:44-UTC-2014'
    property DetailedOS: RawUTF8 read fOSDetailed;
    /// the associated framework information
    // - returns e.g. 'TSQLLog 1.18.2765 ERTL FTS3'
    property Framework: RawUTF8 read fFramework;
    /// the date and time at which the log file was started
    property StartDateTime: TDateTime read fStartDateTime;
    /// number of profiled methods in this .log file
    // - i.e. number of items in the LogProc[] array
    property LogProcCount: integer read fLogProcCurrentCount;
  end;

  /// used to parse a .log file and process into VCL/LCL/FMX
  // - would handle e.g. selection and search feature
  TSynLogFileView = class(TSynLogFile)
  protected
    fSelected: TIntegerDynArray;
    fSelectedCount: integer;
    fEvents: TSynLogInfos;
    fThreadSelected: TByteDynArray;
    fThreadSelectedMax: integer;
    procedure LoadFromMap(AverageLineLength: integer=32); override;
    function GetThreads(thread: integer): boolean;
    procedure SetThreads(thread: integer; value: boolean);
  public
    /// add a new line to the already parsed content
    // - overriden method would add the inserted index to Selected[]
    procedure AddInMemoryLine(const aNewLine: RawUTF8); override;
    /// search for the next matching TSynLogInfo, from the current row index
    // - returns -1 if no match was found
    function SearchNextEvent(aEvent: TSynLogInfo; aRow: integer): integer;
    /// search for the next matching text, from the current row index
    // - returns -1 if no match was found
    function SearchNextText(const aPattern: RawUTF8; aRow, aDelta: integer): integer;
    /// search for the previous matching text, from the current row index
    // - returns -1 if no match was found
    function SearchPreviousText(const aPattern: RawUTF8; aRow: integer): integer;
    /// search for the matching Enter/Leave item, from the current row index
    // - returns -1 if no match was found
    function SearchEnterLeave(aRow: integer): integer;
    /// search for the next specified thread, from the current row index
    // - returns -1 if no match was found
    function SearchThread(aThreadID: word; aRow: integer): integer;
    /// search for the next diverse thread, from the current row index
    // - returns -1 if no match was found
    function SearchNextThread(aRow: integer): integer;
    /// search for the next matching thread, from the current row index
    // - returns -1 if no match was found
    function SearchNextSameThread(aRow: integer): integer;
    /// search for the next row index, appearing after the supplied item index
    // - returns -1 if no match was found
    function SearchNextSelected(aIndex: integer): integer;
    /// search for the previous matching thread, from the current row index
    // - returns -1 if no match was found
    function SearchPreviousSameThread(aRow: integer): integer;
    /// returns the ready-to-be text of a cell of the main TDrawGrid
    function GetCell(aCol, aRow: integer; out aLevel: TSynLogInfo): string;
    /// returns the ready-to-be displayed text of one or several selected rows
    function GetLineForMemo(aRow,aTop,aBottom: integer): string;
    /// returns the ready-to-be copied text of a selected row
    function GetLineForClipboard(aRow: integer): string;
    /// fill all rows matching Events and Threads[] properties in Selected[]
    // - you may specify the current selected row index, which would return
    // the closest one after the selection has been applied
    function Select(aRow: integer): integer; virtual;
    /// set all Threads[] to a specified value
    procedure SetAllThreads(enabled: boolean);
    /// define the current selection range, according to event kinds
    // - once you have set Events and Threads[], call Select() to fill Selected[]
    property Events: TSynLogInfos read fEvents write fEvents;
    /// define the current selection range, according to a thread ID
    // - here the supplied thread ID starts at 1
    // - once you have set Events and Threads[], call Select() to fill Selected[]
    property Threads[thread: integer]: boolean read GetThreads write SetThreads;
    /// the row indexes of the selected entries
    property Selected: TIntegerDynArray read fSelected;
    /// how many entries are currently stored in Selected[]
    property SelectedCount: integer read fSelectedCount;
  end;

type
  /// a list of lof events families, used to gather events by type
  TSynLogFilter = (
    lfNone,lfAll,lfErrors,lfExceptions,lfProfile,lfDatabase,lfClientServer,
    lfDebug,lfCustom,lfDDD);

  /// syslog message facilities as defined by RFC 3164
  TSyslogFacility = (sfKern, sfUser, sfMail, sfDaemon, sfAuth, sfSyslog, sfLpr,
    sfNews, sfUucp, sfClock, sfAuthpriv, sfFtp, sfNtp, sfAudit, sfAlert, sfCron,
    sfLocal0, sfLocal1, sfLocal2, sfLocal3, sfLocal4, sfLocal5, sfLocal6, sfLocal7);

  /// syslog message severities as defined by RFC 5424
  TSyslogSeverity = (ssEmerg, ssAlert, ssCrit, ssErr, ssWarn, ssNotice, ssInfo, ssDebug);

const
  /// up to 16 TSynLogFamily, i.e. TSynLog children classes can be defined
  MAX_SYNLOGFAMILY = 15;

  /// can be set to TSynLogFamily.Level in order to log all available events
  LOG_VERBOSE: TSynLogInfos = [succ(sllNone)..high(TSynLogInfo)];

  /// contains the logging levels for which stack trace should be dumped
  // - which are mainly exceptions or application errors
  LOG_STACKTRACE: TSynLogInfos = [sllException,sllExceptionOS,
    sllLastError,sllError,sllDDDError];

  /// the text equivalency of each logging level, as written in the log file
  // - PCardinal(@LOG_LEVEL_TEXT[L][3])^ will be used for fast level matching
  // so text must be unique for characters [3..6] -> e.g. 'UST4'
  LOG_LEVEL_TEXT: array[TSynLogInfo] of string[7] = (
    '       ', ' info  ', ' debug ', ' trace ', ' warn  ', ' ERROR ',
    '  +    ', '  -    ',
    ' OSERR ', ' EXC   ', ' EXCOS ', ' mem   ', ' stack ', ' fail  ',
    ' SQL   ', ' cache ', ' res   ', ' DB    ', ' http  ', ' clnt  ', ' srvr  ',
    ' call  ', ' ret   ', ' auth  ',
    ' cust1 ', ' cust2 ', ' cust3 ', ' cust4 ', ' rotat ', ' dddER ', ' dddIN ',
    ' mon   ');

  /// RGB colors corresponding to each logging level
  // - matches the TColor values, as used by the VCL
  LOG_LEVEL_COLORS: array[Boolean,TSynLogInfo] of integer = (
    ($FFFFFF,$DCC0C0,$DCDCDC,$C0C0C0,$8080C0,$8080FF,$C0DCC0,$DCDCC0,
{  sllNone, sllInfo, sllDebug, sllTrace, sllWarning, sllError, sllEnter, sllLeave, }
     $C0C0F0, $C080FF, $C080F0, $C080C0, $C080C0,
{  sllLastError, sllException, sllExceptionOS, sllMemory, sllStackTrace,           }
     $4040FF, $B08080, $B0B080, $8080DC, $80DC80, $DC8080, $DCFF00, $DCD000,
{  sllFail, sllSQL, sllCache, sllResult, sllDB, sllHTTP, sllClient, sllServer,     }
     $DCDC80, $DC80DC, $DCDCDC,
{  sllServiceCall, sllServiceReturn, sllUserAuth,                                  }
     $D0D0D0, $D0D0DC, $D0D0C0, $D0D0E0, $20E0D0, $8080FF, $DCCDCD, $C0C0C0),
{  sllCustom1, sllCustom2, sllCustom3, sllCustom4, sllNewRun, sllDDDError,sllDDDInfo }
    ($000000,$000000,$000000,$000000,$000000,$FFFFFF,$000000,$000000,
     $FFFFFF,$FFFFFF,$FFFFFF,$000000,$000000,
     $FFFFFF,$FFFFFF,$000000,$FFFFFF,$000000,$000000,$000000,$000000,
     $000000,$000000,$000000,
     $000000,$000000,$000000,$000000,$000000,$FFFFFF,$000000,$000000));

  /// console colors corresponding to each logging level
  // - SynCommons' TextColor()
  LOG_CONSOLE_COLORS: array[TSynLogInfo] of TConsoleColor = (
  //    sllNone, sllInfo, sllDebug, sllTrace, sllWarning, sllError, sllEnter, sllLeave
    ccLightGray,ccWhite,ccLightGray,ccLightBlue,ccBrown,ccLightRed,ccGreen,ccGreen,
  //    sllLastError, sllException, sllExceptionOS, sllMemory, sllStackTrace,
    ccLightRed, ccLightRed, ccLightRed, ccLightGray, ccCyan,
  //    sllFail, sllSQL, sllCache, sllResult, sllDB, sllHTTP, sllClient, sllServer,
    ccLightRed, ccBrown, ccBlue, ccLightCyan, ccMagenta, ccCyan, ccLightCyan, ccLightCyan,
  //    sllServiceCall, sllServiceReturn, sllUserAuth,
    ccLightMagenta, ccLightMagenta, ccMagenta,
  //    sllCustom1, sllCustom2, sllCustom3, sllCustom4,
    ccLightGray, ccLightGray,ccLightGray,ccLightGray,
  //    sllNewRun, sllDDDError, sllDDDInfo, sllMonitoring
    ccLightMagenta, ccLightRed, ccWhite, ccLightBlue);

  /// how TLogFilter map TSynLogInfo events
  LOG_FILTER: array[TSynLogFilter] of TSynLogInfos = (
    [], [succ(sllNone)..high(TSynLogInfo)],
    [sllError,sllLastError,sllException,sllExceptionOS],
    [sllException,sllExceptionOS], [sllEnter,sllLeave],
    [sllSQL,sllCache,sllDB], [sllClient,sllServer,sllServiceCall, sllServiceReturn],
    [sllDebug,sllTrace,sllEnter], [sllCustom1..sllCustom4],[sllDDDError,sllDDDInfo]);

  /// the "magic" number used to identify .log.synlz compressed files, as
  // created by TSynLogFamily.EventArchiveSynLZ
  LOG_MAGIC = $ABA51051;

  /// may be used to log as Debug or Error event, depending on an Error: boolean
  LOG_DEBUGERROR: array[boolean] of TSynLogInfo = (sllDebug, sllError);

  /// may be used to log as Trace or Warning event, depending on an Error: boolean
  LOG_TRACEWARNING: array[boolean] of TSynLogInfo = (sllTrace, sllWarning);

  /// may be used to log as Info or Warning event, depending on an Error: boolean
  LOG_INFOWARNING: array[boolean] of TSynLogInfo = (sllInfo, sllWarning);

  /// used to convert a TSynLog event level into a syslog message severity
  LOG_TO_SYSLOG: array[TSynLogInfo] of TSyslogSeverity = (
   ssDebug, ssInfo, ssDebug, ssDebug, ssNotice, ssWarn,
  // sllNone, sllInfo, sllDebug, sllTrace, sllWarning, sllError,
   ssDebug, ssDebug,
  // sllEnter, sllLeave,
  ssWarn, ssErr, ssErr, ssDebug, ssDebug,
  // sllLastError, sllException, sllExceptionOS, sllMemory, sllStackTrace,
  ssNotice, ssDebug, ssDebug, ssDebug, ssDebug, ssDebug, ssDebug, ssDebug,
  // sllFail, sllSQL, sllCache, sllResult, sllDB, sllHTTP, sllClient, sllServer,
  ssDebug, ssDebug, ssDebug,
  // sllServiceCall, sllServiceReturn, sllUserAuth,
  ssDebug, ssDebug, ssDebug, ssDebug, ssNotice,
  // sllCustom1, sllCustom2, sllCustom3, sllCustom4, sllNewRun,
  ssWarn, ssInfo, ssDebug);
  // sllDDDError, sllDDDInfo, sllMonitoring);


/// returns the trimmed text value of a logging level
// - i.e. 'Warning' for sllWarning
function ToText(event: TSynLogInfo): RawUTF8; overload;

/// returns the trimmed text value of a logging levels set
function ToText(events: TSynLogInfos): ShortString; overload;

/// returns the ready-to-be displayed text of a TSynLogInfo value
function ToCaption(event: TSynLogInfo): string; overload;

/// returns the ready-to-be displayed text of a TSynLogFilter value
function ToCaption(filter: TSynLogFilter): string; overload;

/// returns a method event as text, using the .map/.mab information if available
function ToText(const Event: TMethod): RawUTF8; overload;

var
  /// the kind of .log file generated by TSynTestsLogged
  TSynLogTestLog: TSynLogClass = TSynLog;

var
  /// low-level variable used internaly by this unit
  // - do not access this variable in your code: defined here to allow inlining
  GlobalThreadLock: TRTLCriticalSection;

{$ifndef NOEXCEPTIONINTERCEPT}
  /// low-level variable used internaly by this unit
  // - do not access this variable in your code: defined here to allow inlining
  GlobalCurrentHandleExceptionSynLog: TSynLog;
{$endif NOEXCEPTIONINTERCEPT}

type
  /// storage of the information associated with an intercepted exception
  // - as returned by GetLastException() function
  TSynLogExceptionInfo = record
    /// low-level calling context
    // - as used by TSynLogExceptionToStr callbacks
    Context: TSynLogExceptionContext;
    /// associated Exception.Message content (if any)
    Message: string;
    /// ready-to-be-displayed text of the exception address
    Addr: RawUTF8;
  end;
  /// storage of the information associated with one or several exceptions
  // - as returned by GetLastExceptions() function
  TSynLogExceptionInfoDynArray = array of TSynLogExceptionInfo;

/// makes a thread-safe copy of the latest intercepted exception
function GetLastException(out info: TSynLogExceptionInfo): boolean;

/// returns some text about the latest intercepted exception
function GetLastExceptionText: RawUTF8;

/// makes a thread-safe copy of the latest intercepted exceptions
procedure GetLastExceptions(out result: TSynLogExceptionInfoDynArray;
  Depth: integer=0); overload;

{$ifndef NOVARIANTS}
/// returns a TDocVariant array of the latest intercepted exception texts
// - runs ToText() over all information returned by overloaded GetLastExceptions
function GetLastExceptions(Depth: integer=0): variant; overload;
{$endif}

/// convert low-level exception information into some human-friendly text
function ToText(var info: TSynLogExceptionInfo): RawUTF8; overload;

/// a TSynLogArchiveEvent handler which will delete older .log files
function EventArchiveDelete(const aOldLogFileName, aDestinationPath: TFileName): boolean;

/// a TSynLogArchiveEvent handler which will compress older .log files
// using our proprietary SynLZ format
// - resulting file will have the .synlz extension and will be located
// in the aDestinationPath directory, i.e. TSynLogFamily.ArchivePath+'\log\YYYYMM\'
// - use UnSynLZ.dpr tool to uncompress it into .log textual file
// - SynLZ is much faster than zip for compression content, but proprietary
function EventArchiveSynLZ(const aOldLogFileName, aDestinationPath: TFileName): boolean;

/// append some information to a syslog message memory buffer
// - following https://tools.ietf.org/html/rfc5424 specifications
// - ready to be sent via UDP to a syslog remote server
// - returns the number of bytes written to destbuffer (which should have
// destsize > 127)
function SyslogMessage(facility: TSyslogFacility; severity: TSyslogSeverity;
  const msg, procid, msgid: RawUTF8; destbuffer: PUTF8Char; destsize: PtrInt;
  trimmsgfromlog: boolean): PtrInt;

/// check if the supplied file name is a currently working log file
// - may be used to avoid e.g. infinite recursion when monitoring the log file
function IsActiveLogFile(const aFileName: TFileName): boolean;


implementation

{$ifdef FPC}
uses
  SynFPCTypInfo // small wrapper unit around FPC's TypInfo.pp
  {$ifdef Linux}
  , SynFPCLinux, BaseUnix, Unix, Errors, dynlibs
  {$endif} ;
{$endif FPC}

var
  _LogInfoText: array[TSynLogInfo] of RawUTF8;
  _LogInfoCaption: array[TSynLogInfo] of string;

function ToText(event: TSynLogInfo): RawUTF8;
begin
  result := _LogInfoText[event];
end;

function ToText(events: TSynLogInfos): ShortString;
begin
  GetSetNameShort(TypeInfo(TSynLogInfos), events, result, {trimleft=}true);
end;

function ToCaption(event: TSynLogInfo): string;
begin
  result := _LogInfoCaption[event];
end;

function ToCaption(filter: TSynLogFilter): string;
begin
  result := GetCaptionFromEnum(TypeInfo(TSynLogFilter), Ord(filter))
end;


{ TSynLogSettings }

constructor TSynLogSettings.Create;
begin
  inherited Create;
  fDestinationPath := GetSystemPath(spLog);
  fLevels := LOG_STACKTRACE + [sllNewRun];
  fRotateFileCount := 2;
end;

procedure TSynLogSettings.SetLog(aLogClass: TSynLogClass);
var
  f: TSynLogFamily;
begin
  if self = nil then
    exit;
  if aLogClass = nil then
    aLogClass := TSynLog;
  f := aLogClass.Family;
  f.DestinationPath := EnsureDirectoryExists(fDestinationPath);
  f.PerThreadLog := ptIdentifiedInOnFile; // ease multi-threaded server debug
  f.RotateFileCount := fRotateFileCount;
  if fRotateFileCount > 0 then begin
    f.RotateFileSizeKB := 20 * 1024; // rotate by 20 MB logs
    f.FileExistsAction := acAppend;  // as expected in rotation mode
  end
  else
    f.HighResolutionTimestamp := true;
  f.Level := fLevels;
  fLogClass := aLogClass;
end;


{ TSynMapFile }

const
  MAGIC_MAB = $A5A5A5A5;

function MatchPattern(P,PEnd,Up: PUTF8Char; var Dest: PUTF8Char): boolean;
begin
  result := false;
  repeat
    if P^ in [#1..' '] then repeat inc(P) until not(P^ in [#1..' ']);
    while NormToUpperAnsi7[P^]=Up^ do begin
      inc(P);
      if P>PEnd then exit;
      inc(Up);
      if (Up^=' ') and (P^ in [#1..' ']) then begin // ignore multiple spaces in P^
        while (P<PEnd) and (P^ in [#1..' ']) do inc(P);
        inc(Up);
      end;
    end;
    if Up^=#0 then // all chars of Up^ found in P^
      break else
    if Up^<>' ' then // P^ and Up^ didn't match
      exit;
    inc(Up);
  until false;
  while (P<PEnd) and (P^=' ') do inc(P); // ignore all spaces
  result := true;
  Dest := P;
end;

procedure ReadSymbol(var R: TFileBufferReader; var A: TDynArray);
var i, n, L: integer;
    S: PSynMapSymbol;
    Addr: cardinal;
    P: PByte;
begin
  n := R.ReadVarUInt32;
  A.Count := n;
  P := R.CurrentMemory;
  if (n=0) or (P=nil) then
    exit;
  S := A.Value^;
  for i := 0 to n-1 do begin
    L := FromVarUInt32(P); // inlined R.Read(S^.Name)
    FastSetString(S^.Name,P,L);
    inc(P,L);
    inc(PByte(S),A.ElemSize); // may be TSynMapSymbol or TSynMapUnit
  end;
  S := A.Value^;
  Addr := FromVarUInt32(P);
  S^.Start := Addr;
  for i := 1 to n-1 do begin
    inc(Addr,FromVarUInt32(P));
    S^.Stop := Addr-1;
    inc(PByte(S),A.ElemSize);
    S^.Start := Addr;
  end;
  S^.Stop := Addr+FromVarUInt32(P);
  R.Seek(PtrUInt(P)-PtrUInt(R.MappedBuffer));
end;

const
  /// Delphi linker starts the code section at this fixed offset
  CODE_SECTION = $1000;

constructor TSynMapFile.Create(const aExeName: TFileName=''; MabCreate: boolean=true);

  procedure LoadMap;
    var P, PEnd: PUTF8Char;
    procedure NextLine;
    begin
      while (P<PEnd) and (P^>=' ') do inc(P);
      if (P<PEnd) and (P^=#13) then inc(P);
      if (P<PEnd) and (P^=#10) then inc(P);
    end;
    function GetCode(var Ptr: integer): boolean;
    begin
      while (P<PEnd) and (P^=' ') do inc(P);
      result := false;
      if (P+10<PEnd) and
         (PInteger(P)^=ord('0')+ord('0')shl 8+ord('0')shl 16+ord('1')shl 24) and
         (P[4]=':') then begin
        if not HexDisplayToBin(PAnsiChar(P)+5,@Ptr,sizeof(Ptr)) then exit;
        while (P<PEnd) and (P^>' ') do inc(P);
        while (P<PEnd) and (P^=' ') do inc(P);
        if P<PEnd then
          result := true;
      end;
    end;
    procedure ReadSegments;
    var Beg: PAnsiChar;
        U: TSynMapUnit;
    begin
      NextLine;
      NextLine;
      while (P<PEnd) and (P^<' ') do inc(P);
      while (P+10<PEnd) and (P^>=' ') do begin
        if GetCode(U.Symbol.Start) and
           HexDisplayToBin(PAnsiChar(P),@U.Symbol.Stop,4) then begin
          while PWord(P)^<>ord('M')+ord('=')shl 8 do
            if P+10>PEnd then exit else inc(P);
          Beg := pointer(P+2);
          while (P<PEnd) and (P^>' ') do inc(P);
          FastSetString(U.Symbol.Name,Beg,P-Beg);
          inc(U.Symbol.Stop,U.Symbol.Start-1);
          if (U.Symbol.Name<>'') and
             ((U.Symbol.Start<>0) or (U.Symbol.Stop<>0)) then
            fUnits.FindHashedAndUpdate(U,{addifnotexisting=}true);
        end;
        NextLine;
      end;
    end;
    procedure ReadSymbols;
    var Beg: PUtf8Char;
        Sym: TSynMapSymbol;
        {$ifdef ISDELPHI2005ANDUP}
        u, l: PtrInt;
        LastUnitUp: RawUTF8; // e.g. 'MORMOT.CORE.DATA.'
        {$endif ISDELPHI2005ANDUP}
    begin
      NextLine;
      NextLine;
      while (P+10<PEnd) and (P^>=' ') do begin
        if GetCode(Sym.Start) then begin
          while (P<PEnd) and (P^=' ') do inc(P);
          Beg := pointer(P);
          while (P<PEnd) and (P^>' ') do inc(P);
          {$ifdef ISDELPHI2005ANDUP}
          // trim left 'UnitName.' for each symbol (since Delphi 2005)
          if (LastUnitUp <> '') and IdemPChar(Beg, pointer(LastUnitUp)) then
            // common case since symbols are grouped by address, i.e. by unit
            inc(Beg, length(LastUnitUp))
          else begin // manual unit name search
            LastUnitUp := '';
            for u := 0 to fUnits.Count - 1 do
              with fUnit[u].Symbol do begin
                l := length(Name);
                if (Beg[l] = '.') and (l > length(LastUnitUp)) and
                   IdemPropNameU(Name, Beg, l) then
                  LastUnitUp := UpperCase(Name); // find longest match
              end;
            if LastUnitUp <> '' then begin
              l := length(LastUnitUp);
              SetLength(LastUnitUp, l + 1);
              LastUnitUp[l] := '.';
              inc(Beg, l + 1);
            end;
          end;
          {$endif ISDELPHI2005ANDUP}
          FastSetString(Sym.Name,Beg,P-Beg);
          if (Sym.Name<>'') and not (Sym.Name[1] in ['$','?']) then
            fSymbols.Add(Sym);
        end;
        NextLine;
      end;
    end;
    procedure ReadLines;
    var Beg: PAnsiChar;
        i, Count, n: integer;
        aName: RawUTF8;
        added: boolean;
        U: ^TSynMapUnit;
    begin
      Beg := pointer(P);
      while P^<>'(' do if P=PEnd then exit else inc(P);
      FastSetString(aName,Beg,P-Beg);
      if aName='' then
        exit;
      i := fUnits.FindHashedForAdding(aName,added);
      U := @fUnit[i];
      if added then
        U^.Symbol.Name := aName; // should not occur, but who knows...
      if U^.FileName='' then begin
        inc(P); Beg := pointer(P);
        while P^<>')' do if P=PEnd then exit else inc(P);
        FastSetString(U^.FileName,Beg,P-Beg);
      end;
      NextLine;
      NextLine;
      n := length(U^.Line);
      Count := n; // same unit may appear multiple times in .map content
      while (P+10<PEnd) and (P^>=' ') do begin
        while (P<PEnd) and (P^=' ') do inc(P);
        repeat
          if Count=n then begin
            n := NextGrow(n);
            SetLength(U^.Line,n);
            SetLength(U^.Addr,n);
          end;
          U^.Line[Count] := GetNextItemCardinal(P,' ');
          if not GetCode(U^.Addr[Count]) then
            break;
          if U^.Addr[Count]<>0 then
            inc(Count); // occured with Delphi 2010 :(
        until (P>=PEnd) or (P^<' ');
        NextLine;
      end;
      SetLength(U^.Line,Count);
      SetLength(U^.Addr,Count);
    end;
  var i, s,u: integer;
      RehashNeeded: boolean;
  begin // LoadMap
    fSymbols.Capacity := 8000;
    with TSynMemoryStreamMapped.Create(fMapFile) do
    try
      // parse .map sections into fSymbol[] and fUnit[]
      P := Memory;
      PEnd := P+Size;
      while P<PEnd do
        if MatchPattern(P,PEnd,'DETAILED MAP OF SEGMENTS',P) then
          ReadSegments else
        if MatchPattern(P,PEnd,'ADDRESS PUBLICS BY VALUE',P) then
          ReadSymbols else
        if MatchPattern(P,PEnd,'LINE NUMBERS FOR',P) then
          ReadLines else
          NextLine;
      // now we should have read all .map content
      s := fSymbols.Count-1;
      RehashNeeded := false;
      for i := fUnits.Count-1 downto 0 do
        with fUnit[i] do
          if (Symbol.Start=0) and (Symbol.Stop=0) then begin
            fUnits.Delete(i); // occurs with Delphi 2010 :(
            RehashNeeded := true;
          end;
      u := fUnits.Count-1;
      if RehashNeeded then
        fUnits.ReHash; // as expected by TDynArrayHashed
      {$ifopt C+}
      for i := 1 to u do
         assert(fUnit[i].Symbol.Start>fUnit[i-1].Symbol.Stop);
      {$endif}
      for i := 0 to s-1 do
        fSymbol[i].Stop := fSymbol[i+1].Start-1;
      if (u>=0) and (s>=0) then
        fSymbol[s].Stop := fUnit[u].Symbol.Stop;
    finally
      Free;
    end;
  end;

  procedure LoadMab(const aMabFile: TFileName);
  var R: TFileBufferReader;
      i: integer;
      S: TCustomMemoryStream;
      MS: TMemoryStream;
  begin
    fMapFile := aMabFile;
    if FileExists(aMabfile) then
    try
      S := TSynMemoryStreamMapped.Create(aMabFile);
      try
        MS := StreamUnSynLZ(S,MAGIC_MAB);
        if MS<>nil then
        try
          R.OpenFrom(MS.Memory,MS.Size);
          ReadSymbol(R,fSymbols);
          ReadSymbol(R,fUnits{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif});
          fUnits.ReHash;
          for i := 0 to fUnits.Count-1 do
          with fUnit[i] do begin
            R.Read(FileName);
            R.ReadVarUInt32Array(Line);
            R.ReadVarUInt32Array(Addr);
          end;
          MabCreate := false;
        finally
          MS.Free;
        end;
      finally
        S.Free;
      end;
    except
      on Exception do; // invalid file -> ignore any problem
    end;
  end;

var i: integer;
    MabFile: TFileName;
    MapAge, MabAge: TDateTime;
    U: RawUTF8;
begin
  fSymbols.Init(TypeInfo(TSynMapSymbolDynArray),fSymbol,@fSymCount);
  fUnits.Init(TypeInfo(TSynMapUnitDynArray),fUnit,nil,nil,nil,@fUnitCount);
  fUnitSynLogIndex := -1;
  fUnitSystemIndex := -1;
  // 1. search for an external .map file matching the running .exe/.dll name
  if aExeName='' then begin
    fMapFile := GetModuleName(hInstance);
    {$ifdef MSWINDOWS}
    fCodeOffset := GetModuleHandle(pointer(ExtractFileName(fMapFile)))+CODE_SECTION;
    {$else}
    {$ifdef KYLIX3}
    fCodeOffset := GetTextStart; // from SysInit.pas
    {$endif}
    {$endif}
  end else
    fMapFile := aExeName;
  fMapFile := ChangeFileExt(fMapFile,'.map');
  MabFile := ChangeFileExt(fMapFile,'.mab');
  GlobalLock;
  try
    MapAge := FileAgeToDateTime(fMapFile);
    MabAge := FileAgeToDateTime(MabFile);
    if (MapAge>0) and (MabAge<MapAge) then
      LoadMap; // if no faster-to-load .mab available and accurate
    // 2. search for a .mab file matching the running .exe/.dll name
    if (fSymCount=0) and (MabAge<>0) then
      LoadMab(MabFile);
    // 3. search for an embedded compressed .mab file appended to the .exe/.dll
    if fSymCount=0 then
      if aExeName='' then
        LoadMab(GetModuleName(hInstance)) else
        LoadMab(aExeName);
    // finalize symbols
    if fSymCount>0 then begin
      for i := 1 to fSymCount-1 do
        assert(fSymbol[i].Start>fSymbol[i-1].Stop);
      SetLength(fSymbol,fSymCount);
      SetLength(fUnit,fUnitCount);
      fSymbols.Init(TypeInfo(TSynMapSymbolDynArray),fSymbol);
      fUnits.Init(TypeInfo(TSynMapUnitDynArray),fUnit);
      if MabCreate then
        SaveToFile(MabFile); // if just created from .map -> create .mab file
      U := 'SynLog';
      fUnitSynLogIndex := fUnits.FindHashed(U);
      U := 'System';
      fUnitSystemIndex := fUnits.FindHashed(U);
      fHasDebugInfo := true;
    end else
      fMapFile := '';
  finally
    GlobalUnLock;
  end;
end;

procedure WriteSymbol(var W: TFileBufferWriter; const A: TDynArray);
var i, n: integer;
    Diff: integer;
    S: PSynMapSymbol;
    P,Beg: PByte;
    tmp: RawByteString;
begin
  n := A.Count;
  W.WriteVarUInt32(n);
  if n=0 then exit;
  S := A.Value^;
  for i := 0 to n-1 do begin
    W.Write(S^.Name);
    inc(PByte(S),A.ElemSize); // may be TSynMapSymbol or TSynMapUnit
  end;
  S := A.Value^;
  Diff := S^.Start;
  W.WriteVarUInt32(Diff);
  P := pointer(W.DirectWritePrepare(n*5,tmp));
  Beg := P;
  for i := 1 to n-1 do begin
    inc(PByte(S),A.ElemSize);
    P := ToVarUInt32(S^.Start-Diff,P);
    Diff := S^.Start;
  end;
  P := ToVarUInt32(S^.Stop-Diff,P);
  W.DirectWriteFlush(PtrUInt(P)-PtrUInt(Beg),tmp);
end;

procedure TSynMapFile.SaveToStream(aStream: TStream);
var W: TFileBufferWriter;
    i: integer;
    MS: TMemoryStream;
begin
  MS := THeapMemoryStream.Create;
  W := TFileBufferWriter.Create(MS,1 shl 20); // 1 MB should be enough at first
  try
    WriteSymbol(W,fSymbols);
    WriteSymbol(W,fUnits{$ifdef UNDIRECTDYNARRAY}.InternalDynArray{$endif});
    for i := 0 to high(fUnit) do
    with fUnit[i] do begin
      W.Write(FileName);
      W.WriteVarUInt32Array(Line,length(Line),wkOffsetI); // not always increasing
      W.WriteVarUInt32Array(Addr,length(Addr),wkOffsetU); // always increasing
    end;
    W.Flush;
    StreamSynLZ(MS,aStream,MAGIC_MAB);
  finally
    MS.Free;
    W.Free;
  end;
end;

procedure TSynMapFile.SaveToJson(W: TTextWriter);
begin
  W.AddShort('{"Symbols":');
  W.AddDynArrayJSON(fSymbols);
  W.AddShort(',"Units":');
  W.AddDynArrayJSON(fUnits);
  W.Add('}');
end;

procedure TSynMapFile.SaveToJson(const aJsonFile: TFileName; aHumanReadable: Boolean);
var S: TFileStream;
    W: TTextWriter;
    json: RawUTF8;
begin
  if aHumanReadable then begin
    W := TTextWriter.CreateOwnedStream(65536);
    try
      SaveToJson(W);
      W.SetText(json);
      JSONBufferReformatToFile(pointer(json),aJsonFile)
    finally
      W.Free;
    end;
  end else begin
    S := TFileStream.Create(aJsonFile,fmCreate);
    try
      W := TTextWriter.Create(S,65536);
      try
        SaveToJson(W);
        W.FlushToStream;
      finally
        W.Free;
      end;
    finally
      S.Free;
    end;
  end;
end;

function TSynMapFile.SaveToFile(const aFileName: TFileName=''): TFileName;
var F: TFileStream;
begin
  if aFileName='' then
    result := ChangeFileExt(GetModuleName(hInstance),'.mab') else
    result := aFileName;
  DeleteFile(result);
  F := TFileStream.Create(result,fmCreate);
  try
    SaveToStream(F);
  finally
    F.Free;
  end;
end;

procedure TSynMapFile.SaveToExe(const aExeName: TFileName);
var mabfilename: TFileName;
    exe, mab: TMemoryStream;
    exesize, mabsize: PtrUInt;
begin
  if not FileExists(aExeName) then
    exit;
  mabfilename := SaveToFile(ChangeFileExt(aExeName,'.mab'));
  try
    exe := THeapMemoryStream.Create;
    mab := THeapMemoryStream.Create;
    try
      // load both files
      mab.LoadFromFile(mabfilename);
      mabsize := mab.Size;
      exe.LoadFromFile(aExeName);
      exesize := exe.Size;
      if exesize<16 then
        exit;
      // trim existing mab content
      exesize := StreamSynLZComputeLen(exe.Memory,exesize,MAGIC_MAB);
      exe.Size := exesize+mabsize;
      // append mab content to exe
      MoveFast(mab.Memory^,PAnsiChar(exe.Memory)[exesize],mabsize);
      exe.SaveToFile(aExeName);
    finally
      mab.Free;
      exe.Free;
    end;
  finally
    DeleteFile(mabfilename);
  end;
end;

function TSynMapFile.FindSymbol(aAddressOffset: integer): integer;
var L,R: integer;
begin
  R := high(fSymbol);
  L := 0;
  if (R>=0) and
     (aAddressOffset>=fSymbol[0].Start) and
     (aAddressOffset<=fSymbol[R].Stop) then
  repeat
    result := (L+R)shr 1;
    with fSymbol[result] do
      if aAddressOffset<Start then
        R := result-1 else
      if aAddressOffset>Stop then
        L := result+1 else
        exit;
  until L>R;
  result := -1;
end;

function TSynMapFile.FindUnit(aAddressOffset: integer; out LineNumber: integer): integer;
var L,R,n,max: integer;
begin
  LineNumber := 0;
  R := high(fUnit);
  L := 0;
  if (R>=0) and
     (aAddressOffset>=fUnit[0].Symbol.Start) and
     (aAddressOffset<=fUnit[R].Symbol.Stop) then
  repeat
    result := (L+R) shr 1;
    with fUnit[result] do
      if aAddressOffset<Symbol.Start then
        R := result-1 else
      if aAddressOffset>Symbol.Stop then
        L := result+1 else begin
        // unit found -> search line number
        L := 0;
        max := high(Addr);
        R := max;
        if R>=0 then
        repeat
          n := (L+R) shr 1;
          if aAddressOffset<Addr[n] then
            R := n-1 else
          if (n<max) and (aAddressOffset>=Addr[n+1]) then
            L := n+1 else begin
            LineNumber := Line[n];
            exit;
          end;
        until L>R;
        exit;
      end;
  until L>R;
  result := -1;
end;

var
  ExeInstanceMapFile: TSynMapFile;

function GetInstanceMapFile: TSynMapFile;
begin
  if ExeInstanceMapFile=nil then
    GarbageCollectorFreeAndNil(ExeInstanceMapFile,TSynMapFile.Create);
  result := ExeInstanceMapFile;
end;

function ToText(const Event: TMethod): RawUTF8;
begin
  FormatUTF8('% using %(%)', [GetInstanceMapFile.FindLocation(PtrUInt(Event.Code)),
    TObject(Event.Data), Event.Data], result);
end;

function TSynMapFile.AbsoluteToOffset(aAddressAbsolute: PtrUInt): integer;
begin
  if self=nil then
    result := 0 else
    result := PtrInt(aAddressAbsolute)-PtrInt(fCodeOffset);
end;

class procedure TSynMapFile.Log(W: TTextWriter; aAddressAbsolute: PtrUInt;
  AllowNotCodeAddr: boolean);
var {$ifdef FPC}s: ShortString;{$else}u, s, Line, offset: integer;{$endif}
begin
  if (W=nil) or (aAddressAbsolute=0) then
    exit;
  {$ifdef FPC}
  s := BacktraceStrFunc(pointer(aAddressAbsolute));
  if Pos('SynLog.pas',s)=0 then // don't log internal calls
    W.AddShort(s);
  {$else}
  with GetInstanceMapFile do
  if HasDebugInfo then begin
    offset := AbsoluteToOffset(aAddressAbsolute);
    s := FindSymbol(offset);
    u := FindUnit(offset,Line);
    if s<0 then begin
      if u<0 then begin
        if AllowNotCodeAddr then begin
          W.AddBinToHexDisplayMinChars(@aAddressAbsolute,SizeOf(aAddressAbsolute));
          W.Add(' ');
        end;
        exit;
      end;
    end else
      if (u>=0) and (s>=0) and not AllowNotCodeAddr then
        if u=fUnitSynLogIndex  then
          exit else // don't log stack trace internal to SynLog.pas :)
        if (u=fUnitSystemIndex) and (PosEx('Except',Symbols[s].Name)>0) then
          exit; // do not log stack trace of System.SysRaiseException
    W.AddBinToHexDisplayMinChars(@aAddressAbsolute,SizeOf(aAddressAbsolute));
    W.Add(' ');
    if u>=0 then begin
      W.AddString(Units[u].Symbol.Name);
      if s>=0 then
        if Symbols[s].Name=Units[u].Symbol.Name then
          s := -1 else
          W.Add('.');
    end;
    if s>=0 then
      W.AddString(Symbols[s].Name);
    W.Add(' ');
    if Line>0 then begin
      W.Add('(');
      W.Add(Line);
      W.Add(')',' ');
    end;
  end else begin // no .map info available -> display address
    W.AddBinToHexDisplayMinChars(@aAddressAbsolute,SizeOf(aAddressAbsolute));
    W.Add(' ');
  end;
  {$endif FPC}
end;

function TSynMapFile.FindLocation(aAddressAbsolute: PtrUInt): RawUTF8;
var u,s,Line,offset: integer;
begin
  if (self=nil) or (aAddressAbsolute=0) or not HasDebugInfo then begin
    PointerToHex(pointer(aAddressAbsolute),result);
    exit;
  end;
  offset := AbsoluteToOffset(aAddressAbsolute);
  s := FindSymbol(offset);
  u := FindUnit(offset,Line);
  if (s<0) and (u<0) then begin
    {$ifdef FPC} // note: BackTraceStrFunc is much slower than TSynMapFile.Log
    if @BackTraceStrFunc=@SysBackTraceStr then // has debug information?
      PointerToHex(pointer(aAddressAbsolute),result) else
      ShortStringToAnsi7String(BackTraceStrFunc(pointer(aAddressAbsolute)),result);
    {$endif FPC}
    exit;
  end;
  result := result+' ';
  if u>=0 then begin
    result := result+Units[u].Symbol.Name;
    if s>=0 then
      if Symbols[s].Name=result then
        s := -1 else
        result := result+'.';
  end;
  if s>=0 then
    result := result+Symbols[s].Name;
  if Line>0 then
    result := result+' ('+UInt32ToUtf8(Line)+')';
end;

class function TSynMapFile.FindLocation(exc: ESynException): RawUTF8;
begin
  if (exc=nil) or (exc.RaisedAt=nil) then
    result := '' else
    result := GetInstanceMapFile.FindLocation(PtrUInt(exc.RaisedAt));
end;

class function TSynMapFile.FindStackTrace(
  const Ctxt: TSynLogExceptionContext): TRawUTF8DynArray;
var i: PtrInt;
    exe: TSynMapFile;
begin
  result := nil;
  exe := GetInstanceMapFile;
  AddRawUTF8(result,exe.FindLocation(Ctxt.EAddr));
  for i := 0 to Ctxt.EStackCount-1 do
    if (i=0) or (PPtrUIntArray(Ctxt.EStack)[i]<>PPtrUIntArray(Ctxt.EStack)[i-1]) then
      AddRawUTF8(result,exe.FindLocation(PPtrUIntArray(Ctxt.EStack)[i]));
end;

function TSynMapFile.FindUnit(const aUnitName: RawUTF8): integer;
begin
  if (self<>nil) and (aUnitName<>'') then
    for result := 0 to high(fUnit) do
      if IdemPropNameU(fUnit[result].Symbol.Name,aUnitName) then
        exit;
  result := -1;
end;

class function TSynMapFile.FindFileName(const unitname: RawUTF8): TFileName;
var map: TSynMapFile;
    name: RawUTF8;
    u: integer;
begin
  result := '';
  map := GetInstanceMapFile;
  if map = nil then
    exit;
  if unitname='' then
    name := ExeVersion.ProgramName else
    name := unitname;
  u := map.FindUnit(name);
  if u>=0 then
    result := UTF8ToString(map.fUnit[u].FileName);
end;

class function TSynMapFile.FromCurrentExecutable: TSynMapFile;
begin
  result := GetInstanceMapFile;
end;


{ TSynLogFamily }

type
  /// an array to all available per-thread TSynLogFile instances
  TSynLogFileIndex = array[0..MAX_SYNLOGFAMILY] of integer;

var
  /// internal list of registered TSynLogFamily instances
  // - up to MAX_SYNLOGFAMILY+1 families may be defined
  SynLogFamily: TSynObjectList = nil;

  /// internal list of created TSynLog instances, one per each log file on disk
  // - do not use directly - necessary for inlining TSynLogFamily.SynLog method
  // - also used by AutoFlushProc() to get a global list of TSynLog instances
  SynLogFileList: TSynObjectListLocked = nil;

threadvar
  /// each thread can access to its own TSynLogFile
  // - is used to implement TSynLogFamily.PerThreadLog=ptOneFilePerThread option
  // - the current TSynLogFile instance of the living thread is
  // ! SynLogFileList[SynLogFileIndexThreadVar[TSynLogFamily.Ident]-1]
  SynLogFileIndexThreadVar: TSynLogFileIndex;
  /// each thread can have exceptions interception disabled
  // - as set by TSynLogFamily.ExceptionIgnoreCurrentThread property
  ExceptionIgnorePerThread: boolean;

/// if defined, will use AddVectoredExceptionHandler() API call
// - this one does not produce accurate stack trace by now, and is supported
// only since Windows XP
// - so default method using RTLUnwindProc should be prefered with Delphi
{.$define WITH_VECTOREXCEPT}

function ToText(var info: TSynLogExceptionInfo): RawUTF8;
begin
  with info.Context do
    if ELevel<>sllNone then begin
      if info.Addr='' then
        info.Addr := GetInstanceMapFile.FindLocation(EAddr);
      FormatUTF8('% % at %: % [%]',[_LogInfoCaption[ELevel],EClass,info.Addr,
        DateTimeToIso8601Text(UnixTimeToDateTime(ETimestamp),' '),
        StringToUTF8(info.Message)],result);
    end else
      result := '';
end;

function GetLastExceptionText: RawUTF8;
var info: TSynLogExceptionInfo;
begin
  if GetLastException(info) then
    result := ToText(info) else
    result := '';
end;

{$ifndef NOVARIANTS}
function GetLastExceptions(Depth: integer): variant;
var info: TSynLogExceptionInfoDynArray;
    i: integer;
begin
  VarClear(result);
  GetLastExceptions(info,Depth);
  if info=nil then
    exit;
  TDocVariantData(result).InitFast(length(info),dvArray);
  for i := 0 to high(info) do
    TDocVariantData(result).AddItemText(ToText(info[i]));
end;
{$endif}

function PrintUSAscii(P: PUTF8Char; const text: RawUTF8): PUTF8Char;
var i: PtrInt;
begin
  P^ := ' ';
  inc(P);
  for i := 1 to length(text) do
    if ord(text[i]) in [33..126] then begin // only printable ASCII chars
      P^ := text[i];
      inc(P);
    end;
  if P[-1]=' ' then begin
    P^ := '-'; // nothing appended -> NILVALUE
    inc(P);
  end;
  result := P;
end;

function SyslogMessage(facility: TSyslogFacility; severity: TSyslogSeverity;
  const msg, procid, msgid: RawUTF8; destbuffer: PUTF8Char; destsize: PtrInt;
  trimmsgfromlog: boolean): PtrInt;
var P: PAnsiChar;
    start: PUTF8Char;
    len: PtrInt;
    st: TSynSystemTime;
begin
  result := 0;
  if destsize<127 then
    exit;
  start := destbuffer;
  destbuffer^ := '<';
  destbuffer := AppendUInt32ToBuffer(destbuffer+1,ord(severity)+ord(facility) shl 3);
  PInteger(destbuffer)^ := ord('>')+ord('1')shl 8+ord(' ')shl 16; // VERSION=1
  inc(destbuffer,3);
  st.FromNowUTC;
  DateToIso8601PChar(destbuffer,true,st.Year,st.Month,st.Day);
  TimeToIso8601PChar(destbuffer+10,true,st.Hour,st.Minute,st.Second,st.MilliSecond,'T',true);
  destbuffer[23] := 'Z';
  inc(destbuffer,24);
  with ExeVersion do begin
    if length(Host)+length(ProgramName)+length(procid)+length(msgid)+
       (destbuffer-start)+15>destsize then
      exit; // avoid buffer overflow
    destbuffer := PrintUSAscii(destbuffer,Host); // HOST
    destbuffer := PrintUSAscii(destbuffer,ProgramName); // APP-NAME
  end;
  destbuffer := PrintUSAscii(destbuffer,procid); // PROCID
  destbuffer := PrintUSAscii(destbuffer,msgid);  // MSGID
  destbuffer := PrintUSAscii(destbuffer,'');     // no STRUCTURED-DATA
  destbuffer^ := ' ';
  inc(destbuffer);
  len := length(msg);
  P := pointer(msg);
  if trimmsgfromlog and (len>27) then
    if (P[0]='2') and (P[8]=' ') then begin
      inc(P,27); // trim e.g. '20160607 06442255  ! trace '
      dec(len,27);
    end else
    if SynCommons.HexToBin(P,nil,8) then begin
      inc(P,25); // trim e.g. '00000000089E5A13  " info '
      dec(len,25);
    end;
  while (len>0) and (P^<=' ') do begin // trim left spaces
    inc(P);
    dec(len);
  end;
  len := Utf8TruncatedLength(P,len,destsize-(destbuffer-start)-3);
  if not IsAnsiCompatible(P,len) then begin
    PInteger(destbuffer)^ := $bfbbef; // UTF-8 BOM
    inc(destbuffer,3);
  end;
  MoveFast(P^,destbuffer^,len);
  result := (destbuffer-start)+len;
end;

function IsActiveLogFile(const aFileName: TFileName): boolean;
var i: PtrInt;
    one: ^TSynLog;
    files: TSynObjectListLocked;
begin
  result := true;
  files := SynLogFileList;
  files.Safe.Lock;
  try
    one := pointer(files.List);
    for i := 1 to files.Count do
      if {$ifdef MSWINDOWS}CompareText(one^.FileName,aFileName)=0{$else}
         one^.FileName=aFileName{$endif} then
        exit else
        inc(one);
  finally
    files.Safe.UnLock;
  end;
  result := false;
end;

{$ifdef NOEXCEPTIONINTERCEPT}

function GetLastException(out info: TSynLogExceptionInfo): boolean;
begin
  result := false;
end;

procedure GetLastExceptions(out result: TSynLogExceptionInfoDynArray;
  Depth: integer);
begin
end;

{$else}

{$ifdef DELPHI5OROLDER}
  {$define WITH_PATCHEXCEPT}
{$endif}

{$ifdef KYLIX3}
  // Kylix has a totally diverse exception scheme
  {$define WITH_MAPPED_EXCEPTIONS}
{$endif}

{$ifdef FPC}
  {$ifdef WIN64}
    {$define WITH_VECTOREXCEPT} // use AddVectoredExceptionHandler Win64 API
  {$else}
    // Win32, Linux: intercept via the RaiseProc global variable
    {$define WITH_RAISEPROC}
  {$endif}
{$else}
  {$ifdef CPU64}
    {$define WITH_VECTOREXCEPT}
  {$endif}
{$endif}

const
  MAX_EXCEPTHISTORY = 15;
type
  TSynLogExceptionInfos = array[0..MAX_EXCEPTHISTORY] of TSynLogExceptionInfo;
var
  GlobalCurrentHandleExceptionHooked: boolean;
  GlobalLastException: TSynLogExceptionInfos;
  GlobalLastExceptionIndex: integer = -1;

function GetLastException(out info: TSynLogExceptionInfo): boolean;
begin
  if GlobalLastExceptionIndex<0 then begin
    result := false;
    exit; // no exception intercepted yet
  end;
  EnterCriticalSection(GlobalThreadLock);
  try
    info := GlobalLastException[GlobalLastExceptionIndex];
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
  info.Context.EInstance := nil; // avoid any GPF
  info.Context.EStack := nil;
  result := info.Context.ELevel<>sllNone;
end;

procedure GetLastExceptions(out result: TSynLogExceptionInfoDynArray;
  Depth: integer);
var infos: TSynLogExceptionInfos; // use thread-safe local copy
    index,last,n,i: integer;
begin
  if GlobalLastExceptionIndex<0 then
    exit; // no exception intercepted yet
  EnterCriticalSection(GlobalThreadLock);
  try
    infos := GlobalLastException;
    index := GlobalLastExceptionIndex;
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
  n := MAX_EXCEPTHISTORY+1;
  if (Depth>0) and (n>Depth) then
    n := Depth;
  SetLength(result,n);
  last := MAX_EXCEPTHISTORY;
  for i := 0 to n-1 do begin
    if i<=index then
      result[i] := infos[index-i] else begin
      result[i] := infos[last];
      dec(last);
    end;
    with result[i].Context do
      if ELevel=sllNone then begin
        SetLength(result,i); // truncate to latest available exception
        break;
      end else begin
        EInstance := nil; // avoid any GPF
        EStack := nil;
      end;
  end;
end;

// this is the main entry point for all intercepted exceptions
procedure SynLogException(const Ctxt: TSynLogExceptionContext);
  function GetHandleExceptionSynLog: TSynLog;
  var files: TSynObjectListLocked;
      Index: ^TSynLogFileIndex;
      i: PtrInt;
      ndx, n: cardinal;
  begin
    result := nil;
    files := SynLogFileList;
    if files.Count=0 then begin // no log content yet
      for i := 0 to SynLogFamily.Count-1 do
        with TSynLogFamily(SynLogFamily.List[i]) do
        if fHandleExceptions then begin
          result := SynLog;
          exit;
        end;
    end else begin
      files.Safe.Lock;
      try
        Index := @SynLogFileIndexThreadVar;
        n := files.Count;
        for i := 0 to high(Index^) do begin
          ndx := Index^[i]-1;
          if ndx<=n then begin
            result := files.List[ndx];
            if result.fFamily.fHandleExceptions then
              exit;
          end;
        end;
        for i := 0 to n-1 do begin
          result := files.List[i];
          if result.fFamily.fHandleExceptions then
            exit;
        end;
        result := nil;
      finally
        files.Safe.UnLock;
      end;
    end;
  end;
var log: TSynLog;
    info: ^TSynLogExceptionInfo;
    locked: boolean;
    {$ifdef FPC}i: PtrInt;{$endif}
label adr,fin;
begin
  if ExceptionIgnorePerThread then
    exit;
  {$ifdef CPU64DELPHI} // Delphi<XE6 in System.pas to retrieve x64 dll exit code
  {$ifndef ISDELPHIXE6}
  if (Ctxt.EInstance<>nil) and // Ctxt.EClass is EExternalException
     (PShortString(PPointer(PPtrInt(Ctxt.EInstance)^+vmtClassName)^)^=
      '_TExitDllException') then
    exit;
  {$endif ISDELPHIXE6}
  {$endif CPU64DELPHI}
  log := GlobalCurrentHandleExceptionSynLog;
  if (log=nil) or not log.fFamily.fHandleExceptions then
    log := GetHandleExceptionSynLog;
  if (log=nil) or not (Ctxt.ELevel in log.fFamily.Level) then
    exit;
  if (Ctxt.EClass=ESynLogSilent) or
     (log.fFamily.ExceptionIgnore.IndexOf(Ctxt.EClass)>=0) then
    exit;
  locked := false;
  try
    if Assigned(log.fFamily.OnBeforeException) then begin
      log.LockAndGetThreadContext; // protect and set fThreadContext
      locked := true;
      if log.fFamily.OnBeforeException(Ctxt,log.fThreadContext^.ThreadName) then
        exit;
    end;
    if log.LogHeaderLock(Ctxt.ELevel,locked) then begin
      locked := true;
      if GlobalLastExceptionIndex=MAX_EXCEPTHISTORY then
        GlobalLastExceptionIndex := 0 else
        inc(GlobalLastExceptionIndex);
      info := @GlobalLastException[GlobalLastExceptionIndex];
      info^.Context := Ctxt;
      {$ifdef FPC}
      if @BackTraceStrFunc<>@SysBackTraceStr then
        ShortStringToAnsi7String(BackTraceStrFunc(pointer(Ctxt.EAddr)),info^.Addr) else
      {$endif FPC}
        info^.Addr := '';
      if (Ctxt.ELevel=sllException) and (Ctxt.EInstance<>nil) then begin
        info^.Message := Ctxt.EInstance.Message;
        if Ctxt.EInstance.InheritsFrom(ESynException) then begin
          ESynException(Ctxt.EInstance).RaisedAt := pointer(Ctxt.EAddr);
          if ESynException(Ctxt.EInstance).CustomLog(log.fWriter,Ctxt) then
            goto fin;
          goto adr;
        end;
      end else
        info^.Message := '';
      if Assigned(DefaultSynLogExceptionToStr) and
         DefaultSynLogExceptionToStr(log.fWriter,Ctxt) then
        goto fin;
adr:  log.fWriter.Add(' [%] at ',[log.fThreadContext^.ThreadName],twOnSameLine);
      {$ifdef FPC} // note: BackTraceStrFunc is much slower than TSynMapFile.Log
      with log.fWriter do
      if @BackTraceStrFunc=@SysBackTraceStr then begin // no debug information
        AddPointer(Ctxt.EAddr); // write addresses as hexa
        for i := 0 to Ctxt.EStackCount-1 do
          if (i=0) or (Ctxt.EStack[i]<>Ctxt.EStack[i-1]) then begin
            Add(' ');
            AddPointer(Ctxt.EStack[i]);
          end;
      end else begin
        AddString(info^.Addr);
        for i := 0 to Ctxt.EStackCount-1 do
          if (i=0) or (Ctxt.EStack[i]<>Ctxt.EStack[i-1]) then
            AddShort(BackTraceStrFunc(pointer(Ctxt.EStack[i])));
      end;
      {$else}
      TSynMapFile.Log(log.fWriter,Ctxt.EAddr,true);
      {$ifndef WITH_VECTOREXCEPT} // stack frame OK for RTLUnwindProc by now
      log.AddStackTrace(Ctxt.EStack);
      {$endif}
      {$endif FPC}
fin:  log.fWriter.AddEndOfLine(log.fCurrentLevel);
      log.fWriter.FlushToStream; // we expect exceptions to be available on disk
    end;
  finally
    if locked then begin
      GlobalCurrentHandleExceptionSynLog := log.fThreadHandleExceptionBackup;
      LeaveCriticalSection(GlobalThreadLock);
    end;
  end;
end;

{$ifdef WITH_PATCHEXCEPT}

var
  // Delphi 5 doesn't define the needed RTLUnwindProc variable :(
  // so we will patch the System.pas RTL in-place
  RTLUnwindProc: Pointer;

procedure PatchCallRtlUnWind;
procedure Patch(P: PAnsiChar);
{   004038B6 52               push edx  // Save exception object
    004038B7 51               push ecx  // Save exception address
    004038B8 8B542428         mov edx,[esp+$28]
    004038BC 83480402         or dword ptr [eax+$04],$02
    004038C0 56               push esi  // Save handler entry
    004038C1 6A00             push $00
    004038C3 50               push eax
    004038C4 68CF384000       push $004038cf  // @@returnAddress
    004038C9 52               push edx
    004038CA E88DD8FFFF       call RtlUnwind
    ...
RtlUnwind:
    0040115C FF255CC14100     jmp dword ptr [$0041c15c]
    where $0041c15c is a pointer to the address of RtlUnWind in kernel32.dll
    -> we will replace [$0041c15c] by [RTLUnwindProc]    }
var i: Integer;
    addr: PAnsiChar;
begin
  for i := 0 to 31 do
    if (PCardinal(P)^=$6850006a) and  // push 0; push eax; push @@returnAddress
       (PWord(P+8)^=$E852) then begin // push edx; call RtlUnwind
      inc(P,10); // go to call RtlUnwind address
      if PInteger(P)^<0 then begin
        addr := P+4+PInteger(P)^;
        if PWord(addr)^=$25FF then begin // jmp dword ptr []
          PatchCodePtrUInt(Pointer(addr+2),cardinal(@RTLUnwindProc));
          exit;
        end;
      end;
    end else
    inc(P);
end;
asm
  mov eax,offset System.@HandleAnyException+200
  call Patch
end;

// the original unwider function, from the Windows API
procedure oldUnWindProc; external kernel32 name 'RtlUnwind';

{$endif WITH_PATCHEXCEPT}

{$ifdef WITH_MAPPED_EXCEPTIONS} // Kylix specific exception handling

{$W-} // disable stack frame generation (duplicate from Synopse.inc)

threadvar
  CurrentTopOfStack: Cardinal;

procedure ComputeCurrentTopOfStack;
const UNWINDFI_TOPOFSTACK  = $BE00EF00; // from SysInit.pas
var top: cardinal;
begin
  asm
    mov top,esp
  end;
  top := (top and (not 3))+4;
  try
    while PCardinal(top)^<>UNWINDFI_TOPOFSTACK do
      inc(top,4);
  except
  end;
  CurrentTopOfStack := top;
end;

function IsBadReadPtr(addr: pointer; len: integer): boolean;
begin
  try
    asm
      mov eax,addr
      mov ecx,len
 @s:  mov dl,[eax]
      inc eax
      dec ecx
      jnz @s
 @e:end;
    result := false; // if we reached here, everything is ok
  except
    result := true;
  end;
end;

// types and constants from from System.pas / unwind.h

type
  PInternalUnwindException = ^TInternalUnwindException;
  TInternalUnwindException = packed record
    exception_class: LongWord;
    exception_cleanup: Pointer;
    private_1: pointer;
    private_2: LongWord;
  end;

  PInternalRaisedException = ^TInternalRaisedException;
  TInternalRaisedException = packed record
    RefCount: Integer;
    ExceptObject: Exception;
    ExceptionAddr: PtrUInt;
    HandlerEBP: LongWord;
    Flags: LongWord;
    Cleanup: Pointer;
    Prev: PInternalRaisedException;
    ReleaseProc: Pointer;
  end;

const
  Internal_UW_EXC_CLASS_BORLANDCPP = $FBEE0001;
  Internal_UW_EXC_CLASS_BORLANDDELPHI = $FBEE0101;
  Internal_excIsBeingHandled = $00000001;
  Internal_excIsBeingReRaised = $00000002;

var oldUnwinder, newUnwinder: TUnwinder;

function HookedRaiseException(Exc: Pointer): LongBool; cdecl;
var ExcRec: PInternalRaisedException;
    Ctxt: TSynLogExceptionContext;
begin
  if GlobalCurrentHandleExceptionSynLog<>nil then
    if Exc<>nil then begin
      Ctxt.ECode := PInternalUnwindException(Exc)^.exception_class;
      case Ctxt.ECode of
      Internal_UW_EXC_CLASS_BORLANDDELPHI: begin
        ExcRec := PInternalUnwindException(Exc)^.private_1;
        if (ExcRec<>nil) and (ExcRec^.ExceptObject<>nil) then begin
          Ctxt.EInstance := ExcRec^.ExceptObject;
          Ctxt.EClass := PPointer(Ctxt.EInstance)^;
          if Ctxt.EInstance is EExternal then begin
            Ctxt.EAddr := EExternal(Ctxt.EInstance).ExceptionAddress;
            Ctxt.ELevel := sllExceptionOS;
          end else begin
            Ctxt.EAddr := ExcRec^.ExceptionAddr;
            Ctxt.ELevel := sllException;
          end;
          Ctxt.EStack := nil;
          Ctxt.EStackCount := 0;
          Ctxt.ETimestamp := UnixTimeUTC; // very fast API call
          SynLogException(Ctxt);
        end;
        // (ExcRec^.Flags and Internal_excIsBeingHandled)<>0)
        // (ExcRec^.Flags and Internal_excIsBeingReRaised)<>0)
      end;
      Internal_UW_EXC_CLASS_BORLANDCPP: ; // not handled
      end;
    end;
  if Assigned(oldUnwinder.RaiseException) then
    result := oldUnwinder.RaiseException(Exc) else
    result := false;
end;

{$else}

{$ifndef WITH_RAISEPROC}
// "regular" exception handling as defined in System.pas
type
  PExceptionRecord = ^TExceptionRecord;
  TExceptionRecord = record
    ExceptionCode: DWord;
    ExceptionFlags: DWord;
    OuterException: PExceptionRecord;
    ExceptionAddress: PtrUInt;
    NumberParameters: Longint;
    case {IsOsException:} Boolean of
    True:  (ExceptionInformation : array [0..14] of PtrUInt);
    False: (ExceptAddr: PtrUInt; ExceptObject: Exception);
  end;
  GetExceptionClass = function(const P: TExceptionRecord): ExceptClass;

const
  cDelphiExcept = $0EEDFAE0;
  cDelphiException = {$ifdef FPC}$E0465043{$else}$0EEDFADE{$endif};
{$endif WITH_RAISEPROC}
{$endif WITH_MAPPED_EXCEPTIONS}

{$ifdef MSWINDOWS}
const
  // see http://msdn.microsoft.com/en-us/library/xcb2z8hs
  cSetThreadNameException = $406D1388;

  DOTNET_EXCEPTIONNAME: array[0..83] of RawUTF8 = (
  'Access', 'AmbiguousMatch', 'appdomainUnloaded', 'Application', 'Argument',
  'ArgumentNull', 'ArgumentOutOfRange', 'Arithmetic', 'ArrayTypeMismatch',
  'BadImageFormat', 'CannotUnloadappdomain', 'ContextMarshal', 'Cryptographic',
  'CryptographicUnexpectedOperation', 'CustomAttributeFormat', 'DirectoryNotFound',
  'DirectoryNotFound', 'DivideByZero', 'DllNotFound', 'DuplicateWaitObject',
  'EndOfStream', 'EntryPointNotFound', '', 'ExecutionEngine', 'External',
  'FieldAccess', 'FileLoad', 'FileLoad', 'FileNotFound', 'Format',
  'IndexOutOfRange', 'InvalidCast', 'InvalidComObject', 'InvalidFilterCriteria',
  'InvalidOleVariantType', 'InvalidOperation', 'InvalidProgram', 'IO',
  'IsolatedStorage', 'MarshalDirective', 'MethodAccess', 'MissingField',
  'MissingManifestResource', 'MissingMember', 'MissingMethod',
  'MulticastNotSupported', 'NotFiniteNumber', 'NotImplemented', 'NotSupported',
  'NullReference', 'OutOfMemory', 'Overflow', 'PlatformNotSupported', 'Policy',
  'Rank', 'ReflectionTypeLoad', 'Remoting', 'RemotingTimeout', 'SafeArrayTypeMismatch',
  'SafeArrayRankMismatch', 'Security', 'SEH', 'Serialization', 'Server', 'StackOverflow',
  'SUDSGenerator', 'SUDSParser', 'SynchronizationLock', 'System', 'Target',
  'TargetInvocation', 'TargetParameterCount', 'ThreadAbort', 'ThreadInterrupted',
  'ThreadState', 'ThreadStop', 'TypeInitialization', 'TypeLoad', 'TypeUnloaded',
  'UnauthorizedAccess', 'InClassConstructor', 'KeyNotFound', 'InsufficientStack',
  'InsufficientMemory');
  // http://blogs.msdn.com/b/yizhang/archive/2010/12/17/interpreting-hresults-returned-from-net-clr-0x8013xxxx.aspx
  DOTNET_EXCEPTIONHRESULT: array[0..83] of cardinal = (
   $8013151A, $8000211D, $80131015, $80131600, $80070057, $80004003, $80131502,
   $80070216, $80131503, $8007000B, $80131015, $80090020, $80004001, $80131431,
   $80131537, $80070003, $80030003, $80020012, $80131524, $80131529, $801338,
   $80131522, $80131500, $80131506, $80004005, $80131507, $80131621, $80131018,
   $80070002, $80131537, $80131508, $80004002, $80131527, $80131601, $80131531,
   $80131509, $8013153A, $80131620, $80131450, $80131535, $80131510, $80131511,
   $80131532, $80131512, $80131513, $80131514, $80131528, $80004001, $80131515,
   $80004003, $8007000E, $80131516, $80131539, $80131416, $80131517,
   $80131602, $8013150B, $8013150B, $80131533, $80131538, $8013150A, $80004005,
   $8013150C, $8013150E, $800703E9, $80131500, $80131500, $80131518, $80131501,
   $80131603, $80131604, $80138002, $80131530, $80131519, $80131520, $80131521,
   $80131534, $80131522, $80131013, $80070005, $80131543, $80131577, $80131578,
   $8013153D);

type
  // avoid linking of ComObj.pas just for EOleSysError
  EOleSysError = class(Exception)
  public
    ErrorCode: cardinal;
  end;

function ExceptionInheritsFrom(E: TClass; const Name: ShortString): boolean;
begin // avoid linking of ComObj.pas just for EOleSysError
  while (E<>nil) and (E<>Exception) do
    if IdemPropName(PShortString(PPointer(PtrInt(E)+vmtClassName)^)^,Name) then begin
      result := true;
      exit;
    end else
      E := GetClassParent(E);
  result := false;
end;

{$endif MSWINDOWS}

function InternalDefaultSynLogExceptionToStr(
  WR: TTextWriter; const Context: TSynLogExceptionContext): boolean;
{$ifdef MSWINDOWS}
var i: integer;
    code: cardinal;
{$endif}
begin
  WR.AddClassName(Context.EClass);
  if (Context.ELevel=sllException) and (Context.EInstance<>nil) and
     (Context.EClass<>EExternalException) then begin
    {$ifdef MSWINDOWS}
    if ExceptionInheritsFrom(Context.EClass,'EOleSysError') then begin
      WR.Add(' ');
      code := EOleSysError(Context.EInstance).ErrorCode;
      WR.AddPointer(code);
      for i := 0 to high(DOTNET_EXCEPTIONHRESULT) do
        if DOTNET_EXCEPTIONHRESULT[i]=code then begin
          WR.AddShort(' [.NET/CLR unhandled ');
          WR.AddString(DOTNET_EXCEPTIONNAME[i]);
          WR.AddShort('Exception]');
        end; // no break on purpose, if ErrorCode matches more than one Exception
    end;
    {$endif}
    WR.Add(' ');
    if (WR.ClassType=TTextWriter) or
       not Context.EInstance.InheritsFrom(ESynException) then begin
      WR.AddShort('("');
      WR.AddJSONEscapeString(Context.EInstance.Message);
      WR.AddShort('")');
    end else
       WR.WriteObject(Context.EInstance);
  end else
  if Context.ECode<>0 then begin
    WR.AddShort(' (');
    WR.AddPointer(Context.ECode);
    WR.AddShort(')');
  end;
  result := false; // caller should append "at EAddr" and the stack trace
end;

{$ifdef WITH_RAISEPROC}
var
  OldRaiseProc : TExceptProc;

procedure SynRaiseProc(Obj: TObject; Addr: CodePointer; FrameCount: Longint; Frame: PCodePointer);
var Ctxt: TSynLogExceptionContext;
    LastError: DWORD;
begin
  if GlobalCurrentHandleExceptionSynLog<>nil then
    if (Obj<>nil) and (Obj.InheritsFrom(Exception)) then begin
      LastError := GetLastError;
      Ctxt.EClass := PPointer(Obj)^;
      Ctxt.EInstance := Exception(Obj);
      Ctxt.EAddr := PtrUInt(Addr);
      if Obj.InheritsFrom(EExternal) then
        Ctxt.ELevel := sllExceptionOS else
        Ctxt.ELevel := sllException;
      Ctxt.ETimestamp := UnixTimeUTC;
      Ctxt.EStack := pointer(Frame);
      Ctxt.EStackCount := FrameCount;
      SynLogException(Ctxt);
      SetLastError(LastError); // SynLogException() above may have changed this
    end;
  if Assigned(OldRaiseProc) then
    OldRaiseProc(Obj, Addr, FrameCount, Frame);
end;
{$else}
{$ifndef WITH_PATCHEXCEPT}
{$ifndef WITH_MAPPED_EXCEPTIONS}

procedure LogExcept(stack: PPtrUInt; const Exc: TExceptionRecord);
var Ctxt: TSynLogExceptionContext;
    LastError: DWORD;
begin
  {$ifdef MSWINDOWS}
  if Exc.ExceptionCode=cSetThreadNameException then
    exit;
  {$endif}
  LastError := GetLastError;
  Ctxt.ECode := Exc.ExceptionCode;
  if (Exc.ExceptionCode=cDelphiException) and (Exc.ExceptObject<>nil) then begin
    if Exc.ExceptObject.InheritsFrom(Exception) then
      Ctxt.EClass := PPointer(Exc.ExceptObject)^ else
      Ctxt.EClass := EExternalException;
    Ctxt.EInstance := Exc.ExceptObject;
    Ctxt.ELevel := sllException;
    Ctxt.EAddr := Exc.ExceptAddr;
  end else begin
    {$ifdef MSWINDOWS}
    if Assigned(ExceptClsProc) then
      Ctxt.EClass := GetExceptionClass(ExceptClsProc)(Exc) else
    {$endif}
      Ctxt.EClass := EExternal;
    Ctxt.EInstance := nil;
    Ctxt.ELevel := sllExceptionOS;
    Ctxt.EAddr := Exc.ExceptionAddress;
  end;
  Ctxt.EStack := stack;
  Ctxt.EStackCount := 0;
  Ctxt.ETimestamp := UnixTimeUTC; // fast API call
  SynLogException(Ctxt);
  SetLastError(LastError); // code above could have changed this
end;

{$ifdef WITH_VECTOREXCEPT}

type
  PExceptionInfo = ^TExceptionInfo;
  TExceptionInfo = packed record
    ExceptionRecord: PExceptionRecord;
    ContextRecord: pointer;
  end;

var
  AddVectoredExceptionHandler: function(FirstHandler: cardinal;
    VectoredHandler: pointer): PtrInt; stdcall;

function SynLogVectoredHandler(ExceptionInfo : PExceptionInfo): PtrInt; stdcall;
const EXCEPTION_CONTINUE_SEARCH = 0;
begin
  if GlobalCurrentHandleExceptionSynLog<>nil then
    LogExcept(nil,ExceptionInfo^.ExceptionRecord^);
  result := EXCEPTION_CONTINUE_SEARCH;
end;

{$else WITH_VECTOREXCEPT}

var oldUnWindProc: pointer;

procedure SynRtlUnwind(TargetFrame, TargetIp: pointer;
  ExceptionRecord: PExceptionRecord; ReturnValue: Pointer); stdcall;
asm
  pushad
  cmp  dword ptr GlobalCurrentHandleExceptionSynLog,0
  jz   @oldproc
  mov  eax,TargetFrame
  mov  edx,ExceptionRecord
  call LogExcept
@oldproc:
  popad
  pop ebp // hidden push ebp at asm level
  jmp oldUnWindProc
end;

{$endif WITH_VECTOREXCEPT}
{$endif WITH_MAPPED_EXCEPTIONS}
{$endif WITH_PATCHEXCEPT}
{$endif WITH_RAISEPROC}

{$endif NOEXCEPTIONINTERCEPT}


procedure TSynLogFamily.SetDestinationPath(const value: TFileName);
begin
  if value='' then
    fDestinationPath := ExeVersion.ProgramFilePath else
    fDestinationPath := IncludeTrailingPathDelimiter(value);
end;

procedure TSynLogFamily.SetLevel(aLevel: TSynLogInfos);
begin
  // ensure BOTH Enter+Leave are always selected at once, if any is set
  if sllEnter in aLevel then
    include(aLevel,sllLeave) else
  if sllLeave in aLevel then
    include(aLevel,sllEnter);
  fLevel := aLevel;
{$ifndef NOEXCEPTIONINTERCEPT}
  // intercept exceptions, if necessary
  fHandleExceptions := (sllExceptionOS in aLevel) or (sllException in aLevel);
  if fHandleExceptions and (GlobalCurrentHandleExceptionSynLog=nil) then begin
    SynLog; // force GlobalCurrentHandleExceptionSynLog definition
    if not GlobalCurrentHandleExceptionHooked then begin
      GlobalCurrentHandleExceptionHooked := true;
      {$ifdef WITH_MAPPED_EXCEPTIONS}
      GetUnwinder(oldUnwinder);
      newUnwinder := oldUnwinder;
      newUnwinder.RaiseException := HookedRaiseException;
      SetUnwinder(newUnwinder);
      {$else}
      {$ifdef WITH_VECTOREXCEPT}
      AddVectoredExceptionHandler :=
        GetProcAddress(GetModuleHandle(kernel32),'AddVectoredExceptionHandler');
      // RemoveVectoredContinueHandler() is available under 64 bit editions only
      if Assigned(AddVectoredExceptionHandler) then
        // available since Windows XP
        AddVectoredExceptionHandler(0,@SynLogVectoredHandler);
      {$else WITH_VECTOREXCEPT}
      {$ifdef WITH_PATCHEXCEPT}
      PatchCallRtlUnWind;
      {$else}
      {$ifdef WITH_RAISEPROC}
      OldRaiseProc := RaiseProc;
      RaiseProc := @SynRaiseProc;
      {$else}
      oldUnWindProc := RTLUnwindProc;
      RTLUnwindProc := @SynRtlUnwind;
      {$endif WITH_RAISEPROC}
      {$endif WITH_PATCHEXCEPT}
      {$endif WITH_VECTOREXCEPT}
      {$endif WITH_MAPPED_EXCEPTIONS}
    end;
  end;
{$endif NOEXCEPTIONINTERCEPT}
end;

procedure TSynLogFamily.SetEchoToConsole(aEnabled: TSynLogInfos);
begin
  if (self=nil) or (aEnabled=fEchoToConsole) then
    exit;
  fEchoToConsole := aEnabled;
end;

procedure TSynLogFamily.SetEchoToConsoleUseJournal(aValue: boolean);
begin
  if self<>nil then
    {$ifdef LINUXNOTBSD}
    if aValue and SystemdIsAvailable then
      fEchoToConsoleUseJournal := true else
    {$endif}
      fEchoToConsoleUseJournal := false;
end;

function TSynLogFamily.GetSynLogClassName: string;
begin
  if self=nil then
    result := '' else
    result := ClassName;
end;

constructor TSynLogFamily.Create(aSynLog: TSynLogClass);
begin
  fSynLogClass := aSynLog;
  fIdent := SynLogFamily.Add(self);
  fDestinationPath := ExeVersion.ProgramFilePath; // use .exe path
  fDefaultExtension := '.log';
  fArchivePath := fDestinationPath;
  fArchiveAfterDays := 7;
  fRotateFileAtHour := -1;
  fBufferSize := 4096;
  fStackTraceLevel := 30;
  fWithUnitName := true;
  fWithInstancePointer := true;
  {$ifndef FPC}
  if DebugHook<>0 then // never let stManualAndAPI trigger AV within the IDE
    fStackTraceUse := stOnlyAPI;
  {$endif}
  fExceptionIgnore := TList.Create;
  fLevelStackTrace := [sllStackTrace,sllException,sllExceptionOS
    {$ifndef FPC},sllError,sllFail,sllLastError,sllDDDError{$endif}];
end;

function TSynLogFamily.GetExceptionIgnoreCurrentThread: boolean;
begin
  result := ExceptionIgnorePerThread;
end;

procedure TSynLogFamily.SetExceptionIgnoreCurrentThread(
  aExceptionIgnoreCurrentThread: boolean);
begin
  ExceptionIgnorePerThread := aExceptionIgnoreCurrentThread;
end;

function TSynLogFamily.CreateSynLog: TSynLog;
var i: integer;
begin
  SynLogFileList.Safe.Lock;
  try
    result := fSynLogClass.Create(self);
    i := SynLogFileList.Add(result);
    if fPerThreadLog=ptOneFilePerThread then
      if (fRotateFileCount=0) and (fRotateFileSize=0) and (fRotateFileAtHour<0) then
        SynLogFileIndexThreadVar[fIdent] := i+1 else begin
        fPerThreadLog := ptIdentifiedInOnFile; // excluded by rotation
        fGlobalLog := result;
      end else
      fGlobalLog := result;
  finally
    SynLogFileList.Safe.UnLock;
  end;
end;

var
  AutoFlushSecondElapsed: cardinal;

{$ifdef MSWINDOWS}
  {$define AUTOFLUSHRAWWIN}
  // if defined, will use direct Windows API calls
{$endif}

{$ifdef AUTOFLUSHRAWWIN}
var
  AutoFlushThread: pointer;

procedure AutoFlushProc(P: pointer); stdcall;
  function Terminated: boolean;
  begin
    result :=  AutoFlushThread=nil;
  end;
{$else}
type
  // cross-platform / cross-compiler TThread-based flush
  TAutoFlushThread = class(TThread)
  protected
    fEvent: TEvent;
    procedure Execute; override;
  public
    constructor Create; reintroduce;
    destructor Destroy; override;
  end;

var
  AutoFlushThread: TAutoFlushThread;

constructor TAutoFlushThread.Create;
begin
  fEvent := TEvent.Create(nil,false,false,'');
  inherited Create(false);
end;

destructor TAutoFlushThread.Destroy;
begin
  inherited Destroy;
  fEvent.Free;
end;

procedure TAutoFlushThread.Execute;
{$endif}
var i: integer;
    files: TSynObjectListLocked;
begin
  SetThreadNameDefault(GetCurrentThreadID,'SynLog AutoFlushProc');
  try
    repeat
      {$ifdef AUTOFLUSHRAWWIN} // check every second for pending data
      for i := 1 to 10 do begin
        SleepHiRes(100);
        if Terminated then
          exit; // avoid GPF
      end;
      {$else}
      FixedWaitFor(fEvent,1000);
      if Terminated then
        exit;
      {$endif}
      files := SynLogFileList;
      if files.Count=0 then
        continue; // nothing to flush
      inc(AutoFlushSecondElapsed);
      files.Safe.Lock;
      try
        for i := 0 to files.Count-1 do
        with TSynLog(files.List[i]) do
          if Terminated then
            break else // avoid GPF
          if (fFamily.fAutoFlush<>0) and (fWriter<>nil) and (fWriter.PendingBytes>1) and
             (AutoFlushSecondElapsed mod fFamily.fAutoFlush=0) then begin
              {$ifdef AUTOFLUSHRAWWIN}
              if not IsMultiThread then
                if not fWriterStream.InheritsFrom(TFileStream) then
                  IsMultiThread := true; // only TFileStream is thread-safe
              {$endif}
              Flush(false); // write pending data
            end;
       finally
         files.Safe.UnLock;
       end;
    until Terminated;
  finally
    {$ifdef AUTOFLUSHRAWWIN}
    ExitThread(0);
    {$endif}
  end;
end;

procedure TSynLogFamily.StartAutoFlush;
{$ifdef AUTOFLUSHRAWWIN}var ID: cardinal;{$endif}
begin
  if (AutoFlushThread=nil) and (fAutoFlush<>0)
     {$ifndef FPC}and (DebugHook=0){$endif} then begin
    AutoFlushSecondElapsed := 0;
    {$ifdef AUTOFLUSHRAWWIN}
    AutoFlushThread := pointer(CreateThread(nil,0,@AutoFlushProc,nil,0,ID));
    {$else}
    AutoFlushThread := TAutoFlushThread.Create;
    {$endif}
  end;
end;

destructor TSynLogFamily.Destroy;
var SR: TSearchRec;
    oldTime,aTime: TDateTime;
    Y,M,D: word;
    aOldLogFileName, aPath: TFileName;
    tmp: array[0..7] of AnsiChar;
begin
  fDestroying := true;
  EchoRemoteStop;
  if AutoFlushThread<>nil then begin
    {$ifdef AUTOFLUSHRAWWIN}
    AutoFlushThread := nil; // Terminated=true to avoid GPF in AutoFlushProc
    {$else}
    AutoFlushThread.Terminate;
    AutoFlushThread.fEvent.SetEvent; // notify TAutoFlushThread.Execute
    FreeAndNil(AutoFlushThread); // wait for the TThread to be terminated
    {$endif}
  end;
  ExceptionIgnore.Free;
  try
    if Assigned(OnArchive) then
    if FindFirst(DestinationPath+'*'+DefaultExtension,faAnyFile,SR)=0 then
    try
      if ArchiveAfterDays<0 then
        ArchiveAfterDays := 0;
      oldTime := Now-ArchiveAfterDays;
      repeat
        if (SR.Name[1]='.') or (faDirectory and SR.Attr<>0) then
          continue;
        aTime := SearchRecToDateTime(SR);
        if (aTime=0) or (aTime>oldTime) then
          continue;
        aOldLogFileName := DestinationPath+SR.Name;
        if aPath='' then begin
          aPath := EnsureDirectoryExists(ArchivePath+'log');
          if aPath='' then
            break; // impossible to create the archive folder
          DecodeDate(aTime,Y,M,D);
          YearToPChar(Y,@tmp[0]);
          PWord(@tmp[4])^ := TwoDigitLookupW[M];
          PWord(@tmp[6])^ := ord(PathDelim);
          aPath := aPath+Ansi7ToString(tmp,7);
        end;
        OnArchive(aOldLogFileName,aPath);
      until FindNext(SR)<>0;
    finally
      try
        OnArchive('',aPath); // mark no more .log file to archive -> close .zip
      finally
        FindClose(SR);
      end;
    end;
  finally
    {$ifdef AUTOFLUSHRAWWIN} // release background thread once for all
    if AutoFlushThread<>nil then
      CloseHandle(THandle(AutoFlushThread));
    {$endif}
    inherited Destroy;
  end;
end;

function TSynLogFamily.SynLog: TSynLog;
var ndx: integer;
begin
  if self<>nil then begin
    if (fPerThreadLog=ptOneFilePerThread) and (fRotateFileCount=0) and
       (fRotateFileSize=0) and (fRotateFileAtHour<0) then begin
      ndx := SynLogFileIndexThreadVar[fIdent]-1;
      if ndx>=0 then // SynLogFileList.Safe.Lock/Unlock is not mandatory here
        result := SynLogFileList.List[ndx] else
        result := CreateSynLog;
    end else // for ptMergedInOneFile and ptIdentifiedInOnFile
      if fGlobalLog<>nil then
        result := fGlobalLog else
        result := CreateSynLog;
    {$ifndef NOEXCEPTIONINTERCEPT}
    if fHandleExceptions and (GlobalCurrentHandleExceptionSynLog<>result) then
      GlobalCurrentHandleExceptionSynLog := result;
    {$endif}
  end else
    result := nil;
end;

procedure TSynLogFamily.SynLogFileListEcho(const aEvent: TOnTextWriterEcho;
  aEventAdd: boolean);
var i: integer;
    files: TSynObjectListLocked;
    f: TSynLog;
begin
  if (self=nil) or (SynLogFileList.Count=0) or not Assigned(aEvent) then
    exit;
  files := SynLogFileList;
  files.Safe.Lock;
  try
    for i := 0 to files.Count-1 do begin
      f := files.List[i];
      if f.fFamily=self then
        if aEventAdd then
          f.fWriter.EchoAdd(aEvent) else
          f.fWriter.EchoRemove(aEvent);
    end;
  finally
    files.Safe.UnLock;
  end;
end;

procedure TSynLogFamily.SetEchoCustom(const aEvent: TOnTextWriterEcho);
begin
  if self=nil then
    exit;
  SynLogFileListEcho(fEchoCustom,false); // unsubscribe any previous
  fEchoCustom := aEvent;
  SynLogFileListEcho(aEvent,true); // subscribe new
end;

procedure TSynLogFamily.EchoRemoteStart(aClient: TObject;
  const aClientEvent: TOnTextWriterEcho; aClientOwnedByFamily: boolean);
begin
  EchoRemoteStop;
  fEchoRemoteClient := aClient;
  fEchoRemoteEvent := aClientEvent;
  fEchoRemoteClientOwned := aClientOwnedByFamily;
  SynLogFileListEcho(fEchoRemoteEvent,true); // subscribe
end;

procedure TSynLogFamily.EchoRemoteStop;
begin
  if fEchoRemoteClient=nil then
    exit;
  if fEchoRemoteClientOwned then
    try
      try
        fEchoRemoteEvent(nil,sllClient,
          FormatUTF8('%00%    Remote Client % Disconnected',
            [NowToString(false),LOG_LEVEL_TEXT[sllClient],self]));
      finally
        fEchoRemoteClient.Free;
      end;
    except
      on Exception do ;
    end;
  fEchoRemoteClient := nil;
  SynLogFileListEcho(fEchoRemoteEvent,false); // unsubscribe
  fEchoRemoteEvent := nil;
end;

function TSynLogFamily.GetExistingLog(MaximumKB: cardinal): RawUTF8;
const MAXPREVIOUSCONTENTSIZE = 1024*1024*128; // a 128 MB RawUTF8 is fair enough
var log: TSynLog;
    stream: TFileStream;
    endpos,start: Int64;
    c: AnsiChar;
    i,len,read,total: integer;
    P: PAnsiChar;
begin
  result := '';
  if SynLogFileList.Count<>0 then begin
    SynLogFileList.Safe.Lock;
    try
      for i := 0 to SynLogFileList.Count-1 do begin
        log := SynLogFileList.List[i];
        if log.fFamily<>self then
          continue;
        EnterCriticalSection(GlobalThreadLock);
        try
          log.Writer.FlushToStream;
          if log.Writer.Stream.InheritsFrom(TFileStream) then begin
            stream := TFileStream(log.Writer.Stream);
            endpos := stream.Position;
            try
              if endpos>MAXPREVIOUSCONTENTSIZE then
                len := MAXPREVIOUSCONTENTSIZE else
                len := MaximumKB shl 10;
              start := log.fStreamPositionAfterHeader;
              if (len<>0) and (endpos-start>len) then begin
                start := endpos-len;
                stream.Position := start;
                repeat
                  inc(start)
                until (stream.Read(c,1)=0) or (ord(c) in [10,13]);
              end else
                stream.Position := start;
              len := endpos-start;
              SetLength(result,len);
              P := pointer(result);
              total := 0;
              repeat
                read := stream.Read(P^,len);
                if read<=0 then begin
                  if total<>len then
                    SetLength(result,total); // truncate on read error
                  break;
                end;
                inc(P,read);
                dec(len,read);
                inc(total,read);
              until len=0;
            finally
              stream.Position := endpos;
            end;
          end;
        finally
          LeaveCriticalSection(GlobalThreadLock);
        end;
        break;
      end;
    finally
      SynLogFileList.Safe.UnLock;
    end;
  end;

end;

procedure TSynLogFamily.OnThreadEnded(Sender: TThread);
begin
  SynLog.NotifyThreadEnded;
end;


{ TFileStreamWithoutWriteError }

function TFileStreamWithoutWriteError.Write(const Buffer; Count: Longint): Longint;

begin
  inherited Write(Buffer,Count);
  result := Count; // ignore I/O errors
end;


{ TSynLog }

{$ifdef HASINLINENOTX86}
class function TSynLog.Family: TSynLogFamily;
begin
  result := pointer(Self);
  if result<>nil then begin
    result := PPointer(PtrInt(PtrUInt(result))+vmtAutoTable)^;
    if result=nil then
      result := FamilyCreate;
  end;
end;

class function TSynLog.Add: TSynLog;
begin
  result := pointer(Self);
  if result<>nil then begin // inlined TSynLog.Family (Add is already inlined)
    result := PPointer(PtrInt(PtrUInt(result))+vmtAutoTable)^;
    if result=nil then
      TSynLogFamily(pointer(result)) := FamilyCreate;
    result := TSynLogFamily(pointer(result)).SynLog;
  end;
end;
{$else}
class function TSynLog.Add: TSynLog;
asm
  push offset TSynLogFamily.SynLog
  jmp TSynLog.Family
end;

class function TSynLog.Family: TSynLogFamily;
asm
  or eax,eax
  jz @null
  mov edx,[eax+vmtAutoTable]
  or edx,edx
  jz FamilyCreate
  mov eax,edx
@null:
end;
{$endif}

procedure TSynLog.LockAndGetThreadContext;
var id: TThreadID;
begin
  EnterCriticalSection(GlobalThreadLock);
  id := GetCurrentThreadId;
  if id<>fThreadID then begin
    fThreadID := id;
    GetThreadContextInternal;
  end;
  {$ifndef NOEXCEPTIONINTERCEPT} // for IsBadCodePtr() or any internal exception
  fThreadHandleExceptionBackup := GlobalCurrentHandleExceptionSynLog;
  GlobalCurrentHandleExceptionSynLog := nil;
  {$endif}
end;

procedure TSynLog.LogTrailerUnLock(Level: TSynLogInfo);
begin
  try
    if Level in fFamily.fLevelStackTrace then
      AddStackTrace(nil);
    fWriter.AddEndOfLine(fCurrentLevel);
    if (fFileRotationNextHour<>0) and (GetTickCount64>=fFileRotationNextHour) then begin
      inc(fFileRotationNextHour,MSecsPerDay);
      PerformRotation;
    end else
    if (fFileRotationSize>0) and (fWriter.WrittenBytes>fFileRotationSize) then
      PerformRotation;
  finally
    {$ifndef NOEXCEPTIONINTERCEPT}
    GlobalCurrentHandleExceptionSynLog := fThreadHandleExceptionBackup;
    {$endif}
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

const
  // would handle up to 4096 threads, using 8 KB of RAM for the hash table
  MAXLOGTHREADBITS = 12;
  // maximum of thread IDs which can exist for a process
  // - shall be a power of 2 (used for internal TSynLog.fThreadHash)
  // - with the default 1MB stack size, max is around 2000 threads for Win32
  // - thread IDs are recycled when released via TSynLog.NotifyThreadEnded
  MAXLOGTHREAD = 1 shl MAXLOGTHREADBITS;

procedure TSynLog.GetThreadContextInternal;
var secondpass: boolean;
    id, hash: PtrUInt;
begin // should match TSynLog.ThreadContextRehash
  if fFamily.fPerThreadLog<>ptNoThreadProcess then begin
    secondpass := false;
    id := PtrUInt(fThreadID); // TThreadID  = ^TThreadRec under BSD
    hash := 0; // efficient TThreadID hash on all architectures
    repeat
      hash := hash xor (id and (MAXLOGTHREAD-1));
      id := id shr (MAXLOGTHREADBITS-1); // -1 for less collisions under Linux
    until id=0;
    fThreadIndex := fThreadHash[hash];
    fThreadLastHash := hash;
    if fThreadIndex<>0 then
      repeat
        fThreadContext := @fThreadContexts[fThreadIndex-1];
        if fThreadContext^.ID=fThreadID then // match found
          exit;
        // hash collision -> try next item in fThreadHash[] if possible
        if fThreadLastHash=MAXLOGTHREAD-1 then
          if secondpass then // avoid endless loop -> reuse last fThreadHash[]
            exit else begin
            fThreadLastHash := 0;
            secondpass := true;
          end else
          inc(fThreadLastHash);
        fThreadIndex := fThreadHash[fThreadLastHash];
      until fThreadIndex=0;
    // here we know that fThreadIndex=fThreadHash[hash]=0 -> register the thread
    if fThreadIndexReleasedCount>0 then begin // reuse NotifyThreadEnded() index
      dec(fThreadIndexReleasedCount);
      fThreadIndex := fThreadIndexReleased[fThreadIndexReleasedCount];
    end else begin // store a new entry
      if fThreadContextCount>=length(fThreadContexts) then
        SetLength(fThreadContexts,fThreadContextCount+128);
      inc(fThreadContextCount);
      fThreadIndex := fThreadContextCount;
    end;
    fThreadHash[fThreadLastHash] := fThreadIndex;
  end else
    fThreadIndex := 1;
  fThreadContext := @fThreadContexts[fThreadIndex-1];
  fThreadContext^.ID := fThreadID;
end;

function TSynLog.NewRecursion: PSynLogThreadRecursion;
begin
  with fThreadContext^ do begin
    if RecursionCount = RecursionCapacity then begin
      RecursionCapacity := NextGrow(RecursionCapacity);
      SetLength(Recursion, RecursionCapacity);
    end;
    result := @Recursion[RecursionCount];
    result^.Caller := 0; // no stack trace by default
    result^.RefCount := 0;
    inc(RecursionCount);
  end;
end;

procedure TSynLog.ThreadContextRehash;
var i: integer;
    id, hash: PtrUInt;
    secondpass: boolean;
    ctxt: PSynLogThreadContext;
begin // should match TSynLog.GetThreadContextInternal
  if fFamily.fPerThreadLog=ptNoThreadProcess then
    exit;
  FillcharFast(fThreadHash[0],MAXLOGTHREAD*sizeof(fThreadHash[0]),0);
  ctxt := pointer(fThreadContexts);
  for i := 1 to fThreadContextCount do begin
    id := PtrUInt(ctxt^.ID); // TThreadID  = ^TThreadRec under BSD
    if id<>0 then begin // not empty slot
      hash := 0; // efficient TThreadID hash on all architectures
      repeat
        hash := hash xor (id and (MAXLOGTHREAD-1));
        id := id shr (MAXLOGTHREADBITS-1); // -1 for less collisions under Linux
      until id=0;
      secondpass := false;
      repeat
        if fThreadHash[hash]=0 then
          break;
        // hash collision (no need to check the ID here)
        if hash=MAXLOGTHREAD-1 then
          if secondpass then // avoid endless loop
            break else begin
            hash := 0;
            secondpass := true;
          end else
          inc(hash);
      until false;
      fThreadHash[hash] := i;
    end;
    inc(ctxt);
  end;
end;

procedure TSynLog.NotifyThreadEnded;
begin
  if (self=nil) or (fThreadContextCount=0) then
    exit; // nothing to release
  LockAndGetThreadContext;
  try
    Finalize(fThreadContext^);
    FillcharFast(fThreadContext^,SizeOf(fThreadContext^),0);
    ThreadContextRehash; // fThreadHash[fThreadLastHash] := 0 is not enough
    if fThreadIndexReleasedCount>=length(fThreadIndexReleased) then
      SetLength(fThreadIndexReleased,fThreadIndexReleasedCount+128);
    fThreadIndexReleased[fThreadIndexReleasedCount] := fThreadIndex;
    inc(fThreadIndexReleasedCount); // allow naive but very efficient reuse
  finally
    {$ifndef NOEXCEPTIONINTERCEPT}
    GlobalCurrentHandleExceptionSynLog := fThreadHandleExceptionBackup;
    {$endif}
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

function TSynLog._AddRef: {$ifdef FPC}longint{$else}integer{$endif};
begin
  if fFamily.Level*[sllEnter,sllLeave]<>[] then
  try
    LockAndGetThreadContext;
    with fThreadContext^ do
    if RecursionCount>0 then
      with Recursion[RecursionCount-1] do begin
        if (RefCount=0) and (sllEnter in fFamily.Level) then begin
          LogHeaderLock(sllEnter,true);
          AddRecursion(RecursionCount-1,sllEnter);
        end;
        inc(RefCount);
        result := RefCount;
      end else
      result := 1; // should never be 0 (would release of TSynLog instance)
  finally
    {$ifndef NOEXCEPTIONINTERCEPT}
    GlobalCurrentHandleExceptionSynLog := fThreadHandleExceptionBackup;
    {$endif}
    LeaveCriticalSection(GlobalThreadLock);
  end else
    result := 1;
end;

{$ifdef MSWINDOWS}
var
  RtlCaptureStackBackTraceRetrieved: (btUntested, btOK, btFailed) = btUntested;
  RtlCaptureStackBackTrace: function(FramesToSkip, FramesToCapture: cardinal;
    BackTrace, BackTraceHash: pointer): byte; stdcall;
{$endif}

{$STACKFRAMES ON}
function TSynLog._Release: {$ifdef FPC}longint{$else}integer{$endif};
{$ifndef CPU64}
{$ifndef PUREPASCAL}
var aStackFrame: PtrInt;
{$endif}
{$endif}
begin
  if fFamily.Level*[sllEnter,sllLeave]<>[] then
  try
    LockAndGetThreadContext;
    with fThreadContext^ do
    if RecursionCount>0 then begin
      with Recursion[RecursionCount-1] do begin
        dec(RefCount);
        if RefCount=0 then begin
          if sllLeave in fFamily.Level then begin
            if MethodName=nil then begin
              {$ifdef CPU64}
              {$ifdef MSWINDOWS}
              if RtlCaptureStackBackTrace(1,1,@Caller,nil)=0 then
                Caller := 0 else
                dec(Caller,5); // ignore caller op codes
              {$else}
              Caller := 0; // no stack trace yet under Linux64
              {$endif}
              {$else}
              {$ifdef PUREPASCAL}
              Caller := 0; // e.g. ARM Linux
              {$else}
              asm
                mov eax,[ebp+16] // +4->_IntfClear +16->initial caller
                mov aStackFrame,eax
              end;
              Caller := aStackFrame-5;
              {$endif}
              {$endif}
            end;
            LogHeaderLock(sllLeave,true);
            AddRecursion(RecursionCount-1,sllLeave);
          end;
          dec(RecursionCount);
        end;
        result := RefCount;
      end;
    end else
      result := 1; // should never be 0 (would release TSynLog instance)
  finally
    {$ifndef NOEXCEPTIONINTERCEPT}
    GlobalCurrentHandleExceptionSynLog := fThreadHandleExceptionBackup;
    {$endif}
    LeaveCriticalSection(GlobalThreadLock);
  end else
    result := 1;
end;
{$STACKFRAMES OFF}

constructor TSynLog.Create(aFamily: TSynLogFamily);
begin
  if aFamily=nil then
    aFamily := Family;
  fFamily := aFamily;
  {$ifdef MSWINDOWS}
  if RtlCaptureStackBackTraceRetrieved=btUntested then begin
    if OSVersion<wXP then
      RtlCaptureStackBackTraceRetrieved := btFailed else begin
     @RtlCaptureStackBackTrace := GetProcAddress(
       GetModuleHandle(kernel32),'RtlCaptureStackBackTrace');
     if @RtlCaptureStackBackTrace=nil then
       RtlCaptureStackBackTraceRetrieved := btFailed else
       RtlCaptureStackBackTraceRetrieved := btOK;
    end;
  end;
  {$ifdef CPU64}
  assert(RtlCaptureStackBackTraceRetrieved=btOK);
  {$endif}
  {$endif}
  SetLength(fThreadHash,MAXLOGTHREAD); // 8 KB buffer
  SetLength(fThreadContexts,128);
end;

destructor TSynLog.Destroy;
begin
{$ifndef NOEXCEPTIONINTERCEPT}
  if fFamily.fHandleExceptions and (GlobalCurrentHandleExceptionSynLog=self) then
    GlobalCurrentHandleExceptionSynLog := nil;
{$endif}
  Flush(true);
  fWriterStream.Free;
  fWriter.Free;
  inherited;
end;

procedure TSynLog.CloseLogFile;
begin
  if fWriter=nil then
    exit;
  EnterCriticalSection(GlobalThreadLock);
  try
    fWriter.FlushFinal;
    FreeAndNil(fWriterStream);
    FreeAndNil(fWriter);
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

procedure TSynLog.Release;
begin
  SynLogFileList.Safe.Lock;
  try
    CloseLogFile;
    SynLogFileList.Remove(self);
    if fFamily.fPerThreadLog=ptOneFilePerThread then
      SynLogFileIndexThreadVar[fFamily.fIdent] := 0;
  finally
    SynLogFileList.Safe.UnLock;
  end;
  Free;
end;

procedure TSynLog.Flush(ForceDiskWrite: boolean);
begin
  if fWriter=nil then
    exit;
  EnterCriticalSection(GlobalThreadLock);
  try
    fWriter.FlushToStream;
    if ForceDiskWrite and fWriterStream.InheritsFrom(TFileStream) then
      FlushFileBuffers(TFileStream(fWriterStream).Handle);
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

function TSynLog.QueryInterface(
{$ifdef FPC}
  {$IFDEF FPC_HAS_CONSTREF}constref{$ELSE}const{$ENDIF} IID: TGUID; out Obj): longint;
{$else}
  const IID: TGUID; out Obj): HResult;
{$endif}
begin
  Result := E_NOINTERFACE;
end;

{$STACKFRAMES ON}

class function TSynLog.Enter(aInstance: TObject; aMethodName: PUTF8Char;
  aMethodNameLocal: boolean): ISynLog;
var aSynLog: TSynLog;
    aStackFrame: PtrUInt;
begin
  // inlined aSynLog := Family.Add
  aSynLog := pointer(self);
  if aSynLog=nil then begin
    result := nil;
    exit;
  end;
  aSynLog := PPointer(PtrInt(PtrUInt(aSynLog))+vmtAutoTable)^;
  if aSynLog=nil then
    TSynLogFamily(pointer(aSynLog)) := FamilyCreate;
  aSynLog := TSynLogFamily(pointer(aSynLog)).SynLog;
  // recursively store parameters
  if sllEnter in aSynLog.fFamily.fLevel then begin
    aSynLog.LockAndGetThreadContext;
    with aSynLog.NewRecursion^ do
    try
      {$ifdef CPU64}
      {$ifdef MSWINDOWS}
      if RtlCaptureStackBackTrace(1,1,@aStackFrame,nil)=0 then
        aStackFrame := 0 else
        dec(aStackFrame,5); // ignore call TSynLog.Enter op codes
      {$else}
      aStackFrame := 0; // No stack trace yet under Linux64
      { TODO : use cross-platform get_caller_addr(get_frame) under FPC }
      {$endif}
      {$else}
      {$ifdef PUREPASCAL}
      aStackFrame := 0; // e.g. ARM Linux
      {$else}
      asm
        mov eax,[ebp+4]  // retrieve caller EIP from push ebp; mov ebp,esp
        sub eax,5        // ignore call TSynLog.Enter op codes
        mov aStackFrame,eax
      end;
      {$endif}
      {$endif}
      Instance := aInstance;
      MethodName := aMethodName;
      if aMethodNameLocal then
        MethodNameLocal := mnEnter else
        MethodNameLocal := mnAlways;
      Caller := aStackFrame;
    finally
      {$ifndef NOEXCEPTIONINTERCEPT}
      GlobalCurrentHandleExceptionSynLog := aSynLog.fThreadHandleExceptionBackup;
      {$endif}
      LeaveCriticalSection(GlobalThreadLock);
    end;
  end;
  // copy to ISynLog interface -> will call TSynLog._AddRef
  result := aSynLog;
end;
{$STACKFRAMES OFF}

class function TSynLog.Enter(const TextFmt: RawUTF8; const TextArgs: array of const;
  aInstance: TObject=nil): ISynLog;
var aSynLog: TSynLog;
begin
  aSynLog := Family.SynLog;
  if (aSynLog<>nil) and (sllEnter in aSynLog.fFamily.fLevel) then begin
    aSynLog.LockAndGetThreadContext;
    with aSynLog.NewRecursion^ do
    try
      Instance := aInstance;
      MethodName := nil; // avoid GPF in RawUTF8(pointer(MethodName)) below
      FormatUTF8(TextFmt,TextArgs,RawUTF8(pointer(MethodName)));
      MethodNameLocal := mnEnterOwnMethodName;
    finally
      {$ifndef NOEXCEPTIONINTERCEPT}
      GlobalCurrentHandleExceptionSynLog := aSynLog.fThreadHandleExceptionBackup;
      {$endif}
      LeaveCriticalSection(GlobalThreadLock);
    end;
  end;
  // copy to ISynLog interface -> will call TSynLog._AddRef
  result := aSynLog;
end;

procedure TSynLog.ManualEnter(aMethodName: PUtf8Char; aInstance: TObject);
begin
  if (self = nil) or
     (fFamily.fLevel * [sllEnter, sllLeave] = []) then
    exit;
  if aMethodName = nil then
    aMethodName := ' '; // something non void (call stack is irrelevant)
  LockAndGetThreadContext;
  try
    with NewRecursion^ do begin
      // inlined TSynLog.Enter
      Instance := aInstance;
      MethodName := aMethodName;
      MethodNameLocal := mnEnter;
      // inlined TSynLog._AddRef
      if sllEnter in fFamily.Level then begin
        LogHeaderLock(sllEnter, true);
        AddRecursion(fThreadContext^.RecursionCount - 1, sllEnter);
      end;
      inc(RefCount);
    end;
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

procedure TSynLog.ManualLeave;
begin
  if self <> nil then
    _Release;
end;

class function TSynLog.FamilyCreate: TSynLogFamily;
var PVMT: pointer;
begin // private sub function makes the code faster in most case
  if not InheritsFrom(TSynLog) then
    // invalid call
    result := nil else begin
    EnterCriticalSection(GlobalThreadLock);
    try
      // TSynLogFamily instance is stored into "AutoTable" unused VMT entry
      PVMT := pointer(PtrInt(PtrUInt(self))+vmtAutoTable);
      result := PPointer(PVMT)^;
      if result=nil then begin // protect from (unlikely) concurrent call
        // create the properties information from RTTI
        result := TSynLogFamily.Create(self); // stored in SynLogFamily list
        PatchCodePtrUInt(PVMT,PtrUInt(result),{LeaveUnprotected=}true);
      end;
    finally
      LeaveCriticalSection(GlobalThreadLock);
    end;
  end;
end;

type
  TSynLogVoid = class(TSynLog);

class function TSynLog.Void: TSynLogClass;
begin
  TSynLogVoid.Family.Level := [];
  result := TSynLogVoid;
end;

function TSynLog.Instance: TSynLog;
begin
  result := self;
end;

{$I-}
function TSynLog.ConsoleEcho(Sender: TTextWriter; Level: TSynLogInfo;
  const Text: RawUTF8): boolean;
{$ifdef MSWINDOWS}
var tmp: AnsiString;
{$endif}
{$ifdef LINUXNOTBSD}
var
  tmp, mtmp: RawUTF8;
  jvec: Array[0..1] of TioVec;
{$endif}
begin
  result := true;
  if not (Level in fFamily.fEchoToConsole) then
    exit;
  {$ifdef LINUXNOTBSD}
  if Family.EchoToConsoleUseJournal then begin
    if length(Text)<18 then // should be at last "20200615 08003008  "
      exit;
    FormatUTF8('PRIORITY=%', [LOG_TO_SYSLOG[Level]],tmp);
    jvec[0].iov_base := pointer(tmp);
    jvec[0].iov_len := length(tmp);
    // skip time "20200615 08003008  ." - journal do it for us; and first space after it
    FormatUTF8('MESSAGE=%', [PUTF8Char(pointer(Text))+18],mtmp);
    jvec[1].iov_base := pointer(mtmp);
    jvec[1].iov_len := length(mtmp);
    ExternalLibraries.sd_journal_sendv(@jvec[0],2);
    exit;
  end;
  {$endif}
  TextColor(LOG_CONSOLE_COLORS[Level]);
  {$ifdef MSWINDOWS}
  tmp := CurrentAnsiConvert.UTF8ToAnsi(Text);
  {$ifndef HASCODEPAGE}
  AnsiToOem(pointer(tmp),pointer(tmp));
  {$endif}
  writeln(tmp);
  {$else}
  writeln(Text);
  {$endif}
  ioresult;
  TextColor(ccLightGray);
end;
{$I+}

procedure TSynLog.Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArgs: array of const;
  aInstance: TObject);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,TextFmt,TextArgs,aInstance);
end;

procedure TSynLog.Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArg: RawUTF8;
  aInstance: TObject=nil);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,TextFmt,[TextArg],aInstance);
end;

procedure TSynLog.Log(Level: TSynLogInfo; const TextFmt: RawUTF8; const TextArg: Int64;
  aInstance: TObject=nil);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,TextFmt,[TextArg],aInstance);
end;

procedure TSynLog.Log(Level: TSynLogInfo; const Text: RawUTF8; aInstance: TObject;
  TextTruncateAtLength: integer);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,Text,aInstance,TextTruncateAtLength);
end;

{$ifdef UNICODE}
procedure TSynLog.Log(Level: TSynLogInfo; const Text: string; aInstance: TObject);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,'%',[Text],aInstance);
end;
{$endif}

procedure TSynLog.LogLines(Level: TSynLogInfo; LinesToLog: PUTF8Char; aInstance: TObject;
  const IgnoreWhenStartWith: PAnsiChar);
procedure DoLog(LinesToLog: PUTF8Char);
var s: RawUTF8;
begin
  repeat
    s := trim(GetNextLine(LinesToLog,LinesToLog));
    if s<>'' then
      if (IgnoreWhenStartWith=nil) or not IdemPChar(pointer(s),IgnoreWhenStartWith) then
        LogInternal(Level,s,aInstance,maxInt);
  until LinesToLog=nil;
end;
begin
  if (self<>nil) and (Level in fFamily.fLevel) and (LinesToLog<>nil) then
    DoLog(LinesToLog);
end;

procedure TSynLog.LogThreadName(const Name: RawUTF8; IgnoreIfAlreadySet: boolean);
begin
  if (self<>nil) and (sllInfo in fFamily.fLevel) then
    if LogHeaderLock(sllInfo,false) then // inlined LogInternal
    try
      if IgnoreIfAlreadySet and (fThreadContext^.ThreadName<>'') then
        exit;
      fWriter.Add('SetThreadName %=%',[pointer(fThreadID),Name],twOnSameLine);
      fThreadContext^.ThreadName := Name;
    finally
      LogTrailerUnLock(sllInfo);
    end;
end;

function TSynLog.LogClass: TSynLogClass;
begin
  if self=nil then
    result := nil else
    result := PPointer(self)^;
end;

procedure TSynLog.ForceRotation;
begin
  EnterCriticalSection(GlobalThreadLock);
  try
    PerformRotation;
  finally
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

procedure TSynLog.DisableRemoteLog(value: boolean);
begin
  if (fDisableRemoteLog=value) or not Assigned(fFamily.fEchoRemoteEvent) then
    exit;
  if value then begin
    // fDisableRemoteLog=false -> remove from events, within the global mutex
    EnterCriticalSection(GlobalThreadLock);
    if fDisableRemoteLog=value then // unlikely set in-between
      LeaveCriticalSection(GlobalThreadLock) else begin
      fDisableRemoteLog := true;
      fWriter.EchoRemove(fFamily.fEchoRemoteEvent);
    end;
  end else begin
    // fDisableRemoteLog=true -> add to events, already within the global mutex
    fDisableRemoteLog := false;
    fWriter.EchoAdd(fFamily.fEchoRemoteEvent);
    LeaveCriticalSection(GlobalThreadLock);
  end;
end;

procedure TSynLog.Log(Level: TSynLogInfo; aInstance: TObject);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    if aInstance<>nil then
      LogInternal(Level,'',aInstance,maxInt) else
      LogInternal(Level,'Instance=nil',nil,maxInt);
end;

procedure TSynLog.Log(Level: TSynLogInfo; const aName: RawUTF8;
  aTypeInfo: pointer; const aValue; Instance: TObject);
begin
  if (self<>nil) and (Level in fFamily.fLevel) then
    LogInternal(Level,aName,aTypeInfo,aValue,Instance);
end;

{$STACKFRAMES ON}
procedure TSynLog.Log(Level: TSynLogInfo);
var LastError: DWORD;
    {$ifndef FPC}aCaller: PtrUInt;{$endif}
begin
  if Level=sllLastError then
    LastError := GetLastError else
    LastError := 0;
  if (self<>nil) and (Level in fFamily.fLevel) then
  if LogHeaderLock(Level,false) then
  try
    if LastError<>0 then
      AddErrorMessage(LastError);
    {$ifndef FPC}
    {$ifdef CPU64}
    {$ifdef MSWINDOWS}
    if RtlCaptureStackBackTrace(1,1,@aCaller,nil)=0 then
      aCaller := 0 else
      dec(aCaller,5); // ignore call TSynLog.Enter op codes
    {$else}
    aCaller := 0; // no stack trace yet under Linux64
    {$endif}
    {$else}
    {$ifdef PUREPASCAL}
    aCaller := 0; // e.g. ARM Linux
    {$else}
    asm
      mov eax,[ebp+4]  // retrieve caller EIP from push ebp; mov ebp,esp
      sub eax,5        // ignore call TSynLog.Enter op codes
      mov aCaller,eax
    end;
    {$endif}
    {$endif}
    TSynMapFile.Log(fWriter,aCaller,false);
    {$endif}
  finally
    LogTrailerUnLock(Level);
    if LastError<>0 then
      SetLastError(LastError);
  end;
end;
{$STACKFRAMES OFF}

class procedure TSynLog.DebuggerNotify(Level: TSynLogInfo;
  const Format: RawUTF8; const Args: array of const);
var Msg: RawUTF8;
begin
  if Format<>''then begin
    FormatUTF8(Format,Args,Msg);
    Add.LogInternal(Level,Msg,nil,maxInt);
    {$ifdef MSWINDOWS}
    {$ifndef FPC} // external exception :(
    //OutputDebugStringA(pointer(CurrentAnsiConvert.UTF8ToAnsi(Msg)));
    {$endif FPC}
    {$else}
    {$I-}
    write(Msg,'  ');
    ioresult;
    {$I+}
    {$endif MSWINDOWS}
  end;
  {$ifndef FPC_OR_PUREPASCAL}
  if DebugHook<>0 then
    asm int 3 end; // force manual breakpoint if tests are run from the IDE
  {$endif}
end;

procedure TSynLog.LogFileInit;
begin
  {$ifdef LINUX}
  QueryPerformanceMicroSeconds(fStartTimestamp);
  {$else}
  QueryPerformanceCounter(fStartTimestamp);
  if not QueryPerformanceFrequency(fFrequencyTimestamp) then begin
    fFamily.HighResolutionTimestamp := false;
    fFrequencyTimestamp := 0;
  end else
  {$endif LINUX}
    if (fFileRotationSize>0) or (fFileRotationNextHour<>0) then
      fFamily.HighResolutionTimestamp := false;
  fStreamPositionAfterHeader := fWriter.WrittenBytes;
  if fFamily.LocalTimestamp then
    fStartTimestampDateTime := Now else
    fStartTimestampDateTime := NowUTC;
  Include(fInternalFlags,logInitDone);
end;

procedure TSynLog.LogFileHeader;
var WithinEvents: boolean;
    i: integer;
    {$ifdef MSWINDOWS}
    Env: PWideChar;
    P: PWideChar;
    L: Integer;
    {$endif}

  procedure NewLine;
  begin
    if WithinEvents then begin
      fWriter.AddEndOfLine(sllNewRun);
      LogCurrentTime;
      fWriter.AddShort(LOG_LEVEL_TEXT[sllNewRun]);
    end else
      fWriter.Add(#10);
  end;

begin
  WithinEvents := fWriter.WrittenBytes>0;
  // array of const is buggy under Delphi 5 :( -> use fWriter.Add*() below
  if WithinEvents then begin
    LogCurrentTime;
    fWriter.AddShort(LOG_LEVEL_TEXT[sllNewRun]);
    fWriter.AddChars('=',50);
    NewLine;
  end;
  with ExeVersion, fWriter do begin
    AddString(ProgramFullSpec);
    NewLine;
    AddShort('Host=');  AddString(Host);
    AddShort(' User='); AddString(User);
    AddShort(' CPU=');
    if CpuInfoText='' then
      Add(SystemInfo.dwNumberOfProcessors) else
      for i := 1 to length(CpuInfoText) do
        if not (ord(CpuInfoText[i]) in [1..32,ord(':')]) then
          Add(CpuInfoText[i]);
    {$ifdef MSWINDOWS}
    with SystemInfo, OSVersionInfo do begin
      Add('*');
      Add(wProcessorArchitecture); Add('-'); Add(wProcessorLevel); Add('-');
      Add(wProcessorRevision);
    {$endif}
      {$ifdef CPUINTEL}
      Add(':'); AddBinToHex(@CpuFeatures,SizeOf(CpuFeatures));
      {$endif}
      AddShort(' OS=');
    {$ifdef MSWINDOWS}
      Add(ord(OSVersion)); Add('.'); Add(wServicePackMajor);
      Add('='); Add(dwMajorVersion); Add('.'); Add(dwMinorVersion); Add('.');
      Add(dwBuildNumber);
    end;
    {$else}
    AddTrimLeftLowerCase(ToText(OS_KIND)); Add('=');
    AddTrimSpaces(@SystemInfo.uts.sysname); Add('-');
    AddTrimSpaces(@SystemInfo.uts.release);
    AddReplace(@SystemInfo.uts.version,' ','-');
    {$endif MSWINDOWS}
    if OSVersionInfoEx<> '' then begin
      Add('/'); AddTrimSpaces(OSVersionInfoEx); end;
    {$ifdef MSWINDOWS}
    AddShort(' Wow64='); Add(integer(IsWow64));
    AddShort(' Freq=');
    QueryPerformanceFrequency(fFrequencyTimestamp);
    Add(fFrequencyTimestamp);
    {$else}
    AddShort(' Wow64=0 Freq=1000000'); // taken by QueryPerformanceMicroSeconds()
    {$endif MSWINDOWS}
    if IsLibrary then begin
      AddShort(' Instance=');
      AddNoJSONEscapeString(InstanceFileName);
    end;
    {$ifdef MSWINDOWS}
    if not fFamily.fNoEnvironmentVariable then begin
      NewLine;
      AddShort('Environment variables=');
      Env := GetEnvironmentStringsW;
      P := pointer(Env);
      while P^<>#0 do begin
        L := StrLenW(P);
        if (L>0) and (P^<>'=') then begin
          AddNoJSONEscapeW(PWord(P),0);
          Add(#9);
        end;
        inc(P,L+1);
      end;
      FreeEnvironmentStringsW(Env);
      CancelLastChar(#9);
    end;
    {$endif MSWINDOWS}
    NewLine;
    AddClassName(self.ClassType);
    AddShort(' '+SYNOPSE_FRAMEWORK_FULLVERSION+' ');
    if fFamily.LocalTimestamp then
      AddDateTime(Now) else
      AddDateTime(NowUTC);
    if WithinEvents then
      AddEndOfLine(sllNone) else
      Add(#10,#10);
    FlushToStream;
    EchoReset; // header is not to be sent to console
  end;
  Include(fInternalFlags,logHeaderWritten);
  if not (logInitDone in fInternalFlags) then
    LogFileInit;
end;

{$ifndef DELPHI5OROLDER}
{$WARN SYMBOL_DEPRECATED OFF} // for GetHeapStatus
procedure TSynLog.AddMemoryStats;
begin
  {$ifdef MSWINDOWS}
  with GetHeapStatus do
    if TotalAddrSpace<>0 then
    fWriter.Add(' AddrSpace=% Uncommitted=% Committed=% Allocated=% Free=% '+
       'FreeSmall=% FreeBig=% Unused=% Overheap=% ',
      [TotalAddrSpace,TotalUncommitted,TotalCommitted,TotalAllocated,TotalFree,
       FreeSmall,FreeBig,Unused,Overhead]);
  {$endif}
end;
{$WARN SYMBOL_DEPRECATED ON}
{$endif}

procedure TSynLog.AddErrorMessage(Error: Cardinal);
{$ifdef MSWINDOWS}
var Len: Integer;
    Buffer: array[byte] of WideChar;
begin
  Len := FormatMessageW(FORMAT_MESSAGE_FROM_SYSTEM or FORMAT_MESSAGE_ARGUMENT_ARRAY,
    nil, Error, 0, Buffer, SizeOf(Buffer), nil);
  while (Len>0) and (ord(Buffer[Len-1]) in [0..32,ord('.')]) do dec(Len);
  Buffer[Len] := #0;
  fWriter.Add(' ','"');
  fWriter.AddOnSameLineW(@Buffer,Len);
  fWriter.AddShort('" (');
{$else}
begin
  fWriter.AddShort('Error "');
  fWriter.AddAnsiString(StrError(Error), twOnSameLine);
  fWriter.AddShort('" (');
{$endif}
  fWriter.Add(Error);
  fWriter.Add(')',' ');
end;

procedure TSynLog.LogCurrentTime;
begin
  {$ifdef LINUX}
  if fFamily.HighResolutionTimestamp then begin
    QueryPerformanceMicroSeconds(fCurrentTimestamp);
  {$else}
  if fFamily.HighResolutionTimestamp and (fFrequencyTimestamp<>0) then begin
    QueryPerformanceCounter(fCurrentTimestamp);
  {$endif}
    dec(fCurrentTimestamp,fStartTimestamp);
    fWriter.AddBinToHexDisplay(@fCurrentTimestamp,sizeof(fCurrentTimestamp));
  end else
    fWriter.AddCurrentLogTime(fFamily.LocalTimestamp);
end;

function TSynLog.LogHeaderLock(Level: TSynLogInfo; AlreadyLocked: boolean): boolean;
var i: integer;
begin
  if not AlreadyLocked then
    LockAndGetThreadContext;
  try
    if fWriter=nil then
      CreateLogWriter; // file creation should be thread-safe
    if not (logHeaderWritten in fInternalFlags) then
      LogFileHeader else
      if not (logInitDone in fInternalFlags) then
        LogFileInit;
    if not (sllEnter in fFamily.Level) and (Level in fFamily.fLevelStackTrace) then
       for i := 0 to fThreadContext^.RecursionCount-1 do begin
         fWriter.AddChars(' ',i+24-byte(fFamily.HighResolutionTimestamp));
         AddRecursion(i,sllNone);
       end;
    LogCurrentTime;
    if fFamily.fPerThreadLog=ptIdentifiedInOnFile then
      fWriter.AddInt18ToChars3(fThreadIndex);
    fCurrentLevel := Level;
    fWriter.AddShort(LOG_LEVEL_TEXT[Level]);
    fWriter.AddChars(#9,fThreadContext^.RecursionCount-byte(Level in [sllEnter,sllLeave]));
    {$ifndef DELPHI5OROLDER}
    case Level of // handle additional text for some special error levels
      sllMemory: AddMemoryStats;
    end;
    {$endif}
    result := true;
  except
    on Exception do
     result := false ;
  end;
end;

procedure TSynLog.PerformRotation;
const _LOG_SYNLZ: array[boolean] of TFileName = ('.synlz','.log');
var currentMaxSynLZ: cardinal;
    i: integer;
    FN: array of TFileName;
begin
  fWriter.FlushFinal;
  FreeAndNil(fWriter);
  FreeAndNil(fWriterStream);
  currentMaxSynLZ := 0;
  if not (assigned(fFamily.fOnRotate) and
          fFamily.fOnRotate(self,fFileName)) then begin
    if fFamily.fRotateFileCount>1 then begin
      SetLength(FN,fFamily.fRotateFileCount-1);
      for i := fFamily.fRotateFileCount-1 downto 1 do begin
        FN[i-1] := ChangeFileExt(fFileName,
          '.'+IntToStr(i)+_LOG_SYNLZ[fFamily.fRotateFileNoCompression]);
        if (currentMaxSynLZ=0) and FileExists(FN[i-1]) then
          currentMaxSynLZ := i;
      end;
      if currentMaxSynLZ=fFamily.fRotateFileCount-1 then
        DeleteFile(FN[currentMaxSynLZ-1]); // delete e.g. '9.synlz'
      for i := fFamily.fRotateFileCount-2 downto 1 do
        RenameFile(FN[i-1],FN[i]); // e.g. '8.synlz' -> '9.synlz'
      if fFamily.fRotateFileNoCompression then
        RenameFile(fFileName,FN[0]) else      // main -> '1.log'
        FileSynLZ(fFileName,FN[0],LOG_MAGIC); // main -> '1.synlz'
    end;
    DeleteFile(fFileName);
  end;
  CreateLogWriter;
  LogFileHeader;
  if fFamily.fPerThreadLog=ptIdentifiedInOnFile then
    for i := 0 to fThreadContextCount-1 do
    with fThreadContexts[i] do
      if (pointer(ID)<>nil) and (ThreadName<>'') then begin // see TSynLog.LogThreadName
        LogCurrentTime;
        fWriter.AddInt18ToChars3(i+1);
        fWriter.AddShort(LOG_LEVEL_TEXT[sllInfo]);
        fWriter.Add('SetThreadName %=%',[pointer(ID),ThreadName],twOnSameLine);
        fWriter.AddEndOfLine(sllInfo);
      end;
end;

procedure TSynLog.LogInternal(Level: TSynLogInfo; const TextFmt: RawUTF8;
  const TextArgs: array of const; Instance: TObject);
var LastError: cardinal;
begin
  if Level=sllLastError then
    LastError := GetLastError else
    LastError := 0;
  if LogHeaderLock(Level,false) then
  try
    if Instance<>nil then
      fWriter.AddInstancePointer(Instance,' ',fFamily.WithUnitName,fFamily.WithInstancePointer);
    fWriter.Add(TextFmt,TextArgs,twOnSameLine,
      [woDontStoreDefault,woDontStoreEmptyString,woDontStore0,woFullExpand]);
    if LastError<>0 then
      AddErrorMessage(LastError);
  finally
    LogTrailerUnLock(Level);
    if LastError<>0 then
      SetLastError(LastError);
  end;
end;

procedure TSynLog.LogInternal(Level: TSynLogInfo; const Text: RawUTF8;
  Instance: TObject; TextTruncateAtLength: integer);
var LastError: cardinal;
begin
  if Level=sllLastError then
    LastError := GetLastError else
    LastError := 0;
  if LogHeaderLock(Level,false) then
  try
    if Text='' then begin
      if Instance<>nil then
        if Instance.InheritsFrom(Exception) then begin
          fWriter.AddInstanceName(Instance,':');
          if Instance.InheritsFrom(ESynException) then
            fWriter.WriteObject(Instance,[woFullExpand]) else begin
            fWriter.Add('"');
            fWriter.AddJSONEscapeString(Exception(Instance).Message);
            fWriter.Add('"');
          end;
        end else
          fWriter.WriteObject(Instance,[woFullExpand]);
    end else begin
      if Instance<>nil then
        fWriter.AddInstancePointer(Instance,' ',fFamily.WithUnitName,fFamily.WithInstancePointer);
      if length(Text)>TextTruncateAtLength then begin
        fWriter.AddOnSameLine(pointer(Text),TextTruncateAtLength);
        fWriter.AddShort('... (truncated) length=');
        fWriter.AddU(length(Text));
      end else
        fWriter.AddOnSameLine(pointer(Text));
    end;
    if LastError<>0 then
      AddErrorMessage(LastError);
  finally
    LogTrailerUnLock(Level);
    if LastError<>0 then
      SetLastError(LastError);
  end;
end;

procedure TSynLog.LogInternal(Level: TSynLogInfo; const aName: RawUTF8;
   aTypeInfo: pointer; const aValue; Instance: TObject);
begin
  if LogHeaderLock(Level,false) then
  try
    if Instance<>nil then
      fWriter.AddInstancePointer(Instance,' ',fFamily.WithUnitName,fFamily.WithInstancePointer);
    fWriter.AddOnSameLine(pointer(aName));
    fWriter.Add('=');
    fWriter.AddTypedJSON(aTypeInfo,aValue);
  finally
    LogTrailerUnLock(Level);
  end;
end;

procedure TSynLog.ComputeFileName;
var timeNow,hourRotate,timeBeforeRotate: TDateTime;
begin
  fFileName := fFamily.fCustomFileName;
  if fFileName='' then begin
    fFileName := UTF8ToString(ExeVersion.ProgramName);
    if fFamily.IncludeComputerNameInFileName then
      fFileName := fFileName+' ('+UTF8ToString(ExeVersion.Host)+')';
  end;
  fFileRotationSize := 0;
  if fFamily.fRotateFileCount>0 then begin
    if fFamily.fRotateFileSize>0 then
      fFileRotationSize := fFamily.fRotateFileSize shl 10; // size KB -> B
    if fFamily.fRotateFileAtHour in [0..23] then begin
      hourRotate := EncodeTime(fFamily.fRotateFileAtHour,0,0,0);
      timeNow := Time;
      if hourRotate<timeNow then
        hourRotate := hourRotate+1; // trigger will be tomorrow
      timeBeforeRotate := hourRotate-timeNow;
      fFileRotationNextHour := GetTickCount64+trunc(timeBeforeRotate*MSecsPerDay);
    end;
  end;
  if (fFileRotationSize=0) and (fFileRotationNextHour=0) then
    fFileName := fFileName+' '+Ansi7ToString(NowToString(false));
  {$ifdef MSWINDOWS}
  if IsLibrary and (fFamily.fCustomFileName='') then
    fFileName := fFileName+' '+ExtractFileName(GetModuleName(HInstance));
  {$endif}
  if fFamily.fPerThreadLog=ptOneFilePerThread then
    fFileName := fFileName+' '+Ansi7ToString(PointerToHex(pointer(GetCurrentThreadId)));
  fFileName := fFamily.fDestinationPath+fFileName+fFamily.fDefaultExtension;
end;

procedure TSynLog.CreateLogWriter;
var i,retry: integer;
    exists: boolean;
begin
  if fWriterStream=nil then begin
    ComputeFileName;
    if fFamily.NoFile then
      fWriterStream := TFakeWriterStream.Create else begin
      if FileExists(fFileName) then
        case fFamily.FileExistsAction of
        acOverwrite:
          DeleteFile(fFileName);
        acAppend:
          Include(fInternalFlags,logHeaderWritten);
        end;
      for retry := 0 to 2 do begin
        for i := 1 to 10 do
        try
          exists := FileExists(fFileName);
          if exists and (fFamily.FileExistsAction<>acOverwrite) then begin
            if fFamily.FileExistsAction=acAppend then
              Include(fInternalFlags,logHeaderWritten);
          end else
          if (fFileRotationSize=0) or not exists then
            TFileStream.Create(fFileName,fmCreate).Free;   // create a void file
          fWriterStream := TFileStreamWithoutWriteError.Create(fFileName,
            fmOpenReadWrite or fmShareDenyWrite); // open with read sharing
          break;
        except
          on Exception do
            SleepHiRes(100);
        end;
        if fWriterStream<>nil then
          break;
        fFileName := ChangeFileExt(fFileName,'-'+fFamily.fDefaultExtension);
      end;
    end;
    if fWriterStream=nil then // go on if file creation fails (e.g. RO folder)
      fWriterStream := TFakeWriterStream.Create;
    if (fFileRotationSize>0) or (fFamily.FileExistsAction<>acOverwrite) then
      fWriterStream.Seek(0,soEnd); // in rotation mode, append at the end
  end;
  if fWriterClass=nil then
    // set to TTextWriterWithEcho or TJSONSerializer if mORMot.pas is linked
    fWriterClass := DefaultTextWriterSerializer;
  if fWriter=nil then begin
    fWriter := fWriterClass.Create(fWriterStream,fFamily.BufferSize);
    fWriter.CustomOptions := fWriter.CustomOptions+[twoEnumSetsAsTextInRecord,twoFullSetsAsStar];
  end;
  fWriter.EndOfLineCRLF := fFamily.EndOfLineCRLF;
  if integer(fFamily.EchoToConsole)<>0 then
    fWriter.EchoAdd(ConsoleEcho);
  if Assigned(fFamily.EchoCustom) then
    fWriter.EchoAdd(fFamily.EchoCustom);
  if Assigned(fFamily.fEchoRemoteClient) then
    fWriter.EchoAdd(fFamily.fEchoRemoteEvent);
  if (AutoFlushThread=nil) and (fFamily.AutoFlushTimeOut<>0) then
    fFamily.StartAutoFlush;
end;

function TSynLog.GetFileSize: Int64;
begin
  if fWriterStream<>nil then begin
    EnterCriticalSection(GlobalThreadLock);
    try
      result := fWriterStream.Size;
    finally
      LeaveCriticalSection(GlobalThreadLock);
    end;
   end else
    result := 0;
end;

procedure TSynLog.AddRecursion(aIndex: integer; aLevel: TSynLogInfo);
begin
  // at entry, aLevel is sllEnter, sllLeave or sllNone (from LogHeaderBegin)
  with fThreadContext^ do
  if cardinal(aIndex)<cardinal(RecursionCount) then
  with Recursion[aIndex] do begin
    if aLevel<>sllLeave then begin
      if Instance<>nil then
        fWriter.AddInstancePointer(Instance,'.',fFamily.WithUnitName,fFamily.WithInstancePointer);
      if MethodName<>nil then begin
        if MethodNameLocal<>mnLeave then begin
          fWriter.AddOnSameLine(MethodName);
          case MethodNameLocal of
          mnEnter:
            MethodNameLocal := mnLeave;
          mnEnterOwnMethodName: begin
            MethodNameLocal := mnLeave;
            RawUTF8(pointer(MethodName)) := ''; // release temp string
          end;
          end;
        end;
      end {$ifndef FPC} else
        TSynMapFile.Log(fWriter,Caller,false){$endif};
    end;
    if (aLevel<>sllNone) {$ifndef LINUX}and (fFrequencyTimestamp<>0){$endif} then begin
      if not fFamily.HighResolutionTimestamp then begin // no previous TSynLog.LogCurrentTime call
        {$ifdef LINUX}QueryPerformanceMicroSeconds{$else}QueryPerformanceCounter{$endif}(fCurrentTimestamp);
        dec(fCurrentTimestamp,fStartTimestamp);
      end;
      case aLevel of
      sllEnter:
        EnterTimestamp := fCurrentTimestamp;
      sllLeave:
        fWriter.AddMicroSec({$ifdef LINUX}fCurrentTimestamp-EnterTimestamp{$else}
          ((fCurrentTimestamp-EnterTimestamp)*(1000*1000))div fFrequencyTimestamp{$endif});
      end; // may be sllNone when called from LogHeaderLock()
    end;
  end;
  fWriter.AddEndOfLine(aLevel);
end;

const
  MINIMUM_EXPECTED_STACKTRACE_DEPTH = 2;

procedure TSynLog.AddStackTrace(Stack: PPtrUInt);
{$ifdef FPC}
var frames: array[0..127] of pointer;
    i, n, depth: PtrInt;
begin
  depth := fFamily.StackTraceLevel;
  if (depth=0) or (@BackTraceStrFunc=@SysBackTraceStr) then
    exit;
  try
    n := CaptureBacktrace(2,length(frames),@frames[0]);
    if n>depth then
      n := depth;
    for i := 0 to n-1 do
      if (i=0) or (frames[i]<>frames[i-1]) then
        TSynMapFile.Log(fWriter,PtrUInt(frames[i]),false); // ignore any TSynLog.*
  except // don't let any unexpected GPF break the logging process
  end;
end;
{$else}
{$ifndef CPU64}
  procedure AddStackManual(Stack: PPtrUInt);
    function check2(xret: PtrUInt): Boolean;
    var i: PtrUInt;
    begin
      result := true;
      for i := 2 to 7 do
        if PWord(xret-i)^ and $38FF=$10FF then
          exit;
      result := false;
    end;
  var st, max_stack, min_stack, depth: PtrUInt;
  begin
    depth := fFamily.StackTraceLevel;
    if depth=0 then
      exit;
    asm
      mov min_stack,ebp
    end;
    if Stack=nil then // if no Stack pointer set, retrieve current one
      Stack := pointer(min_stack);
    {$ifdef WITH_MAPPED_EXCEPTIONS}
    max_stack := CurrentTopOfStack;
    if max_stack=0 then begin
      ComputeCurrentTopOfStack;
      max_stack := CurrentTopOfStack;
    end;
    {$else}
    asm
      mov eax,fs:[4]
      mov max_stack, eax
      // mov eax,fs:[18h]; mov ecx,dword ptr [eax+4]; mov max_stack,ecx
    end;
    {$endif WITH_MAPPED_EXCEPTIONS}
    fWriter.AddShort(' stack trace ');
    if PtrUInt(stack)>=min_stack then
    try
      while (PtrUInt(stack)<max_stack) do begin
        st := stack^;
        if ((st>max_stack) or (st<min_stack)) and
           not IsBadReadPtr(pointer(st-8),12) and
           ((pByte(st-5)^=$E8) or check2(st)) then begin
          TSynMapFile.Log(fWriter,st,false); // ignore any TSynLog.* methods
          dec(depth);
          if depth=0 then break;
        end;
        inc(stack);
      end;
    except
      // just ignore any access violation here
    end;
  end;
{$endif}
{$ifdef WITH_MAPPED_EXCEPTIONS}
begin
  AddStackManual(Stack);
end;
{$else}
var n, i: integer;
    BackTrace: array[byte] of PtrUInt;
begin
  if fFamily.StackTraceLevel<=0 then
    exit;
  {$ifdef MSWINDOWS}
  if (fFamily.StackTraceUse=stOnlyManual) or
     (RtlCaptureStackBackTraceRetrieved<>btOK) then begin
    {$ifndef FPC}
    {$ifndef CPU64}
    AddStackManual(Stack);
    {$endif}
    {$endif}
  end else begin
    try
      n := RtlCaptureStackBackTrace(2,fFamily.StackTraceLevel,@BackTrace,nil);
      if (n<MINIMUM_EXPECTED_STACKTRACE_DEPTH) and
         (fFamily.StackTraceUse<>stOnlyAPI) then begin
        {$ifndef FPC}
        {$ifndef CPU64}
        AddStackManual(Stack);
        {$endif}
        {$endif}
      end else begin
        fWriter.AddShort(' stack trace API ');
        for i := 0 to n-1 do
          TSynMapFile.Log(fWriter,BackTrace[i],false); // ignore any TSynLog.*
      end;
    except
      // just ignore any access violation here
    end;
  end;
  {$endif MSWINDOWS}
end;
{$endif WITH_MAPPED_EXCEPTIONS}
{$endif FPC}


{ TAutoLockerDebug }

constructor TAutoLockerDebug.Create(aLog: TSynLogClass; const aIdentifier: RawUTF8);
begin
  inherited Create;
  fLog := aLog;
  fIdentifier := aIdentifier;
end;

procedure TAutoLockerDebug.Enter;
begin
  fLog.Add.Log(sllTrace,'Lock % %',[fIdentifier,fCounter]);
  inherited Enter;
  fLog.Add.Log(sllTrace,'Locked % %',[fIdentifier,fCounter]);
  inc(fCounter);
end;

procedure TAutoLockerDebug.Leave;
var n: integer;
begin
  dec(fCounter);
  n := fCounter;
  fLog.Add.Log(sllTrace,'Unlock % %',[fIdentifier,n]);
  inherited Leave;
  fLog.Add.Log(sllTrace,'Unlocked % %',[fIdentifier,n]);
end;


{ TSynLogFile }

constructor TSynLogFile.Create;
var L: TSynLogInfo;
begin
  for L := low(TSynLogInfo) to high(TSynLogInfo) do
    fLogLevelsTextMap[L] := PCardinal(@LOG_LEVEL_TEXT[L][3])^; // [3] -> e.g. 'UST4'
end;

function TSynLogFile.GetLogLevelFromText(LineBeg: PUTF8Char): TSynLogInfo;
var P: PtrInt;
begin
  P := PtrInt(IntegerScan(@fLogLevelsTextMap[succ(sllNone)],
    ord(high(TSynLogInfo)),PCardinal(LineBeg+fLineLevelOffset)^));
  if P<>0 then
    result := TSynLogInfo((P-PtrInt(PtrUInt(@fLogLevelsTextMap[succ(sllNone)])))shr 2+1) else
    result := sllNone;
end;

function TSynLogFile.EventCount(const aSet: TSynLogInfos): integer;
var i: integer;
begin
  result := 0;
  if integer(aSet)<>0 then
    for i := 0 to Count-1 do
      if fLevels[i] in aSet then
        inc(result);
end;

function TSynLogFile.LineContains(const aUpperSearch: RawUTF8; aIndex: Integer): Boolean;
begin
  if (self=nil) or (cardinal(aIndex)>=cardinal(fCount)) or (aUpperSearch='') then
    result := false else
    result := GetLineContains(PUTF8Char(fLines[aIndex])+fLineTextOffset,
      fMapEnd,pointer(aUpperSearch));
end;

function TSynLogFile.EventDateTime(aIndex: integer): TDateTime;
var Timestamp: Int64;
    P: PUTF8Char;
    Y,M,D, HH,MM,SS,MS: cardinal;
begin
  if cardinal(aIndex)>=cardinal(fCount) then
    result := 0 else
    if fFreq=0 then begin
      P := fLines[aIndex]; // YYYYMMDD hhmmsszz
      if Char4ToWord(P,Y) or Char2ToByte(P+4,M) or Char2ToByte(P+6,D) or
         Char2ToByte(P+9,HH) or Char2ToByte(P+11,MM) or Char2ToByte(P+13,SS) or
         Char2ToByte(P+15,MS) then
        Iso8601ToDateTimePUTF8CharVar(P,17,result) else
        if TryEncodeDate(Y,M,D,result) then
          // MS shl 4 = 16 ms resolution in TTextWriter.AddCurrentLogTime()
          result := result+EncodeTime(HH,MM,SS,MS shl 4) else
          result := 0;
    end else
      if HexDisplayToBin(fLines[aIndex],@Timestamp,sizeof(Timestamp)) then
        result := fStartDateTime+(Timestamp/fFreqPerDay) else
        result := 0;
end;

procedure TSynLogFile.LoadFromMap(AverageLineLength: integer=32);
  var PBeg, P, PEnd: PUTF8Char;
  function StrPosI(P,PEnd: PUTF8Char; SearchUp: PAnsiChar): PUTF8Char;
  begin
    result := P;
    while result<PEnd do
      if IdemPChar(result,SearchUp) then
        exit else
        inc(result);
    result := nil;
  end;
  function GetOne(const UP: RawUTF8; var S: RawUTF8): boolean;
  var LUP: integer;
  begin
    LUP := length(UP);
    P := StrPosI(PBeg,PEnd-LUP,pointer(UP));
    if P=nil then
      result := false else begin
      FastSetString(S,PBeg,P-PBeg);
      PBeg := P+LUP;
      result := pointer(S)<>nil;
    end;
  end;
  function ComputeProperTime(var procndx: Integer): cardinal; // returns leave
  var start, i: integer;
  begin
    start := procndx;
    with fLogProcNatural[procndx] do begin
      ProperTime := Time;
      result := Index;
    end;
    repeat
      inc(result);
      if result>=Cardinal(Count) then
        break;
      case fLevels[result] of
      sllEnter: begin
        inc(procndx);
        assert(fLogProcNatural[procndx].Index=result);
        result := ComputeProperTime(procndx);
      end;
      sllLeave: begin
        with fLogProcNatural[start] do
        for i := start+1 to procndx do
          dec(ProperTime,fLogProcNatural[i].ProperTime);
        break;
      end;
      end;
    until false;
  end;
  procedure CleanLevels(Log: TSynLogFile);
  var i, aCount, pCount, dCount, dValue, dMax: integer;
  begin
    aCount := 0;
    pCount := 0;
    dCount := 0;
    dMax := Length(fDayChangeIndex);
    if dMax>0 then
      dValue := fDayChangeIndex[0] else
      dValue := -1;
    with Log do
    for i := 0 to fCount-1 do
      if fLevels[i]<>sllNone then begin
        fLevels[aCount] := fLevels[i];
        fLines[aCount] := fLines[i];
        if fThreads<>nil then
          fThreads[aCount] := fThreads[i];
        if fLevels[i]=sllEnter then begin
          fLogProcNatural[pCount].Index := aCount;
          inc(pCount);
        end;
        if dValue=i then begin
          fDayChangeIndex[dCount] := aCount;
          inc(dCount);
          if dCount<dMax then
            dValue := fDayChangeIndex[dCount];
        end;
        inc(aCount);
      end;
    Log.fCount := aCount;
    assert(pCount=Log.fLogProcNaturalCount);
    if dMax>0 then begin
      SetLength(fDayCount,dMax);
      dec(dMax);
      for i := 0 to dMax-1 do
        fDayCount[i] := fDayChangeIndex[i+1]-fDayChangeIndex[i];
      fDayCount[dMax] := aCount-fDayChangeIndex[dMax];
    end;
  end;
var aWow64, feat: RawUTF8;
    i, j, Level: integer;
    TSEnter, TSLeave: Int64;
    OK: boolean;
begin
  // 1. calculate fLines[] + fCount and fLevels[] + fLogProcNatural[] from .log content
  fLineHeaderCountToIgnore := 3; fIsJournald := false;
  if IdemPChar(pointer(fMap.Buffer),'-- LOGS BEGIN AT') then begin
    //-- Logs begin at Sun 2020-06-07 12:42:31 EEST, end at Thu 2020-06-18 18:08:52 EEST. --
    fIsJournald := true;
    fHeaderLinesCount := 1;
    fLineHeaderCountToIgnore := 1;
  end else begin
    //2020-06-18T13:28:20.754089+0300 ub[12316]:
    Iso8601ToDateTimePUTF8CharVar(pointer(fMap.Buffer),26,fStartDateTime);
    if unaligned(fStartDateTime) <> 0 then begin
      if (fMap.Buffer+8)^ <> ' ' then //20200821 14450738 ... - syn log without header
        fIsJournald := true;
      fHeaderLinesCount := 0;
      fLineHeaderCountToIgnore := 0;
    end;
  end;
  inherited LoadFromMap(100);
  // 2. fast retrieval of header
  OK := false;
  try
    // journald export or TSynLog WITHOUT regular header
    if fIsJournald or (fLineHeaderCountToIgnore=0) then begin
      if LineSizeSmallerThan(1,34) then exit;
      Iso8601ToDateTimePUTF8CharVar(fLines[1],26,fStartDateTime);
      if fStartDateTime=0 then
        exit;
    end else begin // TSynLog regular header
{  C:\Dev\lib\SQLite3\exe\TestSQL3.exe 0.0.0.0 (2011-04-07 11:09:06)
   Host=BW013299 User=G018869 CPU=1*0-15-1027 OS=2.3=5.1.2600 Wow64=0 Freq=3579545
   TSynLog 1.13 LVCL 2011-04-07 12:04:09 }
    if (fCount<=fLineHeaderCountToIgnore) or LineSizeSmallerThan(0,24) or
       not IdemPChar(fLines[1],'HOST=') or (fLevels=nil) or (fLineLevelOffset=0) then
      exit;
    PBeg := fLines[0];
    PEnd := PBeg+LineSize(0)-12;
    if PEnd<PBeg then
      exit;
    if PEnd^='(' then begin  // '(2011-04-07)' format
      if (PEnd[-1]<>' ') or (PEnd[0]<>'(') or (PEnd[11]<>')') then
        exit;
      Iso8601ToDateTimePUTF8CharVar(PEnd+1,10,fExeDate);
    end else begin  // '(2011-04-07 11:09:06)' format
      dec(PEnd,9);
      if (PEnd<PBeg) or (PEnd[-1]<>' ') or (PEnd[0]<>'(') or (PEnd[20]<>')') then
        exit;
      Iso8601ToDateTimePUTF8CharVar(PEnd+1,19,fExeDate);
    end;
    dec(PEnd);
    P := PEnd;
    repeat if P<=PBeg then exit else dec(P) until P^=' ';
    FastSetString(fExeVersion,P+1,PEnd-P-1);
    repeat dec(P); if P<=PBeg then exit; until P^<>' ';
    FastSetString(fExeName,PBeg,P-PBeg+1);
    PBeg := PUTF8Char(fLines[1])+5;
    PEnd := PUTF8Char(fLines[1])+LineSize(1);
    if not GetOne(' USER=',fHost) or not GetOne(' CPU=',fUser) or
       not GetOne(' OS=',fCPU)    or not GetOne(' WOW64=',fOsDetailed) or
       not GetOne(' FREQ=',aWow64) then
      exit;
    Split(fCPU,':',fCpu,feat);
    SynCommons.HexToBin(pointer(feat),@fIntelCPU,SizeOf(fIntelCPU));
    fWow64 := aWow64='1';
    SetInt64(PBeg,fFreq);
    while (PBeg<PEnd) and (PBeg^>' ') do inc(PBeg);
    if IdemPChar(PBeg,' INSTANCE=') then // only available for a library log
      FastSetString(fInstanceName,PBeg+10,PEnd-PBeg-10);
    fHeaderLinesCount := 4;
    while fHeaderLinesCount<fCount do begin
      if PAnsiChar(fLines[fHeaderLinesCount-1])^<' ' then
        break; // end of header = void line
      inc(fHeaderLinesCount);
    end;
    if (LineSize(fHeaderLinesCount-1)<>0) or
       LineSizeSmallerThan(fHeaderLinesCount,16) then
      exit;
    if fHeaderLinesCount<>4 then
      FastSetString(fHeaders,fLines[2],PtrUInt(fLines[fHeaderLinesCount-2])-PtrUInt(fLines[2]));
    if PWord(fLines[fHeaderLinesCount])^<>ord('0')+ord('0')shl 8 then // YYYYMMDD -> 20101225 e.g.
      fFreq := 0 else // =0 if date time, >0 if high-resolution time stamp
      fFreqPerDay := fFreq*SecsPerDay;
    P := pointer(fOSDetailed);
    fOS := TWindowsVersion(GetNextItemCardinal(P,'.'));
    if fOS<>wUnknown then
      fOSServicePack := GetNextItemCardinal(P);
    P := fLines[fHeaderLinesCount-2]; // TSQLLog 1.18.2765 ERTL FTS3 2016-07-17T22:38:03
    i := LineSize(fHeaderLinesCount-2)-19; // length('2016-07-17T22:38:03')=19
    if i>0 then begin
      FastSetString(fFramework,PAnsiChar(P),i-1);
      Iso8601ToDateTimePUTF8CharVar(P+i,19,fStartDateTime);
    end;
    if fStartDateTime=0 then
      exit;
    end;
    // 3. compute fCount and fLines[] so that all fLevels[]<>sllNone
    CleanLevels(self);
    if Length(fLevels)-fCount>16384 then begin // size down only if worth it
      SetLength(fLevels,fCount);
      if fThreads<>nil then begin
        SetLength(fThreads,fCount);
        SetLength(fThreadInfo,fThreadMax+1);
      end;
    end;
    // 4. compute customer-side profiling
    SetLength(fLogProcNatural,fLogProcNaturalCount);
    for i := 0 to fLogProcNaturalCount-1 do
      if fLogProcNatural[i].Time>=99000000 then begin // overange 99.000.000 -> compute
        Level := 0;
        j := fLogProcNatural[i].Index;
        repeat
          inc(j);
          if j=fCount then break;
          case fLevels[j] of
          sllEnter: inc(Level);
          sllLeave: if Level=0 then begin
            if fFreq=0 then // adjust huge seconds timing from date/time column
              fLogProcNatural[i].Time :=
                Round((EventDateTime(j)-EventDateTime(fLogProcNatural[i].Index))*86400000000.0)+
                fLogProcNatural[i].Time mod 1000000 else begin
              HexDisplayToBin(fLines[fLogProcNatural[i].Index],@TSEnter,sizeof(TSEnter));
              HexDisplayToBin(fLines[j],@TSLeave,sizeof(TSLeave));
              fLogProcNatural[i].Time := ((TSLeave-TSEnter)*(1000*1000)) div fFreq;
            end;
            break;
          end else dec(Level);
          end;
        until false;
      end;
    i := 0;
    while i<fLogProcNaturalCount do begin
      ComputeProperTime(i);
      inc(i);
    end;
    LogProcMerged := false; // set LogProp[]
    OK := true;
  finally
    if not OK then begin
      Finalize(fLevels); // mark not a valid .log
      Finalize(fThreads);
      fLineLevelOffset := 0;
    end;
  end;
end;

procedure TSynLogFile.AddInMemoryLine(const aNewLine: RawUTF8);
var P: PUTF8Char;
begin
  if aNewLine='' then
    exit;
  P := pointer(aNewLine);
  if (PInteger(P)^=ord('f')+ord('r')shl 8+ord('e')shl 16+ord('q')shl 24) and
     (P[4]='=') then begin
    inc(P,5);
    fFreq := GetNextItemInt64(P);
    fFreqPerDay := fFreq*SecsPerDay;
    fStartDateTime := GetNextItemDouble(P);
    UTF8DecodeToString(P,StrLen(P),string(fFileName));
  end else
    inherited AddInMemoryLine(aNewLine);
end;

procedure TSynLogFile.LogProcSort(Order: TLogProcSortOrder);
begin
  if (fLogProcNaturalCount<=1) or (Order=fLogProcSortInternalOrder) then
    Exit;
  fLogProcSortInternalOrder := Order;
  LogProcSortInternal(0,LogProcCount-1);
end;

function StrICompLeftTrim(Str1, Str2: PUTF8Char): PtrInt;
var C1, C2: integer;
begin
  while Str1^ in [#9,' '] do inc(Str1);
  while Str2^ in [#9,' '] do inc(Str2);
  repeat
    C1 := NormToUpperByte[ord(Str1^)];
    C2 := NormToUpperByte[ord(Str2^)];
    if (C1<>C2) or (C1<32) then
      break;
    Inc(Str1);
    Inc(Str2);
  until false;
  Result := C1-C2;
end;

function TSynLogFile.LogProcSortComp(A, B: PtrInt): PtrInt;
begin
  case fLogProcSortInternalOrder of
    soByName: result :=
      StrICompLeftTrim(PUTF8Char(fLines[LogProc[A].Index])+fLineTextOffset,
                       PUTF8Char(fLines[LogProc[B].Index])+fLineTextOffset);
    soByOccurrence: result := LogProc[A].Index-LogProc[B].Index;
    soByTime:       result := LogProc[B].Time-LogProc[A].Time;
    soByProperTime: result := LogProc[B].ProperTime-LogProc[A].ProperTime;
    else  result := A-B;
  end;
end;

procedure TSynLogFile.LogProcSortInternal(L, R: PtrInt);
  procedure Exchg(var P1,P2: TSynLogFileProc);
  var c: TSynLogFileProc;
  begin
    c := P1;
    P1 := P2;
    P2 := c;
  end;
var I,J,P: PtrInt;
begin
  if L<R then
  repeat
    I := L; J := R;
    P := (L + R) shr 1;
    repeat
      while LogProcSortComp(I,P)<0 do inc(I);
      while LogProcSortComp(J,P)>0 do dec(J);
      if I<=J then begin
        Exchg(LogProc[i],LogProc[j]);
        if P = I then P := J else if P = J then P := I;
        Inc(I); Dec(J);
      end;
    until I>J;
    if J - L < R - I then begin // use recursion only for smaller range
      if L < J then
        LogProcSortInternal(L, J);
      L := I;
    end else begin
      if I < R then
        LogProcSortInternal(I, R);
      R := J;
    end;
  until L >= R;
end;

procedure TSynLogFile.ProcessOneLine(LineBeg, LineEnd: PUTF8Char);
  function DecodeMicroSec(P: PByte): integer;
  var B: integer;
  begin // fast decode 00.020.006 at the end of the line
    B := ConvertHexToBin[P^];   // 00
    if B>9 then
      result := -1 else begin
      result := B;
      inc(P);
      B := ConvertHexToBin[P^];
      if B>9 then
        result := -1 else begin
        result := result*10+B;
        inc(P,2);                 // .
        B := ConvertHexToBin[P^]; // 020
        if B>9 then
          result := -1 else begin
          result := result*10+B;
          inc(P);
          B := ConvertHexToBin[P^];
          if B>9 then
            result := -1 else begin
            result := result*10+B;
            inc(P);
            B := ConvertHexToBin[P^];
            if B>9 then
              result := -1 else begin
              result := result*10+B;
              inc(P,2);                 // .
              B := ConvertHexToBin[P^]; // 006
              if B>9 then
                result := -1 else begin
                result := result*10+B;
                inc(P);
                B := ConvertHexToBin[P^];
                if B>9 then
                  result := -1 else begin
                  result := result*10+B;
                  inc(P);
                  B := ConvertHexToBin[P^];
                  if B>9 then
                    result := -1 else
                    result := result*10+B;
                end;
              end;
            end;
          end;
        end;
      end;
    end;
  end;
var thread,n: cardinal;
    MS: integer;
    L: TSynLogInfo;
    p: PUTF8Char;
    dcOffset: integer;
begin
  inherited ProcessOneLine(LineBeg,LineEnd);
  if length(fLevels)<fLinesMax then
    SetLength(fLevels,fLinesMax);
  if (fCount<=fLineHeaderCountToIgnore) or (LineEnd-LineBeg<24) then
    exit;
  if fIsJournald then
    dcOffset := 2 else // point to last 2 digit of year
    dcOffset := 0;
  if fLineLevelOffset=0 then begin
    if (fCount>50) or not (LineBeg[0] in ['0'..'9']) then
      exit; // definitively does not sound like a .log content
    if fIsJournald then begin
      p := PosChar(LineBeg, ']'); // time proc[pid]:
      if p=nil then
        exit; // not a log
      fLineLevelOffset := (p - LineBeg) + 4; // ":  "
      fDayCurrent := PInt64(LineBeg+dcOffset)^;
    end else if LineBeg[8]=' ' then begin
      // YYYYMMDD HHMMSS is one char bigger than Timestamp
      fLineLevelOffset := 19;
      fDayCurrent := PInt64(LineBeg)^;
      AddInteger(fDayChangeIndex,fCount-1);
    end else
      fLineLevelOffset := 18;
    if (LineBeg[fLineLevelOffset]='!') or // ! = thread 1
       (GetLogLevelFromText(LineBeg)=sllNone) then begin
      inc(fLineLevelOffset,3);
      fThreadsCount := fLinesMax;
      SetLength(fThreads,fLinesMax);
    end;
    fLineTextOffset := fLineLevelOffset+4;
    SetLength(fLogProcStack, fLinesMax);
    SetLength(fLogProcStackCount, fLinesMax);
  end;
  L := GetLogLevelFromText(LineBeg);
  if L=sllNone then
    exit;
  if (fDayChangeIndex<>nil) and (fDayCurrent<>PInt64(LineBeg+dcOffset)^) then begin
    fDayCurrent := PInt64(LineBeg+dcOffset)^;
    AddInteger(fDayChangeIndex,fCount-1);
  end;
  if fThreads<>nil then begin
    if fThreadsCount<fLinesMax then begin
      fThreadsCount := fLinesMax;
      SetLength(fThreads,fLinesMax);
    end;
    thread := Chars3ToInt18(LineBeg+fLineLevelOffset-5);
    fThreads[fCount-1] := thread;
    if thread>fThreadMax then begin
      fThreadMax := thread;
      if thread>=fThreadInfoMax then begin
        fThreadInfoMax := thread+256;
        SetLength(fThreadInfo,fThreadInfoMax);
      end;
    end;
    inc(fThreadInfo[thread].Rows);
    if (L=sllInfo) and IdemPChar(LineBeg+fLineLevelOffset+5,'SETTHREADNAME ') then
      with fThreadInfo[thread] do begin // see TSynLog.LogThreadName
        n := length(SetThreadName);
        SetLength(SetThreadName,n+1);
        SetThreadName[n] := LineBeg;
      end;
  end else
    thread := 0;
  fLevels[fCount-1] := L; // need exact match of level text
  include(fLevelUsed,L);
  case L of
  sllEnter: begin
    if Cardinal(fLogProcStackCount[thread])>=Cardinal(length(fLogProcStack[thread])) then
      SetLength(fLogProcStack[thread],length(fLogProcStack[thread])+256);
    fLogProcStack[thread][fLogProcStackCount[thread]] := fLogProcNaturalCount;
    inc(fLogProcStackCount[thread]);
    if Cardinal(fLogProcNaturalCount)>=Cardinal(length(fLogProcNatural)) then
      SetLength(fLogProcNatural,length(fLogProcNatural)+32768);
    // fLogProcNatural[].Index will be set in TSynLogFile.LoadFromMap
    inc(fLogProcNaturalCount);
  end;
  sllLeave:
  if (LineEnd-LineBeg>10) and (LineEnd[-4]='.') and (LineEnd[-8]='.') and
     (fLogProcStackCount[thread]>0) then begin // 00.020.006
    MS := DecodeMicroSec(PByte(LineEnd-10));
    if MS>=0 then begin
      dec(fLogProcStackCount[thread]);
      fLogProcNatural[fLogProcStack[thread][fLogProcStackCount[thread]]].Time := MS;
    end;
  end;
  end;
end;

function TSynLogFile.ThreadRows(ThreadID: integer): cardinal;
begin
  if fThreadInfo<>nil then
    result := fThreadInfo[ThreadID].Rows else
    result := 0;
end;

function TSynLogFile.ThreadName(ThreadID, CurrentLogIndex: integer): RawUTF8;
var i: integer;
    lineptr: PtrUInt;
    found: pointer;
begin
  if ThreadID=1 then
    result := 'Main Thread' else begin
    result := '';
    if cardinal(ThreadID)<=fThreadMax then
      with fThreadInfo[ThreadID] do
        if SetThreadName<>nil then begin
          found := SetThreadName[0];
          if cardinal(CurrentLogIndex)<cardinal(fCount) then begin
            lineptr := PtrUInt(fLines[CurrentLogIndex]);
            for i := length(SetThreadName)-1 downto 1 do
              if lineptr>=PtrUInt(SetThreadName[i]) then begin
                found := SetThreadName[i];
                break;
              end;
          end;
          FastSetString(result,found,GetLineSize(found,fMapEnd));
          delete(result,1,PosEx('=',result,40));
        end;
    if result='' then
      result := 'Thread';
  end;
  if cardinal(ThreadID)<=fThreadMax then
    result := FormatUTF8('% % (% rows)',[ThreadID,result,fThreadInfo[ThreadID].Rows]);
end;

function TSynLogFile.ThreadNames(CurrentLogIndex: integer): TRawUTF8DynArray;
var i: integer;
begin
  result := nil;
  SetLength(result,fThreadMax);
  if fThreadInfo=nil then
    exit;
  for i := 1 to fThreadMax do
    result[i-1] := ThreadName(i,CurrentLogIndex);
end;

procedure TSynLogFile.GetDays(out Days: TDateTimeDynArray);
var i,n: integer;
begin
  n := length(fDayChangeIndex);
  SetLength(Days,n);
  for i := 0 to n-1 do
    Days[i] := EventDateTime(fDayChangeIndex[i]);
end;

function TSynLogFile.GetEventText(index: integer): RawUTF8;
var L: cardinal;
begin
  if (self=nil) or (cardinal(index)>=cardinal(fCount)) then
    result := '' else begin
    L := GetLineSize(fLines[index],fMapEnd);
    if L<=fLineTextOffset then
      result := '' else
      FastSetString(result,PAnsiChar(fLines[index])+fLineTextOffset,L-fLineTextOffset);
  end;
end;

function TSynLogFile.EventString(index: integer; const replaceTabs: RawUTF8;
  maxutf8len: Integer; includeFirstColumns: boolean): string;
var tmp: RawUTF8;
    header: string;
begin
  tmp := GetEventText(index);
  if tmp = '' then begin
    result := '';
    exit;
  end;
  if maxutf8len>0 then
    Utf8TruncateToLength(tmp,maxutf8len);
  if replaceTabs<>'' then
    tmp := StringReplaceAll(tmp,#9,replaceTabs);
  if IsValidUTF8(pointer(tmp)) then
    result := UTF8ToString(tmp) else
    {$ifdef UNICODE}
    result := CurrentAnsiConvert.AnsiToUnicodeString(pointer(tmp),length(tmp));
    {$else}
    result := tmp;
    {$endif}
  if includeFirstColumns then begin
    UTF8DecodeToString(fLines[index],fLineTextOffset,header);
    result := header+result;
  end;
end;

procedure TSynLogFile.SetLogProcMerged(const Value: boolean);
var i: integer;
    P: ^TSynLogFileProc;
    O: TLogProcSortOrder;
begin
  fLogProcIsMerged := Value;
  O := fLogProcSortInternalOrder;
  if Value then begin
    if fLogProcMerged=nil then begin
      fLogProcCurrent := pointer(fLogProcNatural);
      fLogProcCurrentCount := fLogProcNaturalCount;
      LogProcSort(soByName); // sort by name to identify unique
      SetLength(fLogProcMerged,fLogProcNaturalCount);
      fLogProcMergedCount := 0;
      i := 0;
      P := pointer(fLogProcNatural);
      repeat
        with fLogProcMerged[fLogProcMergedCount] do begin
          repeat
            Index := P^.Index;
            inc(Time,P^.Time);
            inc(ProperTime,P^.ProperTime);
            inc(i);
            inc(P);
          until (i>=fLogProcNaturalCount) or
                (StrICompLeftTrim(PUTF8Char(fLines[LogProc[i-1].Index])+22,
                 PUTF8Char(fLines[P^.Index])+22)<>0);
        end;
        inc(fLogProcMergedCount);
      until i>=fLogProcNaturalCount;
      SetLength(fLogProcMerged,fLogProcMergedCount);
    end;
    fLogProcCurrent := pointer(fLogProcMerged);
    fLogProcCurrentCount := fLogProcMergedCount;
  end else begin
    fLogProcCurrent := pointer(fLogProcNatural);
    fLogProcCurrentCount := fLogProcNaturalCount;
  end;
  fLogProcSortInternalOrder := soNone;
  LogProcSort(O); // restore previous sort order
end;


function EventArchiveDelete(const aOldLogFileName, aDestinationPath: TFileName): boolean;
begin
  result := DeleteFile(aOldLogFileName);
end;

function EventArchiveSynLZ(const aOldLogFileName, aDestinationPath: TFileName): boolean;
begin // aDestinationPath = 'ArchivePath\log\YYYYMM\'
  Result := false;
  if (aOldLogFileName<>'') and FileExists(aOldLogFileName) then
  try
    if DirectoryExists(aDestinationPath) or CreateDir(aDestinationPath) then
      if FileSynLZ(aOldLogFileName,
         aDestinationPath+ExtractFileName(aOldLogFileName)+'.synlz',LOG_MAGIC) then
        result := DeleteFile(aOldLogFileName);
  except
    on Exception do
      result := false;
  end;
end;


{$ifndef DELPHI5OROLDER} // IInvokable was introduced with Delphi 6

{ TSynLogCallbacks }

constructor TSynLogCallbacks.Create(aTrackedLog: TSynLogFamily);
begin
  inherited Create;
  Registrations.Init(TypeInfo(TSynLogCallbackDynArray),Registration,@fCount);
  TrackedLog := aTrackedLog;
  aTrackedLog.EchoRemoteStart(self,OnEcho,false);
end;

destructor TSynLogCallbacks.Destroy;
begin
  if TrackedLog<>nil then
    if TrackedLog.fEchoRemoteClient=self then
      TrackedLog.EchoRemoteStop; // unregister OnEcho() event
  inherited Destroy;
end;

function TSynLogCallbacks.OnEcho(Sender: TTextWriter; Level: TSynLogInfo;
  const Text: RawUTF8): boolean;
var i: integer;
begin
  result := false;
  if (Count=0) or fCurrentlyEchoing then
    exit;
  Safe.Lock;
  try
    fCurrentlyEchoing := true; // avoid stack overflow if exception below
    for i := Count-1 downto 0 do
      if Level in Registration[i].Levels then
      try
        Registration[i].Callback.Log(Level,Text);
        result := true;
      except
        Registrations.Delete(i); // safer to unsubscribe ASAP
      end;
  finally
    fCurrentlyEchoing := false;
    Safe.UnLock;
  end;
end;

function TSynLogCallbacks.Subscribe(const Levels: TSynLogInfos;
  const Callback: ISynLogCallback; ReceiveExistingKB: cardinal): integer;
var Reg: TSynLogCallback;
    previousContent: RawUTF8;
begin
  if Assigned(Callback) then
  try
    if ReceiveExistingKB>0 then begin
      EnterCriticalSection(GlobalThreadLock);
      previousContent := TrackedLog.GetExistingLog(ReceiveExistingKB);
      if TrackedLog.HighResolutionTimestamp and (TrackedLog.fGlobalLog<>nil) then
        with TrackedLog.fGlobalLog do
        Callback.Log(sllNone,FormatUTF8('freq=%,%,%',[{$ifdef LINUX}1000000{$else}
          fFrequencyTimestamp{$endif},double(fStartTimestampDateTime),fFileName]));
      Callback.Log(sllNone,previousContent);
    end;
    Reg.Levels := Levels;
    Reg.Callback := Callback;
    Safe.Lock;
    try
      Registrations.Add(Reg);
    finally
      Safe.UnLock;
    end;
  finally
    if ReceiveExistingKB>0 then
      LeaveCriticalSection(GlobalThreadLock);
  end;
  result := length(previousContent);
end;

procedure TSynLogCallbacks.Unsubscribe(const Callback: ISynLogCallback);
var i: integer;
begin
  Safe.Lock;
  try
    for i := Count-1 downto 0 do
      if Registration[i].Callback=Callback then
        Registrations.Delete(i);
  finally
    Safe.UnLock;
  end;
end;

{$endif DELPHI5OROLDER}


{ TSynLogFileView }

procedure TSynLogFileView.LoadFromMap(AverageLineLength: integer);
begin
  inherited LoadFromMap(AverageLineLength);
  if fLevels<>nil then begin
    SetLength(fSelected,fCount);
    fSelectedCount := fCount;
    FillIncreasing(pointer(fSelected),0,fCount);
    SetLength(fThreadSelected,(fThreadMax shr 3)+1);
    SetAllThreads(true);
  end;
end;

procedure TSynLogFileView.AddInMemoryLine(const aNewLine: RawUTF8);
var index: integer;
    tm: cardinal;
begin
  tm := fThreadMax;
  inherited AddInMemoryLine(aNewLine);
  index := Count-1;
  if EventLevel[index] in fEvents then
    AddInteger(fSelected,fSelectedCount,index);
  if tm<>fThreadMax then begin
    tm := (fThreadMax shr 3)+1;
    if integer(tm)<>length(fThreadSelected) then
      SetLength(fThreadSelected,tm);
    SetBitPtr(pointer(fThreadSelected),fThreadMax-1)
  end;
end;

const
  TIME_FORMAT = 'hh:mm:ss.zzz';
  MAXLOGLINES = 300;

function TSynLogFileView.GetLineForMemo(aRow,aTop,aBottom: integer): string;
var tim: string;
    elapsed: TDateTime;
begin
  result := '';
  if cardinal(aRow)<cardinal(fSelectedCount) then
    aRow := fSelected[aRow];
  if cardinal(aRow)<cardinal(fCount) then begin
    result := EventString(aRow,'',0,true);
    if aBottom>aTop then begin
      elapsed := EventDateTime(aBottom)-EventDateTime(aTop);
      if Freq=0 then begin
        DateTimeToString(tim,TIME_FORMAT,elapsed);
        result := tim+#13#10+result;
      end else begin
        tim := IntToStr(trunc(elapsed*MSecsPerDay*1000) mod 1000);
        result := StringOfChar('0',3-length(tim))+tim+#13#10+result;
        DateTimeToString(tim,TIME_FORMAT,elapsed);
        result := tim+'.'+result;
      end;
      result := FormatString('% lines - time elapsed: %',[aBottom-aTop+1,result]);
    end;
  end;
end;

function TSynLogFileView.GetLineForClipboard(aRow: integer): string;
var dt: TDateTime;
begin
  result := '';
  if cardinal(aRow)<cardinal(fSelectedCount) then
    aRow := fSelected[aRow];
  if cardinal(aRow)<cardinal(fCount) then begin
    dt := EventDateTime(aRow);
    FormatString('% %'#9'%'#9,[DateToStr(dt),FormatDateTime(TIME_FORMAT,dt),
      _LogInfoCaption[EventLevel[aRow]]],result);
    if fThreads<>nil then
      result := result+IntToString(cardinal(fThreads[aRow]))+#9;
    result := result+EventString(aRow,'   ');
  end;
end;

function TSynLogFileView.GetCell(aCol, aRow: integer; out aLevel: TSynLogInfo): string;
begin
  aLevel := sllNone;
  result := '';
  if self<>nil then
    if cardinal(aRow)<cardinal(fSelectedCount) then begin
      aRow := fSelected[aRow];
      case aCol of
      0: DateTimeToString(result,TIME_FORMAT,EventDateTime(aRow));
      1: result := _LogInfoCaption[EventLevel[aRow]];
      2: if fThreads<>nil then
           result := IntToString(cardinal(fThreads[aRow]));
      3: result := EventString(aRow,'   ',MAXLOGLINES);
      end;
      aLevel := EventLevel[aRow];
    end else
      result := EventString(aRow,'   ',MAXLOGLINES);
end;

function TSynLogFileView.SearchNextEvent(aEvent: TSynLogInfo; aRow: integer): integer;
begin
  if cardinal(aRow)<cardinal(fSelectedCount) then begin
    // search from next item
    for result := aRow+1 to fSelectedCount-1 do
      if fLevels[fSelected[result]]=aEvent then
        exit;
    // search from beginning
    for result := 0 to aRow-1 do
      if fLevels[fSelected[result]]=aEvent then
        exit;
  end;
  result := -1;
end;

function TSynLogFileView.SearchNextText(
  const aPattern: RawUTF8; aRow, aDelta: integer): integer;
begin
  result := -1;
  if (self=nil) or (aPattern='') then
    exit;
  if fLevels=nil then begin // plain text search
    // search from next item
    for result := aRow+aDelta to fCount-1 do
      if LineContains(aPattern,result) then
        exit;
    // search from beginning
    for result := 0 to aRow-1 do
      if LineContains(aPattern,result) then
        exit;
  end else begin
    // search from next item
    for result := aRow+aDelta to fSelectedCount-1 do
      if LineContains(aPattern,fSelected[result]) then
        exit;
    // search from beginning
    for result := 0 to aRow-1 do
      if LineContains(aPattern,fSelected[result]) then
        exit;
  end;
  result := -1;
end;

function TSynLogFileView.SearchPreviousText(
  const aPattern: RawUTF8; aRow: integer): integer;
begin
  result := -1;
  if (self=nil) or (aPattern='') then
    exit;
  if fLevels=nil then begin // plain text search
    // search from previous item
    for result := aRow-1 downto 0 do
      if LineContains(aPattern,result) then
        exit;
    // search from end
    for result := fCount-1 downto aRow+1 do
      if LineContains(aPattern,result) then
        exit;
  end else begin
    // search from previous item
    for result := aRow-1 downto 0 do
      if LineContains(aPattern,fSelected[result]) then
        exit;
    // search from end
    for result := fCount-1 downto aRow+1 do
      if LineContains(aPattern,fSelected[result]) then
        exit;
  end;
  result := -1;
end;

function TSynLogFileView.SearchThread(aThreadID: word; aRow: integer): integer;
begin
  if (self<>nil) and (cardinal(aRow)<cardinal(fSelectedCount)) and (fThreads<>nil) then begin
    for result := aRow+1 to fSelectedCount-1 do
      if fThreads[fSelected[result]]=aThreadID then
        exit;
    for result := 0 to aRow-1 do
      if fThreads[fSelected[result]]=aThreadID then
        exit;
  end;
  result := -1;
end;

function TSynLogFileView.SearchNextThread(aRow: integer): integer;
var currentThreadID: Word;
begin
  if (self<>nil) and (cardinal(aRow)<cardinal(fSelectedCount)) and (fThreads<>nil) then begin
    result := aRow;
    currentThreadID := fThreads[fSelected[result]];
    repeat
      inc(result);
      if result=fSelectedCount then
        break;
      if fThreads[fSelected[result]]<>currentThreadID then
        exit; // found
    until false;
  end;
  result := -1;
end;

function TSynLogFileView.SearchNextSameThread(aRow: integer): integer;
var currentThreadID: Word;
begin
  if (self<>nil) and (cardinal(aRow)<cardinal(fSelectedCount)) and (fThreads<>nil) then begin
    result := aRow;
    currentThreadID := fThreads[fSelected[result]];
    repeat
      inc(result);
      if result=fSelectedCount then
        break;
      if fThreads[fSelected[result]]=currentThreadID then
        exit; // found
    until false;
  end;
  result := -1;
end;

function TSynLogFileView.SearchPreviousSameThread(aRow: integer): integer;
var currentThreadID: Word;
begin
  if (self<>nil) and (cardinal(aRow)<cardinal(fSelectedCount)) and (fThreads<>nil) then begin
    result := aRow;
    currentThreadID := fThreads[fSelected[result]];
    repeat
      dec(result);
      if result<0 then
        break;
      if fThreads[fSelected[result]]=currentThreadID then
        exit; // found
    until false;
  end;
  result := -1;
end;

function TSynLogFileView.SearchEnterLeave(aRow: integer): integer;
var Level,ndx: integer;
    currentThreadID: Word;
begin
  if (self=nil) or (cardinal(aRow)>=cardinal(fSelectedCount)) then begin
    result := -1;
    exit;
  end;
  Level := 0;
  result := aRow;
  ndx := fSelected[result];
  if EventThread<>nil then
    currentThreadID := EventThread[ndx] else
    currentThreadID := 0;
  case EventLevel[ndx] of
  sllEnter: // retrieve corresponding Leave event
    repeat
      inc(result);
      if result>=fSelectedCount then
        break;
      ndx := fSelected[result];
      case EventLevel[ndx] of
      sllEnter:
        if (currentThreadID=0) or (EventThread[ndx]=currentThreadID) then
          inc(Level);
      sllLeave:
        if (currentThreadID=0) or (EventThread[ndx]=currentThreadID) then
          if Level=0 then
            exit else
            dec(Level);
      end;
    until false;
  sllLeave: // retrieve corresponding Enter event
    repeat
      dec(result);
      if result<0 then
        break;
      ndx := fSelected[result];
      case EventLevel[ndx] of
      sllLeave:
        if (currentThreadID=0) or (EventThread[ndx]=currentThreadID) then
          inc(Level);
      sllEnter:
        if (currentThreadID=0) or (EventThread[ndx]=currentThreadID) then
          if Level=0 then
            exit else
            dec(Level);
      end;
    until false;
  end;
  result := -1;
end;

function TSynLogFileView.SearchNextSelected(aIndex: integer): integer;
begin
  for result := 0 to fSelectedCount-1 do
    if fSelected[result]>=aIndex then
      exit; // TODO: use faster binary search instead of this O(n) value?
  result := -1;
end;

function TSynLogFileView.Select(aRow: integer): integer;
var i, search: integer;
begin
  result := 0;
  if integer(fEvents)<>0 then begin
    if cardinal(aRow)<cardinal(fSelectedCount) then
      search := fSelected[aRow] else
      search := maxInt;
    fSelectedCount := 0;
    for i := 0 to Count-1 do
      if fLevels[i] in fEvents then
        if (fThreads=nil) or GetBitPtr(pointer(fThreadSelected),fThreads[i]-1) then begin
          if search<=i then begin
            result := fSelectedCount; // found the closed selected index
            search := maxInt;
          end;
          if fSelectedCount=length(fSelected) then
            SetLength(fSelected,NextGrow(fSelectedCount));
          fSelected[fSelectedCount] := i;
          inc(fSelectedCount);
        end;
  end;
end;

procedure TSynLogFileView.SetAllThreads(enabled: boolean);
const B: array[boolean] of byte = (0, 255);
begin
  FillcharFast(fThreadSelected[0],length(fThreadSelected),B[enabled]);
end;

procedure TSynLogFileView.SetThreads(thread: integer; value: boolean);
begin
  dec(thread);
  if cardinal(thread)<fThreadMax then
    if value then
      SetBitPtr(pointer(fThreadSelected),thread) else
      UnSetBitPtr(pointer(fThreadSelected),thread);
end;

function TSynLogFileView.GetThreads(thread: integer): boolean;
begin
  dec(thread);
  result := (cardinal(thread)<fThreadMax) and GetBitPtr(pointer(fThreadSelected),thread);
end;

const
  _TSynMapSymbol = 'Name:RawUTF8 Start,Stop:integer';
  _TSynMapUnit = 'Symbol:TSynMapSymbol FileName:RawUTF8 Line,Addr:TIntegerDynArray';

initialization
  assert(ord(sfLocal7)=23);
  assert(ord(ssDebug)=7);
  InitializeCriticalSection(GlobalThreadLock);   // deleted with the process
  SynLogFamily := TSynObjectList.Create;         // TSynLogFamily instances
  SynLogFileList := TSynObjectListLocked.Create; // TSynLog instances
  {$ifndef NOEXCEPTIONINTERCEPT}
  DefaultSynLogExceptionToStr := InternalDefaultSynLogExceptionToStr;
  {$endif}
  GetEnumTrimmedNames(TypeInfo(TSynLogInfo),@_LogInfoText);
  GetEnumCaptions(TypeInfo(TSynLogInfo),@_LogInfoCaption);
  _LogInfoCaption[sllNone] := '';
  TTextWriter.RegisterCustomJSONSerializerFromText([
    TypeInfo(TSynMapSymbol),_TSynMapSymbol,
    TypeInfo(TSynMapUnit),_TSynMapUnit]);

finalization
  SynLogFileList.Free; // release in proper order: TSynLog then TSynLogFamily
  SynLogFamily.Free;
  {$ifndef NOEXCEPTIONINTERCEPT}
  GlobalCurrentHandleExceptionSynLog := nil; // paranoid
  {$endif}
end.
