[Class] LogClass - Logging and log file management class

Post your working scripts, libraries and tools for AHK v1.1 and older
ahbi
Posts: 17
Joined: 15 Jun 2016, 17:11

[Class] LogClass - Logging and log file management class

15 Jun 2018, 23:14

This class makes working with a log file much easier. Once created, you can write to a log file with a single method call.
Each instance of the object supports a single log file (and its inactive older versions). To use multiple log files (with separate names) create multiple instances.
The log files will be tidied up and rotated automatically.
The current log will be titled with just the base filename (e.g., MyLogFile.log) with older, inactive log files being named with a trailing index (e.g., MyLogFile_1.log, MyLogFile_2.log). Newer files having a higher index.
Log files may be limited by size, and when the threshold is reached the file will be moved to an inactive filename and a new log started.

A brief usage example is as follows:

Code: Select all

	global log := new LogClass(“MyLogFile”)
	log.initalizeNewLogFile(false, “Header text to appear at the start of the new file”)
	log.addLogEntry(“Your 1st message to put in the log file”)
	log.addLogEntry(“Your next message to put in the log file”)
	log.finalizeLog(“Footer text to appear at the very end of your log, which you are done with.”)
This LogClass is inspired (very much so) by the pre-AHK built-in classes (circa 2009) file Log.ahk
by Ben McClure, and found at
From: https://autohotkey.com/board/topic/3795 ... t-library/
Ben's Log.ahk required the Class.ahk, Vector.ahk, and String.ahk which are no longer available (and outdated), and hence was a dead piece of code.

Also a thank you to nnnik's excellent work at: https://autohotkey.com/boards/viewtopic ... 74&t=48740


The <name>String properties (e.g., preEntryString, HeaderString, etc.) may be set via initalizeNewLogFile() or individually.
They are strings that will be applied automatically when creating/ending a log file, or adding a new log message/entry. This allows you to apply a common format (or information) to every logfile or log entry.
The String properties all accept variables which may be expanded when written.
  • $time or %A_Now% expands to the time the log entry is written (as formatted according to this.logsTimeFormat).
  • $printStack expands to a list of the function calls which caused the log entry to be written.
  • %Var% expands to whatever value variable Var is, but Var must be a global/built-in variable.
These String property variables may be used in the log entries too (e.g., by this.addLogEntry(entry))

The class is as follows:

Code: Select all


class LogClass
{ ; Class that handles writing to a log file
	; This class makes working with a log file much easier.  Once created, you can write to a log file with a single method call.
	; Each instance of the object supports a single log file (and its inactive older versions).  To use multiple log files (with separate names) create multiple instances.
	; The log files will be tidied up and rotated automatically.
	; The current log will be titled with just the base filename (e.g., MyLogFile.log) with older, inactive log files being named with a trailing index (e.g., MyLogFile_1.log, MyLogFile_2.log).  Newer files having a higher index.  
	; Log files may be limited by size, and when the threshold is reached the file will be moved to an inactive/indexed filename and a new log started.  
	;
	; A brief usage example is as follows:
	;	global log := new LogClass(“MyLogFile”)
	;	log.initalizeNewLogFile(false, “Header text to appear at the start of the new file”)
	;	log.addLogEntry(“Your 1st message to put in the log file”)
	;	log.addLogEntry(“Your next message to put in the log file”)
	;	log.finalizeLog(“Footer text to appear at the very end of your log, which you are done with.”)
	;
	; This LogClass is inspired (very much so) by the pre-AHK built-in classes (circa 2009) file Log.ahk
	; by Ben McClure, and found at
	; From: https://autohotkey.com/board/topic/37953-class-log-logging-and-log-file-management-library/
	; Ben's Log.ahk required the Class.ahk, Vector.ahk, and String.ahk which are no longer available (and outdated), and hence was a dead piece of code.
	;
	; The <name>String properties (e.g., preEntryString, HeaderString, etc.) may be set via initalizeNewLogFile() or individually.  
	; They are strings that will be applied automatically when creating/ending a log file, or adding a new log message/entry.  This allows you to apply a common format (or information) to every logfile or log entry.
	; The String properties all accept variables which may be expanded when written.
		; * $time or %A_Now% expands to the time the log entry is written (as formatted according to this.logsTimeFormat).
		; * $printStack expands to a list of the function calls which caused the log entry to be written.
		; * %Var% expands to whatever value variable Var is, but Var must be a global/built-in variable.
	; These String property variables may be used in the log entries too (e.g., by this.addLogEntry(entry))
	; (c) 2018 Ahbi Santini


	__New(aLogBaseFilename, aMaxNumbOldLogs=0, aMaxSizeMBLogFile=-1, aLogDir="", aLogExten="")
	{
		; Input variables
		; 	aLogBaseFilename -> 1/3 of the log's full filename (filename -> aLogDir\aLogBaseFilename.aLogExten)
		; 	                    Note: If aLogBaseFilename is blank (e.g., ""), then the ScriptName without the exten is used (e.g., "MyScript.ahk" results in a base name of "MyScript"), which is probably the easiest.
		; 	                    Note: If aLogBaseFilename is fully pathed (e.g., C:\Logs\MyLogFile.log) aLogDir and aLogExten are ignored.
		; 	aMaxNumbOldLogs -> The maximum number of old log files to keep when rotating/tidying up the log files.  -1 is infinite.  0 (the default) will not create any indexed (e.g., MyLogFile_1.log) files.
		; 	aMaxSizeMBLogFile -> The maximum size (in megabytes) before a log file is automatically closed and rotated to a new file.  0 or less is infinite (the default).  
		; 	aLogDir -> 1/3 of the log's full filename (filename -> aLogDir\aLogBaseFilename.aLogExten).  The default is A_WorkingDir.
		; 	aLogExten -> 1/3 of the log's full filename (filename -> aLogDir\aLogBaseFilename.aLogExten).  The default is "log".
		
		this._classVersion := "2018-09-07"
		
		; CONSTANTS
		; establish any default values (These defaults aren't supposed to be user editable. Treat them as programmer's Internal-Use-Only configuration constants)
		this.maxNumbOldLogs_Default     := 0  ; -1 is infinite, 0 is only the current file (NOTE: If initalizeNewLogFile(overwriteExistingFile = -1) this is pretty much ignored except for tidy())
		this.maxSizeMBLogFile_Default   := -1 ; 0 or less than (-1) is unlimited size
		this.logDir_Default             := A_WorkingDir
		this.logExten_Default           := "log"
		this.logsFileEncoding_Default   := "UTF-8"
		this.logsTimeFormat_Default     := "yyyy-MM-dd hh:mm:ss tt"
		this.preEntryString_Default     := ""
		this.postEntryString_Default    := ""
		this.headerString_Default       := ""
		this.footerString_Default       := ""
		this.useRecycleBin_Default      := true
		this.printStackMaxDepth_Default := 5 ; maximum number of parent function calls to include the PrintStack function
		this.isAutoWriteEntries_Default := true
		this.dividerString_Default      := ""

		; actual working variables
		; initialize any properties (not received as inputs)
		this.logsFileEncoding      := this.logsFileEncoding_Default
		this.logsTimeFormat        := this.logsTimeFormat_Default
		this.preEntryString        := this.preEntryString_Default
		this.postEntryString       := this.postEntryString_Default
		this.headerString          := this.headerString_Default
		this.footerString          := this.footerString_Default
		this.useRecycleBin         := this.useRecycleBin_Default
		this.printStackMaxDepth    := this.printStackMaxDepth_Default
		this.isAutoWriteEntries    := this.isAutoWriteEntries_Default
		this.isLogClassTurnedOff   := false
		this._pendingEntries       := []
		this._ignorePendingEntries := false
		this.dividerString         := this.createDividerString("-", 70, true)
		; this.scriptEnvReport       := this.createScriptEnvReport() ; this actually has to be run last or else it doesn't have the values set properly

		; Error checking done in property get/set
		this.maxNumbOldLogs := aMaxNumbOldLogs
		this.maxSizeMBLogFile := aMaxSizeMBLogFile 

		; now error check the filename input values
		if(aLogDir="")
			aLogDir := this.logDir_Default
		if(aLogExten="")
			aLogExten := this.logExten_Default
		if(aLogBaseFilename="")
		{ ; use the ScriptName without the extension (i.e., *.ahk or *.exe)
			SplitPath, A_ScriptFullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			aLogBaseFilename := OutNameNoExt
		}
		else if(this.isFilenameFullyPathed(aLogBaseFilename))
		{ ; ignore the other given filename inputs
			SplitPath, aLogBaseFilename, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			aLogDir := OutDir ; overwrite any aLogDir we were given as input
			aLogExten := OutExtension  ; overwrite any aLogExten we were given as input
			aLogBaseFilename := OutNameNoExt ; now it looks like the normal aLogDir . "\" . aLogBaseFilename . "." aLogExten triplet
		}

		; put the filename parts together
		this._currentFileName_FullPath := aLogDir . "\" . aLogBaseFilename . "." . aLogExten

		; this actually has to be run last or else it doesn't have the values set properly
		this.scriptEnvReport       := this.createScriptEnvReport()
		return this
	}
	
	; Properties
	; ---------------------------------------------
	;BeginRegion
	
	classVersion
	{ ; (Read-Only)  Just the revision/version number of this class
		get
		{
			return this._classVersion
		}
	}

	; List
	; -------------------
	; currentFileName_FullPath ; (Read-Only) The current log filename, in full path form.  All other filename related properties read/write to this as the master/keeper of the filename 
	; preEntryString           ; A string to prepend (before) to any log entry (made via addLogEntry()).
	; postEntryString          ; A string to append (after) to any log entry (made via addLogEntry()).
	; headerString             ; A string to be placed at the start of every new logfile (made via initalizeNewLogFile()).
	; footerString             ; A string to be placed at the end of every new logfile (made via finalizeLog()).
	; logsFileEncoding         ; The encoding for the logfile (see AHK help page on FileEncoding).  The default is UTF-8
	; logsTimeFormat           ; The format that $time or %A_Now% will be expanded to (see AHK help page on FormatTime).  The default is "yyyy-MM-dd hh:mm:ss tt"
	; maxNumbOldLogs           ; The maximum number of old (i.e., indexed; e.g., MyLogFile_1.log) files to keep when rotating/cleaning the logs.  (NOTE: If initalizeNewLogFile(overwriteExistingFile = -1) this is pretty much ignored except for tidy())
	; maxSizeMBLogFile         ; The maximum size (in megabytes) of the log file before a new file is automatically created.  The size is not super-strict.  If you really try you can break this, especially is headerString creates a file bigger than your size limit.
	; useRecycleBin            ; Should deleted files be deleted or moved to the RecycleBin?  The default is true (to the Recycle Bin).
	; isAutoWriteEntries       ; New log entries can be written immediately (the default), or saved up in the pendingEntries array (to be written later).  Useful if you want to limit the number of times a logfile is edited in a given time period (e.g., if your logfile is in a cloud synched folder).  If changed (to true) while entries are pending, they will all be written on next addLogEntry().
	; isLogClassTurnedOff      ; When true, pretty much every function instantly exits ** with NO error value ** (so no entries may be added/files moved).  (The default is: false).  Useful for turning off logging ability, especially when your addLogEntry() calls are buried deep in some class/function and you don't want to hunt them down and comment them out.
	; printStackMaxDepth       ; When expanding the String property variable $printStack how far back in call chain should be reported?  The default is 5
	; dividerString            ; a default string to use as a divider sting.  Created and (may be) set by this.createDividerString()
	; scriptEnvReport          ; a default string that can be used as the headerString.  Created and (may be) set by this.createScriptEnvReport()
	; _pendingEntries[]        ; (Internal use only)  The array of unwritten log entries.
	; _ignorePendingEntries    ; (Internal use only)  If when writing the unwritten log entries a new file needs to be created (due to size), this flags initalizeNewLogFile()/finalizeLog() to not reset the pendingEntries[]

	; not yet implemented (and maybe never will be)
	; maxNumbLogEntries ; before a new logfile
	; numberCurrentLogEntries ; (includes pending) read-only
	; maxPendingEntries ; don't let pending entries get out of control
	
	maxNumbOldLogs
	{ ; The maximum number of old (i.e., indexed; e.g., MyLogFile_1.log) files to keep when rotating/cleaning the logs
		get
		{
			return this._maxNumbOldLogs
		}
		set
		{
			; -1 is infinite, 0 is only the current file
			aMaxNumbOldLogs:= value
			if aMaxNumbOldLogs is not integer
			{
				aMaxNumbOldLogs := this.maxNumbOldLogs_Default
				; Note this default is a constant (as much as AHK has them) and set via New()
			}
			return this._maxNumbOldLogs := aMaxNumbOldLogs 
		}
	}
	
	maxSizeMBLogFile
	{ ; The maximum size (in megabytes) of the log file before a new file is automatically created.  The size is not super-strict.  If you really try you can break this, especially is headerString creates a file bigger than your size limit.
		get
		{
			return this._maxSizeMBLogFile
		}
		set
		{ ; 0 or less than (-1) is unlimited size
			
			aMaxSizeMBLogFile:= value
			if aMaxSizeMBLogFile is not number
			{ 
				aMaxSizeMBLogFile := this.maxSizeMBLogFile_Default
				; Note this default is a constant (as much as AHK has them) and set via New()
			}
			if (aMaxSizeMBLogFile = 0 ) 
			{ ; a value of 0 can be confusing, set it to -1 which means the same thing (infinite size)
				aMaxSizeMBLogFile := -1
			}
			return this._maxSizeMBLogFile := aMaxSizeMBLogFile 
		}
	}
	
	printStackMaxDepth
	{ ; When expanding the String property variable $printStack how far back in call chain should be reported?  The default is 5
		get
		{
			return this._printStackMaxDepth
		}
		set
		{
			aPrintStackMaxDepth:= value
			if aPrintStackMaxDepth is not digit
			{
				aPrintStackMaxDepth := this.printStackMaxDepth_Default
				; Note this default is a constant (as much as AHK has them) and set via New()
			}
			return this._printStackMaxDepth := aPrintStackMaxDepth 
		}
	}
	
	;EndRegion

	; Pseudo-Properties (actually functions that look like properties)
	; ---------------------------------------------
	;BeginRegion
	
	baseFileName
	{ ; (Read-Only) The current log file's filename w/o extension or trailing index (e.g., MyLogFile_1.log would be "MyLogFile").
		get
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutNameNoExt
		}
		set
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutNameNoExt
		}
	}
	
	currentFileName
	{ ; (Read-Only) The current log file's filename, w/o path information.
		get
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutFileName
		}
		set
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutFileName
		}
	}
	
	currentFileName_FullPath
	{ ; (Read-Only) The current log file's filename, w/ path information.  This property is set via New()
		get
		{
			return this._currentFileName_FullPath
		}
		set
		{
			return this._currentFileName_FullPath
		}
	}
	
	logDir
	{ ; (Read-Only) The current log file's directory.
		get
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutDir
		}
		set
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutDir
		}
	}
	
	logExten
	{ ; (Read-Only) The current log file's extension (e.g., *.log).  
		get
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutExtension
		}
		set
		{
			aCurrentFileName_FullPath := this.currentFileName_FullPath
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			return OutExtension
		}
	}

	;EndRegion
	
	
	
	; Methods (of suitable importance)
	; ---------------------------------------------
	;BeginRegion
	
	initalizeNewLogFile(overwriteExistingFile=0, aHeaderString="UNDEF", aPreEntryString="UNDEF", aPostEntryString="UNDEF", aFooterString="UNDEF", aLogsFileEncoding="UNDEF")
	{ ; Start a new log file, and set (if given) any of the predefined string properties. This method will rotate/move any old log files, as needed.  For the Header/Footer isAutoWriteEntries is ignored (always instantly writes)
	; Input variables
	; 	overwriteExistingFile (-1,0, or 1) -> should a pre-existing log file be appended to (-1), a new file created (0), or overwritten (1) ... (creating as new file moves the older files to the end; e.g., MyLogFile_1.log)?
	; 	aHeaderString     -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file)
	; 	aPreEntryString   -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file)
	; 	aPostEntryString  -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file)
	; 	aFooterString     -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file)
	; 	aLogsFileEncoding -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file)
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
	
		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		aLogDir := this.logDir
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		errLvl := false
		
		; UNDEF is used so the properties may have a "" value
		if(aHeaderString != "UNDEF")
			this.headerString := aHeaderString
		if(aPreEntryString != "UNDEF")
			this.preEntryString := aPreEntryString
		if(aPostEntryString != "UNDEF")
			this.postEntryString := aPostEntryString
		if(aFooterString != "UNDEF")
			this.footerString := aFooterString
		if(aLogsFileEncoding != "UNDEF")
			this.logsFileEncoding := aLogsFileEncoding
		if(!this._ignorePendingEntries)
			this._pendingEntries := []
		this._ignorePendingEntries := false ; reset IgnoreVal regardless if it was set or not

		if (!InStr(FileExist(aLogDir), "D"))
		{ ; check if Dir exits
			FileCreateDir, %aLogDir%
			errLvl := ErrorLevel
		}
		else if( FileExist(aCurrentFileName_FullPath) )
		{ ; if the Dir exists, check if the file exists and handle accordingly
			if (overwriteExistingFile == 1)
			{
				FileDelete, %aCurrentFileName_FullPath%
				errLvl := ErrorLevel
			}
			else if (overwriteExistingFile == 0)
			{
				errLvl += this.moveLog()
				errLvl += this.tidyLogs()
			}
			else ; (overwriteExistingFile == -1)
			{
				; append to file
			}
		}
		
		if(!errLvl)
		{ ; now write the header string
			aLogFileEncoding := this.logsFileEncoding
			aHeaderString := this.headerString
			aHeaderString := this.transformStringVars(aHeaderString) ; expand any variables in the string
			; write automatically regardless of this.isAutoWriteEntries
			; if aHeaderString=="", you're essentially touching the file (unix's touch command)
			errLvl := this.appendFile(aHeaderString, aCurrentFileName_FullPath, aLogFileEncoding, false)
		}
		return errLvl
	}
	
	finalizeLog(aFooterString="UNDEF", willTidyLogs=true)
	{ ; End a  log file, and set (if given) the predefined string property. This method will rotate/move any old log files, as needed.  For the Header/Footer isAutoWriteEntries is ignored (always instantly writes)
		; Note: "End" should not be confused with File.close().  File.close() is done every time an entry is written to the log file.  We're not sitting here with an open File the entire time you're using the LogClass. 
	; Input variables
	; 	aFooterString -> If given, sets the corresponding property.  If not given, the corresponding property stays the same (e.g., from your last log file or initalizeNewLogFile())
	; 	willTidyLogs  -> Should the function run tidyLog() or not.  Set to False if you want to save the log file and append to it next time.
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
	
		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		errLvl := false
		aLogDir := this.logDir
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		aMaxNumbOldLogs := this.maxNumbOldLogs

		; UNDEF is used so the properties may have a "" value
		if(aFooterString != "UNDEF")
			this.footerString := aFooterString
		
		if (!InStr(FileExist(aLogDir), "D"))
		{ ; check if Dir exits
			FileCreateDir, %aLogDir%
			errLvl := ErrorLevel
		}

		if(!errLvl)
		{ ; write any pending entries & footer to the file
			if(!this._ignorePendingEntries)
			{ ; if this got called do to a log exceeding its size limit, we'll saving the pending entries for the next log file
				errLvl := this.savePendingEntriesToLog()
				; if a large enough number of entries are pending multiple log files may be written
			}
			
			aLogFileEncoding := this.logsFileEncoding
			aFooterString := this.footerString
			aFooterString := this.transformStringVars(aFooterString)
			; write automatically regardless of this.isAutoWriteEntries
			errLvl += this.appendFile(aFooterString, aCurrentFileName_FullPath, aLogFileEncoding)
		}

		; clean up the multiple log files
		if( (aMaxNumbOldLogs > 0) && (willTidyLogs) )
		{ ; if we only have 1 log file, then leave it alone, also leave alone if you wish to append to a large log file (which can create new if the size exceeds the max).
			errLvl += this.moveLog()
			errLvl += this.tidyLogs()
		}
		
		this._ignorePendingEntries := false ; reset IgnoreVal regardless if it was set or not
		return errLvl
	}
	
	addLogEntry(entryString="", addNewLine=true, usePreEntryString=true, usePostEntryString=true)
	{ ; Adds a new entry (string) to the log file (or pending entries array, see aIsAutoWriteEntries).  Creates the entry as preEntryString . entryString . postEntryString
	; The <name>String properties (e.g., preEntryString, HeaderString, etc.) may be set via initalizeNewLogFile() or individually.  
	; They are strings that will be applied automatically when creating/ending a log file or adding a new log message/entry.  This allows you to apply a common format (or information) to every logfile or log entry.
	; The String properties all accept variables which may be expanded when written.
		; * $time or %A_Now% expands to the time the log entry is written (as formatted according to this.logsTimeFormat).
		; * $printStack expands to a list of the function calls which caused the log entry to be written.
		; * %Var% expands to whatever value variable Var is, but Var must be a global/built-in variable.
	; These String property variables may be used in entryString too
	; -----------------
	; Input variables
	; 	entryString          -> The string you wish to write to the log.  Will be pre/post-appended as dictated by the other inputs
	; 	addNewLine (Boolean) -> Should a "`n" be added to the end of entryString or not (useful if use 1-line entries)?  Default is true.
	; 	usePreEntryString (Boolean) -> Should preEntryString by added before entryString?  Default is true.
	; 	usePostEntryString (Boolean) -> Should postEntryString by added after entryString?  Default is true.
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)

		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		aPreEntryString := this.preEntryString
		aPostEntryString := this.postEntryString
		aIsAutoWriteEntries := this.isAutoWriteEntries
		errLvl := false

		; transform (unpack the variables) in each string
		; then concatenate them together, as desired
		entryString := this.transformStringVars(entryString)
		if(usePreEntryString)
		{
			aPreEntryString := this.transformStringVars(aPreEntryString)
			entryString := aPreEntryString . entryString
		}
		if(addNewLine)
		{
			entryString := entryString . "`n"
		}
		if(usePostEntryString)
		{
			aPostEntryString := this.transformStringVars(aPostEntryString)
			entryString := entryString . aPostEntryString
		}
		
		; now add the entry to file/array
		; add everything to the array, then if auto writing, write out the array
		retVal := this._pendingEntries.push(entryString) ; God I love push/pop. I don't know why but I have loved those 2 functions for decades now.
		if(aIsAutoWriteEntries) 
		{ ; if auto writing, write out the array
			errLvl := this.savePendingEntriesToLog()
		}
		return errLvl
	}
	
	savePendingEntriesToLog()
	{ ; Writes the pending entries to the log file, creating more log files if needed
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		aLogDir := this.logDir
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		aLogFileEncoding := this.logsFileEncoding
		aMaxSizeMBLogFile := this.maxSizeMBLogFile
		errLvl := false

		if (!InStr(FileExist(aLogDir), "D"))
		{ ; check if Dir exits
			FileCreateDir, %aLogDir%
			errLvl := ErrorLevel
		}
		
		if(!errLvl)
		{
			arrayLength := this._pendingEntries.length()
			Loop, %arrayLength% ; this should be a contiguous array 
			{ ; start writing out the array
				; is the log file too big to add more to it?
				if(aMaxSizeMBLogFile > 0) ; < or = 0 is unlimited size
				{
					if(FileExist(aCurrentFileName_FullPath)) ; FileGetSize fails if the file doesn't exist
					{ 
						FileGetSize, logSize, %aCurrentFileName_FullPath% ; get in bytes because the K & M options return integers and this annoys me
						if(!ErrorLevel) 
						{
							logSizeMB := this.byteToMB(logSize)
							if(logSizeMB > aMaxSizeMBLogFile)
							{  ; close the file and start a new one
								this._ignorePendingEntries := true ; don't let finalizeLog()/initalizeNewLogFile() clear the array (array := [])
								errLvl += this.finalizeLog()
								this._ignorePendingEntries := true ; this got cleared by finalizeLog()
								errLvl += this.initalizeNewLogFile() ; this does move & tidy too
							}
						}
					}
				}
				
				; write the next entry
				minIndex := this._pendingEntries.MinIndex() ; rename for easier handling
				entryString := this._pendingEntries.RemoveAt(minIndex) ; I want this to be called shift().  If I have push/pop, I want shift/unshift. :(
				try {
					; I know this.appendFile() has a very similar name to the old FileAppend command
					; but this.appendFile() is really using the File.Write() object (unless you changed it)
					errLvl += this.appendFile(entryString, aCurrentFileName_FullPath, aLogFileEncoding)
				} catch e {
					
					; sometimes the file can be written to too quickly, if you're pounding it with new entries
					; try again after a rest
					; hopefully moving to the File Object (from FileAppend) solves this
					MsgBox, 16,, % "Before we try again...`nUnable to write to log file!" "`n" "`t" "(Possibly written to too fast?)" "`n`n" "Filename: " aCurrentFileName_FullPath "`n" "LogEntry: " entryString "`n" "Exception.message: " e.message "`n" "Exception.Extra: " e.extra "`n`n`n" "------------------" "`n" . this.printStack(10)
					Sleep 500
					try {
						errLvl += this.appendFile(entryString, aCurrentFileName_FullPath, aLogFileEncoding)
					} catch e {
						MsgBox, 16,, % "Unable to write to log file!" "`n" "`t" "(Possibly written to too fast?)" "`n`n" "Filename: " aCurrentFileName_FullPath "`n" "LogEntry: " entryString "`n" "Exception.message: " e.message "`n" "Exception.Extra: " e.extra "`n`n`n" "------------------" "`n" . this.printStack(10)
					}
				}
			}
		}
		return errLvl
	}

	moveLog(aNewLogFilename="", aNewLogDir="", overwriteExistingFile=false)
	{ ; Moves the current log to a new filename.  Will place it at the end of the index chain (e.g., MyLogFile_1.log), or to the name given via input variables.
	; Input variables
	; 	aNewLogFilename -> The filename to move current log to.  If blank, will place it at the end of the index chain (e.g., MyLogFile_1.log).  If a fully pathed filename, aNewLogDir is ignored.
	; 	aNewLogDir      -> The directory to move current log to.  The full filename's path will be aNewLogDir\aNewLogFilename (unless the caveats described above apply)
	; 	overwriteExistingFile (Boolean) -> Should any existing files be overwritten?  
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)

	if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		aCurrentFileName_FullPath := this.currentFileName_FullPath ; source file
		errLvl := false

		; check that the source file actually exists
		errLvl := !FileExist(aCurrentFileName_FullPath)
		if(!errLvl)
		{
			; now error check the input values
			if(this.isFilenameFullyPathed(aNewLogFilename))
			{
				SplitPath, aNewLogFilename, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
				aNewLogDir := OutDir ; overwrite any aNewLogDir we were given as input
				aNewLogFilename := OutFileName ; now it looks like the normal aNewLogDir . "\" . aNewLogFilename pair
			}
			else if (aNewLogDir = "")
			{ ; if aNew is fully pathed, this will be skipped
				aNewLogDir := this.logDir
			}

			if (!InStr(FileExist(aNewLogDir), "D"))
			{ ; check if Dir exits
				FileCreateDir, %aNewLogDir%
				errLvl := ErrorLevel
			}
			
			if(!errLvl)
			{
				if(aNewLogFilename = "") ; destination file
				{ ; if "", move log to end of A_Index chain (e.g., MyLogFile_1.log)
					SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
					
					Loop
					{
						candidateFilename := aNewLogDir . "\" . OutNameNoExt . "_" . A_Index . "." OutExtension
						if (!FileExist(candidateFilename))
							break
					}
					aNewLogFilename_FullPath := candidateFilename ; set to whichever candidate won
				}
				else
				{ ; if != "", move to explicitly given destination file
					aNewLogFilename_FullPath := aNewLogDir . "\" . aNewLogFilename
				}
				
				; Move the file
				FileMove, %aCurrentFileName_FullPath%, %aNewLogFilename_FullPath%, %overwriteExistingFile%
				errLvl := ErrorLevel
			}
		}
		return errLvl
	}
	
	tidyLogs(aMaxNumbOldLogs="")
	{ ; Rotates logs (newest is highest in the index chain (e.g., MyLogFile_1.log)).  Deletes oldest logs (and re-numbers the less-old) until there are less than aMaxNumbOldLogs/maxNumbOldLogs.
	; Input variables
	; 	aMaxNumbOldLogs -> The maximum number of old log files to keep.  If not blank, sets this.maxNumbOldLogs.
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)

		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		aUseRecycleBin := this.useRecycleBin
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		errLvl := 0

		if (aMaxNumbOldLogs = "")
			aMaxNumbOldLogs := this.maxNumbOldLogs
		
		if (aMaxNumbOldLogs >= 0) ; -1 means keep all old logs
		{
			SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
			; count the number of files (yeah, a discontinuity in the numbering system breaks this (e.g., 1, 2, 3, 7, 8 ... will stop at 3)
			Loop
			{
				candidateFilename := OutDir . "\" . OutNameNoExt . "_" . A_Index . "." OutExtension
				doesExist := FileExist(candidateFilename)
				if (!doesExist)
					break
				totalNumbOldLogs := A_Index
			}

			; rotate/delete the files
			new_Index := 0
			if(totalNumbOldLogs > aMaxNumbOldLogs)
			{
				numbLogsToDelete := totalNumbOldLogs - aMaxNumbOldLogs
				Loop, %totalNumbOldLogs%
				{
					oldFilename := OutDir . "\" . OutNameNoExt . "_" . A_Index . "." OutExtension
					if(FileExist(oldFilename))
					{
						if(A_Index <= numbLogsToDelete)
						{ ; delete the older/lower numbered files (makes way for the next step)
							errLvl1 := this.deleteLog(oldFilename, aUseRecycleBin)
							errLvl := errLvl + errLvl1
						}
						else
						{ ; move the newer/higher numbered files to a lower number (they are now older)
							new_Index++
							newFilename := OutDir . "\" . OutNameNoExt . "_" . new_Index . "." OutExtension
							FileMove, %oldFilename%, %newFilename%, 1 ; overwrite if file still exists (will probably fail if that is the case, or else FileDelete would have handled it)
							errLvl := errLvl + ErrorLevel
						}
					}
				}
			}
		}
		return errLvl
	}
	
	deleteAllLogs(putInRecycleBin=true, useWildCard=true)
	{ ; Deletes all log files (using 1 of 2 methods). 
	; Input variables
	; 	putInRecycleBin -> switches between FileDelete and FileRecycle
	; 	useWildCard -> If false, will walk through log files until a numeric break is hit.  If true, will delete everything using a "baseName_*.exten" wildcard string.
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		errLvl := 0

		; delete the current file
		errLvl += this.deleteLog(aCurrentFileName_FullPath, putInRecycleBin)

		; delete all the old files
		errLvl += this.deleteAllOldLogs(putInRecycleBin, useWildCard)
		return errLvl
	}

	deleteAllOldLogs(putInRecycleBin=true, useWildCard=true)
	{ ; Deletes all OLD log files (using 1 of 2 methods). Will not delete the current log file.
	; Input variables
	; 	putInRecycleBin -> switches between FileDelete and FileRecycle
	; 	useWildCard -> If false, will walk through log files until a numeric break is hit.  If true, will delete everything using a "baseName_*.exten" wildcard string.
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
		aCurrentFileName_FullPath := this.currentFileName_FullPath
		errLvl := 0

		; delete all the old files
		SplitPath, aCurrentFileName_FullPath, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
		if(useWildCard)
		{ ; the wildcard technique, which just deletes everything
			candidateFilename := OutDir . "\" . OutNameNoExt . "_" . "*" . "." OutExtension
			errLvl += this.deleteLog(candidateFilename, putInRecycleBin)
		}
		else
		{ ; the original walking technique which stops once you hit a discontinuity (which allows you to hide old files at high numbers)
			Loop
			{
				candidateFilename := OutDir . "\" . OutNameNoExt . "_" . A_Index . "." OutExtension
				doesExist := FileExist(candidateFilename)
				if (!doesExist)
					break
				errLvl += this.deleteLog(candidateFilename, putInRecycleBin)
			}
		}
		return errLvl
	}

	deleteLog(fileToDelete, putInRecycleBin=true)
	{ ; deletes/recycles a log (any file really)
	; Output variables
	; 	integer - 0 if everything went well, 1+ for each time something went wrong (a sum of ErrorLevel and the various File actions)
		errLvl := false
		
		doesExist := FileExist(fileToDelete)
		if (doesExist)
		{
			if(putInRecycleBin)
			{
				FileRecycle, %fileToDelete%
			}
			else
			{
				FileDelete, %fileToDelete%
			}
			errLvl := ErrorLevel
		}
		return errLvl
	}

	; addLogObject(entryObject="", addNewLine=true, usePreEntryString=true, usePostEntryString=true) 
	; { ; converts an Object to a String and then calls addLogEntry()
		; ; from nnnik
		; ; see https://autohotkey.com/boards/viewtopic.php?p=224955#p224955
		
		; ; !!!!!!!!!!! NOTICE !!!!!!!!!!!
		; ; until I put in a default method to do the conversion this.objectToString() this isn't going to work
		
		
		; return this.addLogEntry(this.objectToString(entryObject), addNewLine, usePreEntryString, usePostEntryString)
	; }

	; setObjectToString(objectToStringFunction)
	; { ;replaces the current function that's responsible for turning an object into a string with this one
		; ; So, if you have a toString() function in your object you're doing log entries of, you can either use:
		; ; 1. addLogEntry(obj.toString()) or 
		; ; 2. set setObjectToString(obj.toString()) and then call addLogObject(obj), which if you had multiple instantiations would be easier.
	
		; ; from nnnik
		; ; see https://autohotkey.com/boards/viewtopic.php?p=224955#p224955
		; This.objectToString := objectToStringFunction
		; return
	; }
	
	;EndRegion


	; Methods (helpers)
	; ---------------------------------------------
	;BeginRegion
	
	appendFile(text, filename, encoding, skipIfEmpty=true)
	{ ; a wrapper to FileAppend or its File Object equivalent (so much faster)
		errLvl := false
		USE_FILE_OBJECT := true

		if this.isLogClassTurnedOff ; should this method/class be turned off?
			return false ; if so, exit out before doing anything.

		if( (text == "") && (skipIfEmpty) )
		{
			; do not write if equal to ""
		}
		else
		{
			if(USE_FILE_OBJECT)
			{
				file := FileOpen(filename, "a" , encoding)
				if(file) ; no error occurred
				{
					bytesWritten := file.Write(text)
					file.Close()
					file := "" ; free up the object
					
					; error check: see if anything was written.  Tested that (text != ""), which would write no bytes.
					if( (bytesWritten == 0) && (text != "") )
					{
						errLvl := true
					}
				}
				else
				{
					errLvl := true
				}
			}
			else
			{
				FileAppend, %text%, %filename%, %encoding%
				errLvl := ErrorLevel
			}
		}
		return errLvl
	}
	
	createDividerString(char="-", length=70, includeNewLine=true, useAsProperty=false)
	{ ; create a string that repeats X times (e.g., "---------------" and can be used as a line or divider)
		outStr := ""
		Loop, %length%
		{
			outStr .= char
		}
		if(includeNewLine)
		{
			outStr .= "`n"
		}

		if(useAsProperty)
		{
			this.dividerString := outStr
		}
		return outStr
	}
	
	byteToMB(bytes, decimalPlaces=2)
	{ ; It converts bytes to megabytes (you expected something else?).  And limits things to X digits after the decimal place.
		kiloBytes := bytes / 1024.0
		megaBytes := kiloBytes / 1024.0
		megaBytes := round(megaBytes, decimalPlaces)

		return megaBytes
	}
	
	isFilenameFullyPathed(filename)
	{ ; determine if a filename has a relative path or starts at the root (i.e., fully pathed)
		SplitPath, filename, OutFileName, OutDir, OutExtension, OutNameNoExt, OutDrive
		if(OutDrive != "")
			return true
		else
			return false
	}
	
	transformStringVars(encodedString)
	{ ; converts a variable (normal %var%, or made up for this class, $printStack) to whatever its value is (when this function is run)
	; The String properties all accept variables which may be expanded when written.
	; * $time or %A_Now% expands to the time the log entry is written (as formatted according to this.logsTimeFormat).
	; * $printStack expands to a list of the function calls which caused the log entry to be written.
	; * $printCalling expands to only the function which caused the log entry to be written. (A shorter version of $printStack)
	; * %Var% expands to whatever value variable Var is, but Var must be a global/built-in variable.
	; ------------------------
	; Input variables
	; 	encodedString -> A string, possibly with an encoded %var%/$var in it
	; Non-Input variables (that are relied upon in an important enough way that I should tell you about it).
	; 	this.logsTimeFormat -> The format that $time or %A_Now% will be expanded to (see AHK help page on FormatTime).  The default is "yyyy-MM-dd hh:mm:ss tt"
	; Output variables
	; 	plaintextString -> A string with any %var%/$var replaced by those variables values.

		aLogsTimeFormat := this.logsTimeFormat
		plaintextString := ""
		
		if(encodedString != "")
		{
			; Replace $time var
			if ( (InStr(encodedString, "$time")) OR (InStr(encodedString, "`%A_Now`%")) )
			{ ; $time is really a hold over from the (circa 2009) Log.ahk class.  %A_Now% is preferred.
				; Format the current time
				FormatTime, formattedTime, %A_Now%, %aLogsTimeFormat%
				encodedString := RegExReplace(encodedString, "\$time", formattedTime)
				encodedString := RegExReplace(encodedString, "\%A_Now\%", formattedTime)
			}

			; Expand %var%
			if (RegExMatch(encodedString,"%\w+%"))
			{ 
				; variables need to be ** global ** to be DeRef'ed
				Transform, encodedString, DeRef, %encodedString% ; convert any variables to actual values
			}

			; Replace $printStack var
			if (InStr(encodedString, "$printStack"))
			{ 
				depth := this.printStackMaxDepth
				skip := 2 ; skip transformStringVars() and printStack()
				encodedString := StrReplace(encodedString, "$printStack", this.printStack(depth, skip))
			}

			; Replace $printCalling var
			if (InStr(encodedString, "$printCalling"))
			{ 
				encodedString := StrReplace(encodedString, "$printCalling", this.printCalling())
			}

			; ; Replace $function() var
			; if ( (InStr(encodedString, "$function(")) OR (InStr(encodedString, "`%A_Now`%")) )
			{ ; uncommented to allow code folding (based on brackets)
				; I haven't figured out a way to expand a function call yet.  
				; I played with variadic functions, and pulling the function name & parameters then using x := %func%(params*), but I never could get it to work.
				; Plus if you had a string param with commas (,) in it (i.e., commas that should be ignored), the RegEx to properly grab the params would be tough.  I could default back to CSV processing techniques.
				; from my test.ahk file, as of the Nth attempt when I gave up and just made $printStack its own thing.
					; I got it to work with 1 param
						; filename := "D:\Handbrake\MKV"
						; testStr := "This is $function(isFilenameFullyPathed(" . filename . ")).  See what it did."
						; FoundPos := RegExMatch(testStr,"\$function\((.+)\((.+)\)\)", v)
						; tmpVar := %v1%(v2)
						; newStr := RegExReplace(testStr,"\$function\(.+\)", tmpVar)
						; msgbox, %newStr%
					; I never got it to work with 2+ params
						; filename := "D:\Handbrake\MKV"
						; cmd = MinMax
						; testStr := "This is $function(WinGet(cmd, filename)).  See what it did."
						; FoundPos := RegExMatch(testStr,"\$function\((.+)\((.+)\)\)", v)
						; numbArg := isFunc(v1) - 1
						; vArray := StrSplit(v2,",")
						; aLen := vArray.length()
						; Loop, aLen
						; {
							; arVal := vArray[A_Index]
							; bArray.Insert(%arVal%)
						; }
						; tmpVar := %v1%(bArray*)
						; tmpVar2 := WinGet(cmd, filename)
						; newStr := RegExReplace(testStr,"\$function\(.+\)", tmpVar)
						; tmpStr  =	
								; ( LTrim
									; TestStr: %testStr%
									; FoundPos: %FoundPos%
									; numbArg: %numbArg%
									; vArrayL: %aLen%
									; v1: %v1%
									; v2: %v2%
									; tmpVar: %tmpVar%
									; tmpVar2: %tmpVar2%
									; newStr: %newStr%
								; )
						; msgbox, %tmpStr%
			}

			; The (circa 2009) Log.ahk class used $title, but since he didn't write any examples of using $title I don't understand how it was to be used, and I don't have a property that supports it.  I have Header/FooterString to do what I think you might have used $title for, but don't really know.
			; ; Replace $title var
			; if (InStr(string, "$title")) {
				; ; Get saved title
				; if (title := Log_getTitle(LogObject))
					; string := RegExReplace(string, "\$title", title)
			; }
			
			; copy the now-expanded string to the output string
			plaintextString := encodedString
		}
		return plaintextString
	}

	printStack(maxNumbCalls=2000, numbCallsToSkipOver=1 )
	{ ; outputs a String detailing the parentage of functions that called this function
		; slightly modified from nnnik's excellent work at ..
		; https://autohotkey.com/boards/viewtopic.php?f=74&t=48740

		; if numbCallsToSkipOver=1, skip this printStack() since the work is done in generateStack()
		str := "Stack Print, most recent function calls first:`n"
		str .= "`t" . "Skipping over " . numbCallsToSkipOver . " calls and a maximum shown call depth of " . maxNumbCalls . ".`n"

		maxNumbCalls := maxNumbCalls + numbCallsToSkipOver
		stack := this.generateStack()
		for each, entry in stack
		{
			if(A_Index <= numbCallsToSkipOver)
			{ ; do nothing
			}
			else if(A_Index <= maxNumbCalls) 
			{ ; the default is 2000 which is likely every call (i.e. the for-loop will end first)
				str .= "Line: " . entry.line . "`tCalled: " . entry.what "`tFile: " entry.file "`n"
			}
			else
			{
				break
			}
		}
		return str
	}

	printCalling(maxNumbCalls=1, numbCallsToSkipOver=3)
	{ ; outputs a String detailing the parentage of functions that called this function
		; slightly modified from nnnik's excellent work at ..
		; https://autohotkey.com/boards/viewtopic.php?f=74&t=48740

		maxNumbCalls := maxNumbCalls + numbCallsToSkipOver
		stack := this.generateStack()
		for each, entry in stack
		{
			if(A_Index <= numbCallsToSkipOver)
			{ ; do nothing
			}
			else if(A_Index <= maxNumbCalls) 
			{ ; the default is 2000 which is likely every call (i.e. the for-loop will end first)
				str := entry.what . "(ln: " . entry.line . ")"
			}
			else
			{
				break
			}
		}
		return str
	}

	generateStack( offset := -1 )
	{ ;returns the call stack as an Array of exception objects - the first array is the function most recently called.
		; from nnnik's excellent work at ..
		; https://autohotkey.com/boards/viewtopic.php?f=74&t=48740
		if ( A_IsCompiled )
			Throw exception( "Cannot access stack with the exception function, as the script is compiled." )
		stack := []
		While ( exData := exception("", -(A_Index-offset)) ).what != -(A_Index-offset)
			stack.push( exData )
		return stack
	}
	
	createScriptEnvReport(useAsProperty=false)
	{ ; creates a large string that details the environment the script is running in
		lesserTab := ""
		Loop, 4
		{
			lesserTab .= A_Space
		}
		newlineChar := "`n"
		lineStr := this.dividerString
		if( (!InStr(lineStr, newlineChar)) && (StrLen(lineStr) != InStr(lineStr, newlineChar)) )
		{ ; add a newline to the divderString if there isn't one
			lineStr .= "`n"
		}
		
		; FileGetTime, OutputVar [, Filename, WhichTime]
		FileGetTime, ModTime, %A_ScriptFullPath%, M
		FormatTime, ModTime, ModTime, ddd MMM d, yyyy hh:mm:ss tt
		
		currentFileName_FullPath := this.currentFileName_FullPath ; the below doesn't like the this.var notation
		maxNumbOldLogs     := this.maxNumbOldLogs ; the below doesn't like the this.var notation
		maxSizeMBLogFile   := this.maxSizeMBLogFile ; the below doesn't like the this.var notation
		isAutoWriteEntries := this.boolean2String(this.isAutoWriteEntries, "Yes", "No")
		
		is64bitOS  := this.boolean2String(A_Is64bitOS, "Yes", "No")
		isCompiled := this.boolean2String(A_IsCompiled, "Yes", "No")
		isUnicode  := this.boolean2String(A_IsUnicode, "Yes", "No")
		isAdmin    := this.boolean2String(A_IsAdmin, "Yes", "No")
		
		url=https://api.ipify.org ; CHANGE ME, if this website stops working
		externalIPAddr := this.URLDownloadToVar(url)
		aLogsTimeFormat := this.logsTimeFormat
		FormatTime, formattedTime, %A_Now%, %aLogsTimeFormat%

		infoStr  =	
			( LTrim
			Script:    %A_ScriptName%
			Computer:  %A_ComputerName%
			User:      %A_UserName%
			Script Last Modified:  
			%lesserTab%%ModTime%
			
			Script Dir: 
			%lesserTab%%A_ScriptDir%
			Working Dir: 
			%lesserTab%%A_WorkingDir%
			
			Log Info: 
			%lesserTab%%currentFileName_FullPath%
			Maximum number of old logs:    %maxNumbOldLogs%
			Maximum size (MB) of logs:     %maxSizeMBLogFile%
			Write log entries immediately: %isAutoWriteEntries%

			AHK version:      %A_AhkVersion%
			OS version:       %A_OSVersion%
			is OS 64-bit:     %is64bitOS%
			is AHK Unicode:   %isUnicode%
			script Compiled:  %isCompiled%
			running as Admin: %isAdmin%

			External IP:    %externalIPAddr%%lesserTab%(at %formattedTime%)
			IP Address #1:  %A_IPAddress1%
			IP Address #2:  %A_IPAddress2%
			IP Address #3:  %A_IPAddress3%
			IP Address #4:  %A_IPAddress4%
			)

			scriptInfoString := lineStr . lineStr
			scriptInfoString .= infoStr . "`n"
			scriptInfoString .= lineStr . lineStr

		if(useAsProperty)
		{
			this.scriptEnvReport := scriptInfoString
		}
		return scriptInfoString
	}
	
	boolean2String(bool, trueVal="true", falseVal="false")
	{ ; just re-formats a boolean to a given string pair
		if(bool)
		{
			return trueVal
		}
		else
		{
			return falseVal
		}
	}

	URLDownloadToVar(url)
	{ ; downloads a URL to the return variable.  Returns "ERROR" if an error occurred.
		; url -> a fully formed URL (e.g., https://https://api.ipify.org)
	
		; see documentation for URLDownloadToFile()
		; https://autohotkey.com/docs/commands/URLDownloadToFile.htm
		; https://docs.microsoft.com/en-us/windows/desktop/winhttp/iwinhttprequest-interface
		
		retVal := "ERROR"
		try {
			hObject := ComObjCreate("WinHttp.WinHttpRequest.5.1")
			hObject.Open("GET",url, true)
			hObject.Send()
			hObject.WaitForResponse()
			retVal := hObject.ResponseText
		}
		catch {
			; retVal := hObject.status
			retVal := "ERROR"
		}
		if (retVal == "")
		{
			retVal := "ERROR"
		}
		return retVal
	}
	
	objectToString(object) 
	{ ;turns an object into a string
		; from nnnik
		; see https://autohotkey.com/boards/viewtopic.php?p=224955#p224955

		; Disp(byref Obj, ShowBase=0, circularPrevent := "", indent := 0 )
		; {
			; try {
				; if !circularPrevent
					; circularPrevent := []
				; OutStr:=""
				; If !IsObject(Obj)
				; {
					; If ((Obj+0)=="")
					; {
						; If (IsByRef(Obj) && (StrPut(Obj)<<A_IsUnicode)<VarSetCapacity(Obj))
						; {
							; Loop % VarSetCapacity(Obj)
								; outstr.=Format("{1:02X} ",NumGet(Obj,A_Index-1,"UChar"))
							; msgbox test
							; return """" . outstr . """"
						; }
						; Else If (Strlen(obj)!="")
							; return """" . obj . """"
					; }
					; return Obj
				; }
				; If IsFunc(Obj)
					; return "func( """ . Obj.name . """ )"
				; if circularPrevent.hasKey( &Obj )
					; return  Format("Object(&{:#x})", &Obj) 
				; circularPrevent[&Obj] := 1
				; StartNum := 0
				; For Each in Obj {
					; If !(StartNum+1 = Each) {
						; NonNumeric:=1
						; break
					; }
					; StartNum := Each
				; }
				; If (NonNumeric) {
					; identStr := ""
					; indent++
					; Loop % indent
						; indentStr .= "`t"
					; For Each,Key in Obj
						; OutStr .= indentStr . Disp( noByRef( each ), ShowBase, circularPrevent, indent ) . ": " . Disp( noByRef(Key),ShowBase, circularPrevent, indent ) . ", `n"
					
					; StringTrimRight,outstr,outstr,3
					; If ( IsObject( ObjGetBase( Obj ) ) && ShowBase )
						; OutStr.= ( outStr ? ", `n" . indentStr : "" ) "base:" . Disp( obj.Base,ShowBase, circularPrevent, indent )
					; outstr:="`n" . subStr( indentStr, 1, -1 ) . Format("{`t = Object(&{:#x})`n", &Obj) . OutStr . "`n" . subStr( indentStr, 1, -1 ) . "}"
				; }
				; Else {
					; For Each,Key in Obj
						; OutStr .= Disp( noByRef(Key),ShowBase, circularPrevent, indent ) . ", "
					; StringTrimRight,outstr,outstr,2
					; outstr:="[" . OutStr . "]"
				; }
				; return outstr
			; }
			; catch e
				; return "Error(" e.what ", " e.message ")"
		; }
		; noByRef( var ) {
			; return var
		; }
		
		return "ERROR"
	}


	;EndRegion
	
	; Revision History
	; ---------------------------------------------
	;BeginRegion
	; 2018-09-07
	; * Support for writing to the same log file over multiple runs of the script (i.e., appending to an existing log file)
	;   * initalizeNewLogFile(overwriteExistingFile=0) overwriteExistingFile is now -1, 0 or 1 where -1 means "append to an existing file"
	;   * finalizeLog(willTidyLogs=true) if willTidyLogs=false it will leave the log there for the next time 
	;   * appendFile() will no longer skip writing if the string to write is "".  Allows the Unix `touch` command to occur via initalizeNewLogFile() if HeaderString is blank and no file exists.
	; * Created/Changed more helper methods
	;   * LogClass.dividerString & createDividerString(useAsProperty=false) are the results of createDivierString() saved to a property.
	;   * createDividerString(includeNewLine=true) now adds a trailing NewLine character by default.
	;   * LogClass.scriptEnvReport & createScriptEnvReport() creates a string with a bunch of data about the environment the script is running in.  See the LogExamples.ahk script for a, well, example.
	; * created $printCalling expanding variable.  It is a shortened version of $printStack in that it only prints the calling function's name
	;   * changed transformStringVars() to support $printCalling
	;   * added printCalling() to create the text that replaces $printCalling
	; * NOT FINALIZED
	; * NOT YET Support for converting a random Object to String and then writing it to the log
	;   * added addLogObject() to allow you to add a log entry from an Object vs. addLogEntry() which is for Strings
	;   * added setObjectToString() to allow you to define which method addLogObject() will use to convert an object to a string
	;   * NEED TO put in a default method for setObjectToString() and test it.
	
	
	; 2018-06-19
	; * Made all filename properties read-only (set via New())
	; * changed printStack().maxNumbCalls to 2000 which is pretty much ALL calls
	;
	; 2018-06-15
	; * Initial fully commented draft of class
	; * TODO: more error checking on the filename properties
	;
	; 2018-06-08
	; * This folly begins
	;EndRegion

}


Several examples of how to use class are as follows:

Code: Select all

#NoEnv  ; Recommended for performance and compatibility with future AutoHotkey releases.
#SingleInstance force
SendMode Input  ; Recommended for new scripts due to its superior speed and reliability.
SetWorkingDir %A_ScriptDir%  ; Ensures a consistent starting directory.

/*
;==============================================================================
;========== What This Script Does  ============================================
;==============================================================================
; A script to show how to use the LogClass.ahk class
; Made for the 2018-09-07 version of LogClass.ahk (but should be roughly compatible with other versions)
;==============================================================================
*/

#Include LogClass.ahk

; ------------------------
; Example 1
; ------------------------
; Let us start with a simple example of using the class

	; create a LogClass object for each series of log files you're going to use
	; We'll name it "LogClass_Example01.log" (notice the lack of an extension in the New() call)
	; 	You may want to look at the LogClass source as we do this
	; We'll also allow 0 old log files (i.e., only the current log file)
	; I suggest you make it global, so you can easily reference it in various functions and classes
global log_ex01 := new LogClass("LogClass_Example01", 0)
	; We'll initialize the log file.  You don't have to do this, but it is preferred as it does some cleaning functions and sets a bunch of variables.
log_ex01.initalizeNewLogFile(true)
	; Write things to the log file.  In this case we'll create a series of simple 1 line entries.
	; They are written immediately to the log file.
log_ex01.addLogEntry("This is your 1st log entry")
log_ex01.addLogEntry("This is your 2nd log entry")
	; You can stop writing a log file and clean up the log directory when you're done.
	; For a log this simple, I wouldn't bother, but it is good practice.
	; Don't worry the file handle was closed (File.Close()) as part of addLogEntry()).  The log file handle isn't kept open.  Finalize just cleans things up.
log_ex01.finalizeLog()
log_ex01 := {} ; free the object (which is really about me not accidentally using it again in this example file)


; ------------------------
; Example 2
; ------------------------
; Let us explore using the String properties and their ability to expand into useful values

; Before we start, since the question has come up, I'll talk about the philosophical mindset behind what is getting written to the log file.
; There are 3 types of “things” that get written to the log: Headers, Footers, and Entries.  
; They are all text (you can convert an Object to text, but ultimately the log is meant to be text not binary).
; 
; The concept is to think of a Microsoft Word file.  You have your Header at the top of the document (or every page of it).  Your Footer at the bottom.  And in the middle, the main body of the document, are your Entries.
; The idea is that the Header gets written at the top of every log file.  The Footer gets written at the bottom of every log file.  And in between, you write your Entries.  
;
; If you don’t want a Header or Footer, then leave them blank (“”).  
; If you don’t want to use the built-in initalizeNewLogFile()/finalizeLog() methods to write your Header/Footer, you can fake a Header/Footer with calls to addLogEntry().  See Example 7a.
; It’ll cause some minor problems with log rotation (see Examples 4 & 5) but if you don’t care, faking it works great.  In fact, the Header/Footer are just special types of Entries that the Class remembers for log rotation purposes.
; And, pre/postEntryStrings and expanding variables (e.g., %A_Now%) are just there to make less work for you out of writing Entries. 


	; create a LogClass object
global log_ex02 := new LogClass("LogClass_Example02")
	; You can set these properties via initalizeNewLogFile() but we'll set/talk about them individually
	; You can set 4 string properties that the LogClass will add to your log file (on initialize or finalize) or as part of every log entry.
	; If you're going to use the same beginning for each entry (e.g., a timestamp), this can make your code cleaner.
	; The headerString is written at the top of a log file when it is initialized.
	; In this case, let us get some information about what environment the script is running in (see the Functions section below)
	; Remember to include `n when you want a new line or else your entries could end up running together.
		; Note: createScriptEnvReport() got added in ver. 2018-09-7 before that the LogClass_Example.ahk used a non-class function 
		;       We could have also used the built-in property LogClass.scriptEnvReport which New() creates
		;       Since ver. 2018-09-07 createScriptEnvReport() has a trailing NewLine character.  So, you don't have to manually add one.
log_ex02.headerString := log_ex02.createScriptEnvReport()
	; The pre/postEntryStrings are added before/after every entry
log_ex02.preEntryString := "%A_Now% -- "
log_ex02.postEntryString := "" ; We'll ignore this, but it works like preEntryString
	; Now here is the thing, LogClass will expand variables embedded in the strings.  So, you can prepend every entry with a timestamp.
	; And it isn't just %A_Now%, you can use any global or built-in variable.
	; It HAS to be a global/built-in variable.  If the function deep in the LogClass that expands things can't see the variable it will expand to "".
	; Also, there is a built-in helper function to make a divider line for you
		; Note: After ver. 2018-09-7, we could have also used the built-in property LogClass.dividerString which New() creates
		; Nte: After ver. 2018-09-07 createDividerString()/dividerString add a trailing newLine for you by default.
lineStr := log_ex02.createDividerString("-", 70) ; makes a string "-----------" for 70 char
log_ex02.footerString :=  "`n" . lineStr . "Log file made by %A_UserName% on %A_ComputerName%"
	; Now we'll write some stuff to the log
log_ex02.initalizeNewLogFile(true) ; writes the headerString at the top
log_ex02.addLogEntry("This is your 1st log entry") ; includes pre-/postEntryString
Sleep, 2000 ; wait 2 sec for A_Now to change.
log_ex02.addLogEntry("This is your 2nd log entry") ; includes pre-/postEntryString
log_ex02.finalizeLog() ; writes the footerString at the bottom
log_ex02 := {} ; free the object
	

; ------------------------
; Example 3a
; ------------------------
; Let us talk about $printStack/LogClass.printStack()

	; create a LogClass object
global log_ex03 := new LogClass("LogClass_Example03a")
lineStr := log_ex03.createDividerString() ; makes a string "-----------" for 70 char
	; We'll ignore header/footer, you already know how those work.
log_ex03.preEntryString := "%A_Now% -- "
log_ex03.postEntryString := lineStr . "$printStack" . "`n" . lineStr
	; While %A_ThisFunc% shows you the current function, you really can't use that in pre/postEntryString
	; By the time they expand, A_ThisFunc is equal to LogClass.transformStringVars(), which is not useful.
	; nnnik figured out how to get the entire call stack, and that is very useful.
	; Since printStack() is a function not a variable, it won't expand like a %var%.
	; So, I created its own keyword $printStack.  When LogClass sees $printStack it runs printStack() and places that output in your entry.
	; A few things:
	;	1. printStack skips over 2 calls -- LogClass.transformStringVars() and LogClass.printStack() -- as they are uninteresting.
	;	2. $printStack by default only shows 5 calls.  You can change this via LogClass.printStackMaxDepth
log_ex03.initalizeNewLogFile() ; no header in this example
log_ex03.addLogEntry("This is your 1st log entry") ; includes pre-/postEntryString
	; This 1st entry isn't very exciting
	; Now we'll call a series of functions that call other functions and eventually call addLogEntry().
	; See how that $printStack value differs
Sleep, 2000 ; wait 2 sec for A_Now to change.
funcA() ; see the functions in the Function section below.
Sleep, 2000 ; wait 2 sec for A_Now to change.
log_ex03.addLogEntry("This is your 3rd log entry") ; includes pre-/postEntryString
log_ex03.finalizeLog() ; no footer in this example
log_ex03 := {} ; free the object


; ------------------------
; Example 3b
; ------------------------
; Let us talk about $printCalling/LogClass.printCalling()
; This was added in ver. 2018-09-7 as a shortened version of $printStack

	; create a LogClass object
global log_ex03 := new LogClass("LogClass_Example03b")
lineStr := log_ex03.dividerString ; uses a string "-----------" for 70 char that New() created
	; We'll ignore header/footer, you already know how those work.
log_ex03.preEntryString := "%A_Now% -- "
log_ex03.postEntryString := lineStr . "$printCalling" . "`n" . lineStr
	; $printStack was a bit verbose for everyday use.
	; So, I created its own keyword $printCalling, which just gives you the calling funciton and line number
	; As above:
	;	1. $printCalling skips over uninteresting function calls
log_ex03.initalizeNewLogFile() ; no header in this example
log_ex03.addLogEntry("This is your 1st log entry") ; includes pre-/postEntryString
	; This 1st entry isn't very exciting
	; Now we'll call a series of functions that call other functions and eventually call addLogEntry().
	; See how that $printStack value differs
Sleep, 2000 ; wait 2 sec for A_Now to change.
funcA() ; see the functions in the Function section below.
Sleep, 2000 ; wait 2 sec for A_Now to change.
log_ex03.addLogEntry("This is your 3rd log entry") ; includes pre-/postEntryString
log_ex03.finalizeLog() ; no footer in this example
log_ex03 := {} ; free the object


; ------------------------
; Example 4
; ------------------------
; Let us talk about log file rotation and cleaning

	; Before we start this example, we need a bunch of existing log files
	; These files represent a series of current and old files that were created during previous runs of whatever script you used.
FileAppend, , LogClass_Example04.log, UTF-8 ; This would be the current log file, where addLogEntry() writes
FileAppend, , LogClass_Example04_1.log, UTF-8 ; This is the oldest log file
FileAppend, , LogClass_Example04_2.log, UTF-8
FileAppend, , LogClass_Example04_3.log, UTF-8
FileAppend, , LogClass_Example04_4.log, UTF-8
FileAppend, , LogClass_Example04_5.log, UTF-8
FileAppend, , LogClass_Example04_6.log, UTF-8
FileAppend, , LogClass_Example04_7.log, UTF-8
FileAppend, , LogClass_Example04_8.log, UTF-8
FileAppend, , LogClass_Example04_9.log, UTF-8 ; as the highest number, this is the newest logfile
MsgBox, Example 04, Before start:`nPlease look in your script directory for the Example_04 log files.
	; create a LogClass object
global log_ex04 := new LogClass("LogClass_Example04") ; notice the base name needs to be the same as the existing logfiles created above.
	; We're not writing any entries this example.  That is the next example.
	; The property maxNumbOldLogs (default value of 5) dictates how many log files should exist at any one time.  Note, the current log file doesn't count, as it isn't "old".
	; So, LogClass_Example04_6.log - LogClass_Example04_9.log should not exist.
	; A maxNumbOldLogs of 0 will just allow the current file (e.g., Examples 1-3).  A value of -1 is infinite log files.
log_ex04.maxNumbOldLogs := 7 ; just showing you how to set maxNumbOldLogs.  Nothing exciting.
	; Now we'll have the LogClass get rid of any excess files
	; Since we changed maxNumbOldLogs to 7, LogClass_Example04_8.log & LogClass_Example04_9.log should not exist.
	; tidyLogs() will delete the older logs (lower numbers) and move the newer (higher numbers) down, until only the right number are left.
	; BTW, based on LogClass.useRecycleBin it'll put them in the RecycleBin or delete them (the default is the RecycleBin).
log_ex04.tidyLogs()
MsgBox, Example 04, After Tidy:`nPlease look in your script directory for the Example_04 log files.
	; Let us say you want to stop making logs (e.g., you want to archive the set of log files), and you don't want the current file to be named without a *_Index.
	; You can used moveLog() to take the current log (LogClass_Example04.log) and move it to the file with the highest index (or index +1) (LogClass_Example04_8.log)
	; Note: You now have maxNumbOldLogs + 1 (or 8, in this example) old/indexed log files.  You'd have to run tidyLogs() again to get back to 7 log files
log_ex04.moveLog()
MsgBox, Example 04, After Move:`nPlease look in your script directory for the Example_04 log files.
	; Let us tidy the files down to a small number
log_ex04.tidyLogs(2) ; You can override (without setting the property) maxNumbOldLogs
MsgBox, Example 04, After 2nd Tidy:`nPlease look in your script directory for the Example_04 log files.
	; moveLog()/tidyLogs() is one of the main purposes of initalizeNewLogFile()/finalizeLog()
	; You really don't have to tidy things manually, but you can.
	; Just use initalizeNewLogFile()/finalizeLog() and in Example 5 addEntry() and it'll all happen automatically.
log_ex04 := {} ; free the object


; ------------------------
; Example 5
; ------------------------
; Let us talk about automatic log rotation and maximum file size

	; create a LogClass object
global log_ex05 := new LogClass("LogClass_Example05a")
lineStr := log_ex05.dividerString
	; Just showing that you can use initalizeNewLogFile() to set these strings
varH     := log_ex05.scriptEnvReport
varPreE  := "%A_Now% -- "
varPostE := lineStr . "$printStack" . "`n" . lineStr
varF     :=  "`n" . lineStr . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . lineStr
log_ex05.initalizeNewLogFile(true,varH, varPreE, varPostE, varF)
	; You can set a limit for how big your log files should get.
	; When they get too big, the current log file is finalized, tidied, and new log file is initialized.
	; The header/footerStrings are used for each log file.  The same way pre/PostEntry is used for each entry.
	; The default size limit is infinite (or, LogClass.maxSizeMBLogFile := -1 or 0) 
	; You can change that via New() or manually.
log_ex05.maxSizeMBLogFile := 0.04 ; change to ~40Kb (as it is in Mb)
	; Note: If this value is too close to or less than the size of your headerString, you'll fall into an infinite loop and not be able to write anything.
	; This trap is mostly avoided by LogClass doing a FileSize check with a granularity of 0.01Mb (i.e., rounding to 2 decimal places)
	; Also, this limit isn't that strict.
log_ex05.maxNumbOldLogs := 3 ; lower the old log file limit for this example
	; NOTE:  There is a minor cosmetic error here that I didn't feel like correcting.  See Example 7c for the fix.
log_ex05.printStackMaxDepth := 8 ; I also want a deeper printStack
	; Now we're going to write a lot of data to the log files.
	; We're going to write so much data, that the size limit is exceeded, and the logs auto-rotate
	; This will take a while.  We'll show a MsgBox when done.
global G_Index ; I need a global variable so addLogEntry() can expand it.
Loop, 100
{
	G_Index := A_Index ; copy the index to the global variable
	log_ex05.addLogEntry(A_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
	funcA(2) ; G_Index is used deep in here
}
log_ex05.finalizeLog()
MsgBox, Example 05, After Loop:`nPlease look in your script directory for the Example_05 log files.
	; When you look in your directory, you should see ...
	; LogClass_Example05.log -> containing (depending on your PC etc.) loops 87-100
	; LogClass_Example05_1.log -> containing loops 1-29
	; LogClass_Example05_2.log -> containing loops 29-57
	; LogClass_Example05_3.log -> containing loops 57-86
	; You'll also notice they all have the same header and footers
	; Each time addLogEntry() was called the file size was checked and the files rotated if needed.
log_ex05 := {} ; free the object



; ------------------------
; Example 6
; ------------------------
; Let us talk about NOT writing automatically to the log files but saving up the writes and writing them later
; This is useful if you have a log file on a networked or synchronized directory and you don't want to be changing it too often (e.g., only every 5 minutes).

	; this is going to look a lot like Example 5.  I'll just skip anything previously explained there.
global log_ex06 := new LogClass("LogClass_Example06")
varH     := log_ex06.scriptEnvReport
varPreE  := "%A_Now% -- "
varPostE := log_ex06.dividerString . "$printStack" . "`n" . log_ex06.dividerString
varF     :=  "`n" . log_ex06.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex06.dividerString
	; Normally LogClass.isAutoWriteEntries is set to true and as soon as you use addLogEntry() things are written to the log file.
	; If LogClass.isAutoWriteEntries := false then addLogEntry() starts stacking entries in an array.
	; The String %Var% are expanded before being pushed onto the pending entry stack.  So, %A_Now% is still when addLogEntry() was called.
	; Let us experiment with that.
log_ex06.isAutoWriteEntries := false ; save entries to an array not the file.
	; initalizeNewLogFile() and finalizeLog() ignore isAutoWriteEntries
log_ex06.initalizeNewLogFile(true,varH, varPreE, varPostE, varF) ; the header is still written out
MsgBox, Example 06, Init Log:`nPlease look the Example_06 log file, and see that Header was written.
	; Same settings as last example
log_ex06.maxSizeMBLogFile := 0.04 ; change to ~40Kb (as it is in Mb)
log_ex06.maxNumbOldLogs := 3 ; lower the old log file limit for this example
log_ex06.printStackMaxDepth := 8 ; I also want a deeper printStack
	; Now we're going to write a lot of data, but NOT to the log file, to the array
global G_Index ; I need a global variable so addLogEntry() can expand it.
Loop, 10
{
	G_Index := A_Index ; copy the index to the global variable
	log_ex06.addLogEntry(A_Index . ": In the loop")
	funcA(3) ; G_Index is used deep in here
}
	; while we "made" a lot of entries none of them gotten written to the file
MsgBox, Example 06,After 1st Loop:`nPlease look the Example_06 log file, and see that no entries were written.
	; To save them we have to do it manually
	; Again, if this results in a log file that is too big, new ones will be created.
	; The size check is made as each entry is about to be written.
log_ex06.savePendingEntriesToLog()
MsgBox, Example 06,After manual write:`nPlease look the Example_06 log file, and see that all entries were written.
	; The pendingEntries should now be empty
	; Let us write a lot more stuff to it and see it create a bunch of logs.
Loop, 150
{
	G_Index++ ; continue numbering from the last loop
	log_ex06.addLogEntry(G_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
	funcA(3) ; G_Index is used deep in here
}
	; while we "made" a lot of entries none of them gotten written to the file
MsgBox, Example 06,After 2nd Loop:`nPlease look the Example_06 log file, and see that no entries were written.
	; We could use savePendingEntriesToLog() here, but I want to show you another way
	; If you change isAutoWriteEntries, the pending entries won't be written until the next addLogEntry() 
log_ex06.addLogEntry("This entry will NOT be automatically written because isAutoWriteEntries=false")
log_ex06.isAutoWriteEntries := true ; save future entries to the file.
log_ex06.addLogEntry("This entry will  be automatically written, and all pending entries will be too.") ; and now the pending entries are written
MsgBox, Example 06,After automatic write:`nPlease look the Example_06 log files, and see that all entries were written.
	; A couple of caveats.
	; 1. LogClass.finalizeLog() will write out the footer regardless of isAutoWriteEntries.
	; 2. LogClass.finalizeLog(), will write out ALL PENDING ENTRIES regardless of isAutoWriteEntries.  
	; 3. LogClass.initalizeNewLogFile(), when called MANUALLY, will erase ALL PENDING ENTRIES.  When called automatically I do some behind the scenes magic to make sure that doesn't happen.


; ------------------------
; Example 7a 
; ------------------------
; Let us talk about writing to the same log over multiple runs/sessions of the script
; Originally the idea was you'd have 1 log file for every time you ran your script.
; Run the script a 2nd time, get a 2nd log file. And so on.
; But sometimes you just want one 1 log file no matter how often you run the script.  You just want the script to keep adding/appending to that same log file.
; There are 2 ways of doing this
; 1. Never run initalizeNewLogFile()/finalizeLog() or tidyLogs(), which initLogs() and finalizeLogs() call.
; 2. Tell initalizeNewLogFile()/finalizeLog() to not run tidyLogs(), and don't call tidyLogs yourself (obviously)
;
; In this version we'll go with Option #1 (don't use initalizeNewLogFile()/finalizeLog())
; And to simulate running the script twice, we'll run 2 LogClass objects to the same file

	; create a LogClass object
global log_ex07 := new LogClass("LogClass_Example07a")
	; these are the same variables we've created for the last few examples.
	; Note: they are just local variables not yet used to initialize the LogClass
varH     := log_ex07.scriptEnvReport
varPreE  := "%A_Now% -- "
varPostE := log_ex07.dividerString . "$printStack" . "`n" . log_ex07.dividerString
varF     :=  "`n" . log_ex07.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex07.dividerString
	; Previously we'd have use initalizeNewLogFile() to initialize the LogClass, tidy the log files, and create a new log file
	; but we're not doing that here
		; log_ex07.initalizeNewLogFile(true,varH, varPreE, varPostE, varF)
		; seriously you could use initalizeNewLogFile() if you wanted since it is the 1st run of the script, but you'd have to build in logic to use initalizeNewLogFile() for the 1st run and then do it manually for the 2+ runs of the script (e.g., if file exists)
	; Instead set the log properties manually.
log_ex07.headerString := varH
log_ex07.preEntryString := varPreE
log_ex07.postEntryString := varPostE
log_ex07.footerString :=  varF
	; Now you could have just ignored headerString & footerString but I'll show you how to use them without initalizeNewLogFile()/finalizeLog()
	; To add the header string, just add it like a normal loggEntry
log_ex07.addLogEntry(log_ex07.headerString)
Loop, 5
{
	log_ex07.addLogEntry(A_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
}
	; Again when you're done you can add a footer, but given you're appending you probably don't want to
log_ex07.addLogEntry(log_ex07.footerString)
	; The Log file closes after each write operation.  So, no need for a manual close
	; We'll just clean up the script and pretend the script has shutdown
log_ex07 := {} ; free the object
	; Go look at your log file now that the script has "ended"
MsgBox, Example 07a, After 1st Script run:`nPlease look the Example_07a log file, and see that all entries were written.`nAlso, if you ran LogClass_Examples.ahk multiple times, you'll have a very long log file.

	; Now we'll run the script a 2nd time and append to the same file
	; Use the same log filename, same everything
global log_ex07 := new LogClass("LogClass_Example07a")
varH     := log_ex07.scriptEnvReport
varPreE  := "%A_Now% -- "
varPostE := log_ex07.dividerString . "$printStack" . "`n" . log_ex07.dividerString
varF     :=  "`n" . log_ex07.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex07.dividerString
log_ex07.headerString := varH
log_ex07.preEntryString := varPreE
log_ex07.postEntryString := varPostE
log_ex07.footerString :=  varF
	; We'll skip adding the headerString, I assume you'll have some "if file exists" statement that does that for you
Loop, 5
{
	; I'll bump up the Index value for purposes of illustration.  Again, if you wanted things to change between running of your script you'd have to build in logic to change behaviour or carry over variables between script runs.
	E_Index := A_Index + 5
	log_ex07.addLogEntry(E_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
}
	; We'll skip adding the footerString
	; The Log file closes after each write operation.  So, no need for a manual close
	; We'll just clean up the script and pretend the script has shutdown
log_ex07 := {} ; free the object
	; Go look at your log file now that the script has "ended"
MsgBox, Example 07a, After 2nd Script run:`nPlease look the Example_07a log file, and see that new entries were appeneded to the existing log file.



; ------------------------
; Example 7b
; ------------------------
; Let us talk about writing to the same log over multiple runs/sessions of the script
; Originally the idea was you'd have 1 log file for every time you ran your script.
; Run the script a 2nd time, get a 2nd log file. And so on.
; But sometimes you just want one 1 log file no matter how often you run the script.  You just want the script to keep adding/appending to that same log file.
; There are 2 ways of doing this
; 1. Never run initalizeNewLogFile()/finalizeLog() or tidyLogs(), which initLogs() and finalizeLogs() call.
; 2. Tell initalizeNewLogFile()/finalizeLog() to not run tidyLogs(), and don't call tidyLogs yourself (obviously)
;
; In this version we'll go with Option #2 (manually turn off tidyLogs() in initalizeNewLogFile()/finalizeLog())
; And to simulate running the script twice, we'll run 2 LogClass objects to the same file

	; create a LogClass object
global log_ex07 := new LogClass("LogClass_Example07b")
	; these are the same variables we've created for the last few examples.
	; Note: they are just local variables not yet used to initialize the LogClass
varH     := "" ; NOTE: this is "" for this example
varPreE  := "%A_Now% -- "
varPostE := log_ex07.dividerString
varF     :=  "`n" . log_ex07.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex07.dividerString
	; We're going to use initalizeNewLogFile() but we have to turn off that pesky tidyLogs() it runs
	; to do that, we'll set initalizeNewLogFile(overwriteExistingFile) to -1 (previously, it could only either be true (1) or false (0))
log_ex07.initalizeNewLogFile(-1,varH, varPreE, varPostE, varF)
	; Note: Here is something else that changed in ver. 2018-09-07.
	; initalizeNewLogFile() is going to write headerString (a.k.a. varH) to the log file, but headerString=="".
	; Previously, the function that does the write would have recognized an empty string and just skipped over it.
	; Now, the function that does the write writes the empty string, creating a new file if needed (like the Unix `touch` command)
MsgBox, Example 07b, Logfile just created:`nPlease look the Example_07b log file, and see that a new but empty log file has been created (if you had deleted the log file from the last time you ran LogClass_Exmaples.ahk).
	; let us get back to writing log entries.
Loop, 5
{
	log_ex07.addLogEntry(A_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
}
	; When you're done with your script you might want to write a last log entry
	; You can use addLogEntry() as shown above in Example 7a, or
	; you can call finalizeLog() but turn off its call to tidyLogs()
	; You do this by setting finalizeLog(willTidyLogs) for false
log_ex07.finalizeLog(, false) ; note we're skipping over the aFooterString input variable
	; The Log file closes after each write operation.  So, no need for a manual close
	; We'll just clean up the script and pretend the script has shutdown
log_ex07 := {} ; free the object
	; Go look at your log file now that the script has "ended"
MsgBox, Example 07b, After 1st Script run:`nPlease look the Example_07b log file, and see that all entries were written.

	; Now we'll run the script a 2nd time and append to the same file
	; Use the same log filename, same everything
	; And I do mean the exact same everything.  No tweaking things for illustrative purposes
global log_ex07 := new LogClass("LogClass_Example07b")
varH     := "" ; NOTE: this is "" for this example
varPreE  := "%A_Now% -- "
varPostE := log_ex07.dividerString
varF     :=  "`n" . log_ex07.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex07.dividerString
log_ex07.initalizeNewLogFile(-1,varH, varPreE, varPostE, varF)
Loop, 5
{
	log_ex07.addLogEntry(A_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
}
log_ex07.finalizeLog(, false) ; note we're skipping over the aFooterString input variable
log_ex07 := {} ; free the object
MsgBox, Example 07b, After 2nd Script run:`nPlease look the Example_07b log file, and see that new entries were appeneded to the existing log file.


; ------------------------
; Example 7c
; ------------------------
; So, the ability to keep appending to a single log file must mean maxSizeMBLogFile and maxNumbOldLogs are worthless when appending?
; No, they still matter.
; 
; When is a new log file created?
; 1. When you explicitly call tidyLogs()
; 2. When you call initalizeNewLogFile()/finalizeLog() in non-append mode, which implicitly calls tidyLogs(). See Example 7b for calling in append mode.
; 3. When the log file's size exceeds maxSizeMBLogFile
;
; To avoid case #1, just don't to that ("that" being going out of your way to call tidyLogs()).  Do nothing.
; In Examples 7a and 7b we avoided case #2 from occurring.  
;    Example 7a avoided case #2 by not calling initalizeNewLogFile()/finalizeLog()
;    Example 7b avoided case #2 by calling initalizeNewLogFile()/finalizeLog() in append-mode
; But neither of those did anything with case #3.
; If your monolithic log file gets too big, LogClass will start creating and rotating log files.
; Fortunately, maxSizeMBLogFile's default value is -1 or an infinite size limit.  So, to avoid case #3 you just have to do nothing.
; But, if you do change maxSizeMBLogFile/maxNumbOldLogs, they'll start to work just like they did in Example 5.  And, they'll use headers & footers and all the rest


; Let's go
; We'll create a loop that "runs" your script X number of times
	; but first some clean-up
	log_ex07 := new LogClass("LogClass_Example07c")
	log_ex07.deleteAllLogs()
	log_ex07 := {} ; free the object
; now for the script
Loop, 2
{
		; create a LogClass object
	global log_ex07 := new LogClass("LogClass_Example07c")
		; set the size and number limits (See Example 5)
	log_ex07.maxSizeMBLogFile := 0.04 ; change to ~40Kb (as it is in Mb)
	log_ex07.maxNumbOldLogs := 3 ; I really don't need this for this example, but just to remind you it exists and will work like in Example 5.
	
	log_ex07.printStackMaxDepth := 8 ; I also want a deeper printStack

		; set the String properties
	varPreE  := "%A_Now% -- "
	varPostE := log_ex07.dividerString . "$printStack" . "`n" . log_ex07.dividerString
	varF     :=  "`n" . log_ex07.dividerString . "Log file made by %A_UserName% on %A_ComputerName%" . "`n" . log_ex07.dividerString
	varH     := log_ex07.createScriptEnvReport(true)
		; Here is something you'd likely miss (I did at first).
		; Because log_ex07.scriptEnvReport gets created when New() runs, if you change maxSizeMBLogFile, etc. outside of New() that wouldn't be displayed/reflected in log_ex07.scriptEnvReport, as the purpose is to be a stable, no further computation required value.
		; If you change maxSizeMBLogFile, etc. after running New(), you should refresh log_ex07.scriptEnvReport by calling createScriptEnvReport(true).
		; If you change maxSizeMBLogFile, etc. by making them inputs to New(), scriptEnvReport will be just fine and you don't need the extra bother.
		; For example, global log_ex07 := new LogClass("LogClass_Example07c", 3, 0.04)


		; We're going to use initalizeNewLogFile() but we have to turn off that pesky tidyLogs() it runs
		; to do that, we'll set initalizeNewLogFile(overwriteExistingFile) to -1 (previously, it could only either be true (1) or false (0))
	log_ex07.initalizeNewLogFile(-1,varH, varPreE, varPostE, varF)
		; When a new log file is created due to the file size, we want to note that differently than when we "start" writing to the log file due to the script running 
		; So, when the script starts, we'll write the scriptEnvReport to the Header (which initalizeNewLogFile() just did)
		; But if the script creates a new log file due to file size we'll use a 2nd headerString
	log_ex07.headerString := "`n`n`n`n" . log_ex07.dividerString . "Logs rotated on %A_Now%" . "`n" . log_ex07.dividerString
		; now if initalizeNewLogFile() gets called due to log rotation, this 2nd headerString will be used
		; we'll keep footerString the same

		; let us get back to writing log entries.
	global G_Index ; I need a global variable so addLogEntry() can expand it.
	Loop, 50
	{
		G_Index := A_Index ; copy the index to the global variable
		log_ex07.addLogEntry(A_Index . ": In the loop") ; you can enter variables normally, no need to have them be something that expands.
		funcA(4) ; G_Index is used deep in here
	}
		; When you're done with your script you might want to write a last log entry
		; You can call finalizeLog() but turn off its call to tidyLogs()
		; You do this by setting finalizeLog(willTidyLogs) for false
	log_ex07.finalizeLog(, false) ; note we're skipping over the aFooterString input variable
	log_ex07 := {} ; free the object
}
; Go look in the LogClass_Example07c*.log files
; You'll see LogClass_Example07c_1.log starts with the scriptEnvReport Header,
; but LogClass_Example07c_2.log (and LogClass_Example07c.log) start with the shorter 2nd header
; Also, half-way down LogClass_Example07c_2.log (about line 300 on mine), the script ends the 1st time and runs the 2nd time (the outer-loop repeats in our example)
; You'll notice the footerString is written, and then the scriptEnvReport Header is written as the the script  starts the 2nd time
; Also, the footerString is written (by the 2nd run of the script) at the end of LogClass_Example07c.log 


; ------------------------
; Example 8
; ------------------------
; Let us talk about turning off the log
; I don't have a good code example, for this (and I am tired)
; So, I'll just talk about it.

; You've written your code, and embedded instances of LogClass.addLogEntry() all over the place.  In functions, in classes, in subroutines.    
; Now you're done with coding, and you've used LogClass to debug things, but you don't want it in a production code.
; Or, you have a setting where the user can turn on/off logging.  

; What a pain to have to go into every place LogClass was called and either (a) rip it out, or (b) put in a conditional statement based on your setting.
; Well, I created LogClass.isLogClassTurnedOff to get around that (hopefully).

; If you set isLogClassTurnedOff := true, most of the LogClass functions stop working.
; Before they do anything, they check this property and, if set, they exit right out.  
; Nothing gets written.
; Nothing gets initialized/finalized
; Nothing gets tidied.
; The class is close to being turned off.  (You can still change properties.)


return








; =========================================
; ===== Functions
; =========================================
;BeginRegion

funcA(value=1)
{
	funcB(value)
	return
}

funcB(value)
{
	funcC(value)
	return
}

funcC(value)
{
	funcD(value)
	return
}

funcD(value)
{
	if(value = 1)
		funcE()
	else if(value = 2)
		funcF()
	else if(value = 3)
		funcG()
	else if(value = 4)
		funcH()
	return
}

funcE()
{
	log_ex03.addLogEntry("This is a deep call to FuncE")
	return
}

funcF()
{
	log_ex05.addLogEntry("%G_Index%: this is FuncF")
	return
}

funcG()
{
	log_ex06.addLogEntry("%G_Index%: this is FuncG")
	return
}

funcH()
{
	log_ex07.addLogEntry("%G_Index%: this is FuncH")
	return
}


;EndRegion


Updates:
2018-09-07
  • Support for writing to the same log file over multiple runs of the script (i.e., appending to an existing log file)
    * initalizeNewLogFile(overwriteExistingFile=0) overwriteExistingFile is now -1, 0 or 1 where -1 means "append to an existing file"
    * finalizeLog(willTidyLogs=true) if willTidyLogs=false it will leave the log there for the next time
    * appendFile() will no longer skip writing if the string to write is "". Allows the Unix `touch` command to occur via initalizeNewLogFile() if HeaderString is blank and no file exists.
  • Created/Changed more helper methods
    * LogClass.dividerString & createDividerString(useAsProperty=false) are the results of createDivierString() saved to a property.
    * createDividerString(includeNewLine=true) now adds a trailing NewLine character by default.
    * LogClass.scriptEnvReport & createScriptEnvReport() creates a string with a bunch of data about the environment the script is running in. See the LogExamples.ahk script for a, well, example.
  • created $printCalling expanding variable. It is a shortened version of $printStack in that it only prints the calling function's name
    * changed transformStringVars() to support $printCalling
    * added printCalling() to create the text that replaces $printCalling
  • NOT FINALIZED
  • NOT YET Support for converting a random Object to String and then writing it to the log
    * added addLogObject() to allow you to add a log entry from an Object vs. addLogEntry() which is for Strings
    * added setObjectToString() to allow you to define which method addLogObject() will use to convert an object to a string
    * NEED TO put in a default method for setObjectToString() and test it.
2018-06-19:
* Made all filename properties read-only (set via New())
* changed printStack().maxNumbCalls to 2000 which is pretty much ALL
Last edited by ahbi on 07 Oct 2018, 16:32, edited 3 times in total.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: [Class] LogClass - Logging and log file management class

16 Jun 2018, 01:38

This looks good. I'm probably going to use it. Glad you liked my discovery :)
Do you plan to host it on git?
Recommends AHK Studio
ahbi
Posts: 17
Joined: 15 Jun 2016, 17:11

Re: [Class] LogClass - Logging and log file management class

18 Jun 2018, 12:51

FYI:
I am thinking about pulling out the ability to set the log's filename through anything but New().

This would mean all the filename-related properties would be read-only (get(), but no set())

PS:
GitHub is an idea. I have never used it before.
User avatar
Chunjee
Posts: 1400
Joined: 18 Apr 2014, 19:05
Contact:

Re: [Class] LogClass - Logging and log file management class

19 Jun 2018, 14:40

Wow I started needing a logging class today and I see this is less than a week old. Perfect timing. Thanks my dude.

I noticed that both .initalizeNewLogFile() and .finalizeLog() ignore pre/postEntryString, which seems a little inconsistant to me but I don't have any strong feeling about it.


In the future I'd like to see this go more async as FileAppend has the potential to significantly slow down an application. Might have to figure that one out on my own.
ahbi
Posts: 17
Joined: 15 Jun 2016, 17:11

Re: [Class] LogClass - Logging and log file management class

19 Jun 2018, 23:04

Chunjee wrote:Wow I started needing a logging class today and I see this is less than a week old. Perfect timing. Thanks my dude.
Thanks

Please let me know if I need to correct any errors.

Chunjee wrote:I noticed that both .initalizeNewLogFile() and .finalizeLog() ignore pre/postEntryString, which seems a little inconsistant to me but I don't have any strong feeling about it.
That is because pre/post are for entries and init/final are for header/footers.

Which is to say, the init/final are for starting/stopping a log file and don't write "entries"

Whereas addLogEntry() is for the middle/body of the file and for writing an "entry"

Conceptually they are 2 different things. Header/Footer (init/final) are to be written once (or none) per file, and an entry (and per/post) are to be used over-and-over again, each time you want to write a note/message/entry/line to the log.

If it helps think of header/footer like the header/footer of a Word file that includes your letterhead, document title, page numbering (which doesn't work in the log context, but you know that in Word you put page numbers in the footer).
And an entry is the text in the main body of the Word document.
In the analogy, each log file (in the index chain MyLogFile_1.log) is a "page" in the Word document. As you go to a new "page" (or log file) a new copy of the header/footer gets written (well footer on the old "page" and header on the new). addLogEntry() writes in the body of each page.

Part of the idea is I expect preEntryString to (often) be a timestamp, and I don't want the header/footer muddied up with the timestamp at the front.

And I also admit init() is a slightly strange beast and it kind of acts like a 2nd half of the New() function by setting all those string properties.
Chunjee wrote: In the future I'd like to see this go more async as FileAppend has the potential to significantly slow down an application. Might have to figure that one out on my own.
It actually does use the File object.

I know it is easy to miss and it is in a wrapper function called appendFile() (vs. FileAppend, which is just the name flipped around; look down in the helper methods section).

I started off with FileAppend, but as you said it was too slow. You'll see a try/catch group in savePending() where I discovered that FileAppened couldn't keep up. So, I created the wrapper function appendFile() and started working on the File object (which is so much faster).
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: [Class] LogClass - Logging and log file management class

19 Jun 2018, 23:41

Is the file object completely async?

A thing that would be nice to have is something like logObject which could handle objects. And let's the user define a function for Object logging beforehand.
Generally dealing with multiple types would by nice.
Recommends AHK Studio
ahbi
Posts: 17
Joined: 15 Jun 2016, 17:11

Re: [Class] LogClass - Logging and log file management class

20 Jun 2018, 00:11

nnnik wrote:Is the file object completely async?

A thing that would be nice to have is something like logObject which could handle objects. And let's the user define a function for Object logging beforehand.
Generally dealing with multiple types would by nice.
Explain to me more how to do that?

Or point me to some example.

The problem is, as a hobbyist, I don't fully understand the request.
User avatar
nnnik
Posts: 4500
Joined: 30 Sep 2013, 01:01
Location: Germany

Re: [Class] LogClass - Logging and log file management class

20 Jun 2018, 00:28

Well for every data type that would be loggeable add a code in this pattern:

Code: Select all

class LogClass {
	
	addLogObject(entryObject="", addNewLine=true, usePreEntryString=true, usePostEntryString=true) {
		this.addLogEntry(this.objectToString(entryObject), addNewLine,usePreEntryString, usePostEntryString)
	}
	
	objectToString(object) { ;turns an object into a string
		;code to turn an object into a string
	}
	
	setObjectToString(objectToStringFunction) { ;replaces the currect function thats responsible for turning an object into a string with this one
		This.objectToString := objectToStringFunction
	}
	
}
There aren't many data types in AHK:
Number (turns into a string automatically)
Binary (might be neccessary to add a function for)
Object (I guess the most important case)

Edit:
This is my function for turning objects or rather anything into a string:

Code: Select all

Disp(byref Obj,ShowBase=0, circularPrevent := "", indent := 0 )
{
	try {
		if !circularPrevent
			circularPrevent := []
		OutStr:=""
		If !IsObject(Obj)
		{
			If ((Obj+0)=="")
			{
				If (IsByRef(Obj) && (StrPut(Obj)<<A_IsUnicode)<VarSetCapacity(Obj))
				{
					Loop % VarSetCapacity(Obj)
						outstr.=Format("{1:02X} ",NumGet(Obj,A_Index-1,"UChar"))
					msgbox test
					return """" . outstr . """"
				}
				Else If (Strlen(obj)!="")
					return """" . obj . """"
			}
			return Obj
		}
		If IsFunc(Obj)
			return "func( """ . Obj.name . """ )"
		if circularPrevent.hasKey( &Obj )
			return  Format("Object(&{:#x})", &Obj) 
		circularPrevent[&Obj] := 1
		StartNum := 0
		For Each in Obj {
			If !(StartNum+1 = Each) {
				NonNumeric:=1
				break
			}
			StartNum := Each
		}
		If (NonNumeric) {
			identStr := ""
			indent++
			Loop % indent
				indentStr .= "`t"
			For Each,Key in Obj
				OutStr .= indentStr . Disp( noByRef( each ), ShowBase, circularPrevent, indent ) . ": " . Disp( noByRef(Key),ShowBase, circularPrevent, indent ) . ", `n"
			
			StringTrimRight,outstr,outstr,3
			If ( IsObject( ObjGetBase( Obj ) ) && ShowBase )
				OutStr.= ( outStr ? ", `n" . indentStr : "" ) "base:" . Disp( obj.Base,ShowBase, circularPrevent, indent )
			outstr:="`n" . subStr( indentStr, 1, -1 ) . Format("{`t = Object(&{:#x})`n", &Obj) . OutStr . "`n" . subStr( indentStr, 1, -1 ) . "}"
		}
		Else {
			For Each,Key in Obj
				OutStr .= Disp( noByRef(Key),ShowBase, circularPrevent, indent ) . ", "
			StringTrimRight,outstr,outstr,2
			outstr:="[" . OutStr . "]"
		}
		return outstr
	}
	catch e
		return "Error(" e.what ", " e.message ")"
}
noByRef( var ) {
	return var
}
Recommends AHK Studio
User avatar
Chunjee
Posts: 1400
Joined: 18 Apr 2014, 19:05
Contact:

Re: [Class] LogClass - Logging and log file management class

10 Jul 2018, 09:59

My application usually just runs once a day, but occasionally more often. In those cases the original log file gets overwritten. Is there a built-in way to control this? I'd rather it just keep appending or rotate the logs and keep the original intact somehow.

EDIT: After looking at overwriteExistingFile, I'm thinking maxNumbOldLogs_Default needs to be set to -1 as the default 0 keeps only the current file.


Because I'm feeding logs up to Sumologic and want them a little more machine readable I wrote a custom method that just writes the entry as a stringified JSON object. I'm working to make it a little more useable by others and will post it eventually.
User avatar
Chunjee
Posts: 1400
Joined: 18 Apr 2014, 19:05
Contact:

Re: [Class] LogClass - Logging and log file management class

16 Jul 2018, 15:11

I tried changing isAutoWriteEntries_Default to false and it writes nothing beyond the initalize line. What gives?


I'm trying to figure it out. the library doesn't periodically call .savePendingEntriesToLog() by itself. I was expecting somekind of "Is the CPU idle enough to start writing?" timer.

.finalizeLog() should be calling it and my app does so, but some other error or setting is preventing it from hitting there. Still looking into it.


EDIT: Figured out my problem. I was calling .finalizeLog() in a scope with no access to the logging object.
Last edited by Chunjee on 17 Jul 2018, 09:50, edited 1 time in total.
User avatar
KuroiLight
Posts: 327
Joined: 12 Apr 2015, 20:24
Contact:

Re: [Class] LogClass - Logging and log file management class

16 Jul 2018, 21:52

More extensive than mine, I may take some ideas from it :P, I like that its really customizable.

heres mine for reference:
Spoiler
Windows 10, Ryzen 1600, 16GB G.Skill DDR4, 8GB RX 480 | [MyScripts][MySublimeSettings] [Unlicense][MIT License]
01/24/18
[/color]
User avatar
Chunjee
Posts: 1400
Joined: 18 Apr 2014, 19:05
Contact:

Re: [Class] LogClass - Logging and log file management class

17 Jul 2018, 11:27

maxNumbOldLogs_Default and maxSizeMBLogFile_Default are basically ignored. These two attributes have default inputs and are never used again outside of their respective setters.

This is a little baffling as they both have comments that I would think are to draw attention to where the user would make their changes, but these changes are never recognized for the reason stated above.


I've made some logic changes to how those are applied and re-ordered the __New parameters because the log location seems more important than the max MB or max OldLogs to keep.
ahbi
Posts: 17
Joined: 15 Jun 2016, 17:11

Re: [Class] LogClass - Logging and log file management class

07 Oct 2018, 16:49

Chunjee wrote:My application usually just runs once a day, but occasionally more often. In those cases the original log file gets overwritten. Is there a built-in way to control this? I'd rather it just keep appending or rotate the logs and keep the original intact somehow.
There is.

My initial idea was you just wouldn't use initalizeNewLogFile()/finalizeLog(), but based on your comment I changed how initalizeNewLogFile()/finalizeLog() work.

Look into the updated LogClass_Examples.ahk (above) at Examples 7a-7c. They will show you how to append to a monolithic log file.
Chunjee wrote: maxNumbOldLogs_Default and maxSizeMBLogFile_Default are basically ignored. These two attributes have default inputs and are never used again outside of their respective setters.
I am guessing your expectations differ from my thought processes.

Anything *_Defaults is essentially a programmer defined Constant (as much as AHK has constants). The user (user of LogClass) should not be changing those. The user should be changing maxNumbOldLogs (no "_Default"). The *_Defaults are to provide baseline, configuration-free, error-checked values the user shouldn't have to think about, or kick in if the user tries to set things to a bad value. The user of the class shouldn't be messing with those.
Chunjee wrote: I'm trying to figure it out. the library doesn't periodically call .savePendingEntriesToLog() by itself. I was expecting somekind of "Is the CPU idle enough to start writing?" timer.
Nope. It is entirely up to the user when they want to savePendingEntriesToLog().

Chunjee wrote: I wrote a custom method that just writes the entry as a stringified JSON object.
and
nnnik wrote: A thing that would be nice to have is something like logObject which could handle objects.
It was left up to the user (of the class) how they wanted to format the Strings for addLogEntry(). And how they wanted to give the formatted strings to addLogEntry().
If you want JSON strings go for it.
Same with Objects. In my code I use addLogEntry(myotherclass.ToString()). So, object conversion wasn't something I was after.

But, as nnnik gave me that code, I have incorporated it (currently commented out) into the new version. So, if someone doesn't want to wait (and given my life it could be awhile), they can uncomment that and write their own addLogObject() conversion function.

Return to “Scripts and Functions (v1)”

Who is online

Users browsing this forum: Google [Bot] and 126 guests