Monorepo/Modules.Logging/LogConfig.vb

396 lines
15 KiB
VB.net

Imports System.IO
Imports NLog
Imports NLog.Config
Imports NLog.Targets
''' <module>LogConfig</module>
''' <version>0.0.0.5</version>
''' <date>02.10.2018</date>
''' <summary>
''' Module that writes file-logs to different locations:
''' local application data, the current directory or a custom path.
''' Files and directories will be automatically created.
''' </summary>
''' <dependencies>
''' NLog, >= 4.5.8
''' </dependencies>
''' <params>
''' logPath, PathType
''' The basepath to write logs to. Can be AppData, CurrentDirectory or CustomPath.
'''
''' - AppData: writes to local application data directory
''' - CurrentDirectory: writes to `Log` directory relative to the current directory
''' - CustomPath: writes to custom path specified in `customLogPath`
'''
''' customLogPath, String (optional)
''' If `logPath` is set to custom, this defines the custom logPath.
'''
''' suffix, String (optional)
''' If set to anything other than Nothing, extends the logfile name with this suffix.
''' </params>
''' <props>
''' LogFile, String (readonly)
''' Returns the full path of the default log file.
'''
''' LogPath, String (readonly)
''' Returns the path to the log directory.
'''
''' LogFactory, NLog.LogFactory (readonly)
''' Returns the LogFactory that is used to create the Logger object
'''
''' Debug, Boolean
''' Determines if the debug log should be written.
''' </props>
''' <example>
''' Imports DigitalData.Modules.Logging
'''
''' Class FooProgram
''' Private Logger as Logger
''' Private LogConfig as LogConfig
'''
''' Public Sub New()
''' LogConfig = new LogConfig(args)
''' Logger = LogConfig.GetLogger()
''' End Sub
'''
''' Public Sub Bar()
''' Logger.Info("Baz")
''' End Sub
''' End Class
'''
''' Class FooLib
''' Private Logger as NLog.Logger
'''
''' Public Sub New(LogConfig as LogConfig)
''' Logger = LogConfig.GetLogger()
''' End Sub
'''
''' Public Sub Bar()
''' Logger.Info("Baz")
''' End Sub
''' End Class
''' </example>
''' <remarks>
''' If logpath can not be written to, falls back to temp folder as defined in:
''' https://docs.microsoft.com/de-de/dotnet/api/system.io.path.gettemppath?view=netframework-4.7.2
'''
''' If used in a service, LogPath must be set to CustomPath, otherwise the Log will be written to System32!
'''
''' For NLog Troubleshooting, set the following Environment variables to write the NLog internal Log:
''' - NLOG_INTERNAL_LOG_LEVEL: Debug
''' - NLOG_INTERNAL_LOG_FILE: ex. C:\Temp\Nlog_Internal.log
''' </remarks>
Public Class LogConfig
Private Const KEEP_FILES_OPEN As Boolean = False
' MAX_ARCHIVES_FILES works like this (in version 4.5.8):
' 0 = keep ALL archives files
' 1 = only keep latest logfile and NO archive files
' n = keep n archive files
Private Const MAX_ARCHIVE_FILES_DEFAULT As Integer = 0
Private Const MAX_ARCHIVE_FILES_DEBUG_DETAIL As Integer = 0
Private Const ARCHIVE_EVERY As FileArchivePeriod = FileArchivePeriod.Day
Private Const FILE_NAME_FORMAT_DEFAULT As String = "${shortdate}-${var:product}${var:suffix}.log"
Private Const FILE_NAME_FORMAT_DETAIL As String = "${shortdate}-${var:product}${var:suffix}-Detail.log"
Private Const FILE_NAME_FORMAT_DEBUG As String = "${shortdate}-${var:product}${var:suffix}-Debug.log"
Private Const FILE_NAME_FORMAT_ERROR As String = "${shortdate}-${var:product}${var:suffix}-Error.log"
Private Const TARGET_DEFAULT As String = "defaultTarget"
Private Const TARGET_ERROR_EX As String = "errorExceptionTarget"
Private Const TARGET_ERROR As String = "errorTarget"
Private Const TARGET_DETAIL As String = "detailTarget"
Private Const TARGET_DEBUG As String = "debugTarget"
Private Const DATE_FORMAT_LONG As String = "${longdate}"
Private Const DATE_FORMAT_DEFAULT As String = "${date:format=yyyy-MM-dd HH\:mm\:ss}"
Private Const LOG_FORMAT_BASE As String = DATE_FORMAT_DEFAULT & "|${logger:shortName=True}|${level:uppercase=true}"
Private Const LOG_FORMAT_BASE_LONG_DATE As String = DATE_FORMAT_LONG & "|${logger:shortName=True}|${level:uppercase=true}"
Private Const LOG_FORMAT_DEFAULT As String = LOG_FORMAT_BASE & " >> ${message}"
Private Const LOG_FORMAT_EXCEPTION As String = LOG_FORMAT_BASE & " >> ${exception:format=Message}${newline}${exception:format=StackTrace}"
Private Const LOG_FORMAT_DEBUG As String = LOG_FORMAT_BASE_LONG_DATE & " >> ${message}"
Private ReadOnly failSafePath As String = Path.GetTempPath()
Private ReadOnly basePath As String = failSafePath
Private config As LoggingConfiguration
Private isDebug As Boolean = False
Public Enum PathType As Integer
AppData = 0
CurrentDirectory = 1
CustomPath = 2
End Enum
''' <summary>
''' Returns the NLog.LogFactory object that is used to create Loggers
''' </summary>
''' <returns>LogFactory object</returns>
Public ReadOnly Property LogFactory As LogFactory
''' <summary>
''' Returns the path to the current default logfile
''' </summary>
''' <returns>Filepath to the logfile</returns>
Public ReadOnly Property LogFile As String
''' <summary>
''' Returns the path to the current log directory
''' </summary>
''' <returns>Directory path to the log directory</returns>
Public ReadOnly Property LogDirectory As String
''' <summary>
''' Determines if a debug log will be written
''' </summary>
''' <returns>True, if debug log will be written. False otherwise.</returns>
Public Property Debug As Boolean
Get
Return isDebug
End Get
Set(isDebug As Boolean)
Me.isDebug = isDebug
ReloadConfig(isDebug)
End Set
End Property
''' <summary>
''' Initializes a new LogConfig object with a logpath and optinally a filename-suffix.
''' </summary>
''' <param name="logPath">The basepath to write logs to. Can be AppData, CurrentDirectory or CustomPath.</param>
''' <param name="customLogPath">If `logPath` is set to custom, this defines the custom logPath.</param>
''' <param name="suffix">If set to anything other than Nothing, extends the logfile name with this suffix.</param>
Public Sub New(logPath As PathType, Optional customLogPath As String = Nothing, Optional suffix As String = Nothing)
Dim productName As String = My.Application.Info.ProductName
Dim companyName As String = My.Application.Info.CompanyName
If logPath = PathType.AppData Then
Dim appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData)
basePath = Path.Combine(appDataDir, companyName, productName)
ElseIf logPath = PathType.CurrentDirectory Then
Dim currentDirectory As String = My.Application.Info.DirectoryPath
basePath = Path.Combine(currentDirectory, "Log")
Else 'Custom Path
basePath = customLogPath
End If
' If directory does not exist, try to create it!
If Not Directory.Exists(basePath) Then
Try
Directory.CreateDirectory(basePath)
Catch ex As Exception
' If creation fails, use failSafe path
basePath = failSafePath
End Try
End If
' Try to create a file in `basePath` to check write permissions
Try
Dim fileAccessPath = Path.Combine(basePath, "accessTest.txt")
Using fs As FileStream = File.Create(fileAccessPath)
fs.WriteByte(0)
End Using
File.Delete(fileAccessPath)
Catch ex As UnauthorizedAccessException
' If creation fails, use failSafe path
basePath = failSafePath
End Try
' Set the suffix to the given string if it exists
Dim logFileSuffix As String = String.Empty
If suffix IsNot Nothing AndAlso suffix.Count > 0 Then
logFileSuffix = $"-{suffix}"
End If
' Create config object and initalize it
config = GetConfig(productName, logFileSuffix)
' Save config
LogFactory = New LogFactory With {
.Configuration = config
}
' Save log paths for files/directory
LogDirectory = basePath
LogFile = GetCurrentLogFilePath()
End Sub
''' <summary>
''' Returns the Logger for the calling class
''' </summary>
''' <returns>An object of Logging.Logger</returns>
Public Function GetLogger() As Logger
Dim oClassName As String = GetClassFullName()
Return LogFactory.GetLogger(Of Logger)(oClassName)
End Function
''' <summary>
''' Gets the fully qualified name of the class invoking the calling method,
''' including the namespace but Not the assembly.
''' </summary>
''' <returns>The fully qualified class name</returns>
''' <remarks>This method is very resource-intensive!</remarks>
Public Shared Function GetClassFullName() As String
Dim oFramesToSkip As Integer = 2
Dim oClassName As String = String.Empty
Dim oStackTrace = Environment.StackTrace
Dim oStackTraceLines = oStackTrace.Replace(vbCr, "").Split({vbLf}, StringSplitOptions.RemoveEmptyEntries)
For i As Integer = 0 To oStackTraceLines.Length - 1
Dim oCallingClassAndMethod = oStackTraceLines(i).Split({" ", "<>", "(", ")"}, StringSplitOptions.RemoveEmptyEntries)(1)
Dim oMethodStartIndex As Integer = oCallingClassAndMethod.LastIndexOf(".", StringComparison.Ordinal)
If oMethodStartIndex > 0 Then
Dim oCallingClass = oCallingClassAndMethod.Substring(0, oMethodStartIndex)
oClassName = oCallingClass.TrimEnd("."c)
If Not oClassName.StartsWith("System.Environment") AndAlso oFramesToSkip <> 0 Then
i += oFramesToSkip - 1
oFramesToSkip = 0
Continue For
End If
If Not oClassName.StartsWith("System.") Then Exit For
End If
Next
Return oClassName
End Function
''' <summary>
''' Returns the initial log configuration
''' </summary>
''' <param name="productName">The chosen productname</param>
''' <param name="logFileSuffix">The chosen suffix</param>
''' <returns>A NLog.LoggingConfiguration object</returns>
Private Function GetConfig(productName As String, logFileSuffix As String) As LoggingConfiguration
config = New LoggingConfiguration()
config.Variables("product") = productName
config.Variables("suffix") = logFileSuffix
' Add default targets
config.AddTarget(TARGET_ERROR_EX, GetErrorExceptionLogTarget(basePath))
config.AddTarget(TARGET_ERROR, GetErrorLogTarget(basePath))
config.AddTarget(TARGET_DEFAULT, GetDefaultLogTarget(basePath))
config.AddTarget(TARGET_DEBUG, GetDebugLogTarget(basePath))
' Add default rules
AddDefaultRules(config)
Return config
End Function
''' <summary>
''' Adds the default rules
''' </summary>
''' <param name="config">A NLog.LoggingConfiguration object</param>
Private Sub AddDefaultRules(ByRef config As LoggingConfiguration)
config.AddRuleForOneLevel(LogLevel.Error, TARGET_ERROR_EX)
config.AddRuleForOneLevel(LogLevel.Fatal, TARGET_ERROR_EX)
config.AddRuleForOneLevel(LogLevel.Warn, TARGET_ERROR)
config.AddRuleForOneLevel(LogLevel.Warn, TARGET_DEFAULT)
config.AddRuleForOneLevel(LogLevel.Info, TARGET_DEFAULT)
End Sub
''' <summary>
''' Returns the full path of the current default log file.
''' </summary>
''' <returns>Full path of the current default log file</returns>
Private Function GetCurrentLogFilePath()
Dim logEventInfo As New LogEventInfo() With {.TimeStamp = Date.Now}
Dim target As FileTarget = config.FindTargetByName(TARGET_DEFAULT)
Dim fileName As String = target.FileName.Render(logEventInfo)
Return fileName
End Function
''' <summary>
''' Reconfigures and re-adds all loggers, optionally adding the debug rule.
''' </summary>
''' <param name="Debug">Adds the Debug rule if true.</param>
Private Sub ReloadConfig(Optional Debug As Boolean = False)
' Clear Logging Rules
config.LoggingRules.Clear()
' Add default rules
AddDefaultRules(config)
' Add debug rule, if configured
If Debug Then
config.AddRuleForOneLevel(LogLevel.Debug, TARGET_DEBUG)
End If
' Reload all running loggers
LogFactory.ReconfigExistingLoggers()
End Sub
#Region "Log Targets"
Private Function GetDefaultLogTarget(basePath As String) As FileTarget
Dim defaultLog As New FileTarget() With {
.FileName = Path.Combine(basePath, FILE_NAME_FORMAT_DEFAULT),
.Name = TARGET_DEFAULT,
.Layout = LOG_FORMAT_DEFAULT,
.MaxArchiveFiles = MAX_ARCHIVE_FILES_DEFAULT,
.ArchiveEvery = ARCHIVE_EVERY,
.KeepFileOpen = KEEP_FILES_OPEN
}
Return defaultLog
End Function
Private Function GetErrorExceptionLogTarget(basePath As String) As FileTarget
Dim errorLogWithExceptions As New FileTarget() With {
.FileName = Path.Combine(basePath, FILE_NAME_FORMAT_ERROR),
.Name = TARGET_ERROR_EX,
.Layout = LOG_FORMAT_EXCEPTION,
.MaxArchiveFiles = MAX_ARCHIVE_FILES_DEFAULT,
.ArchiveEvery = ARCHIVE_EVERY,
.KeepFileOpen = KEEP_FILES_OPEN
}
Return errorLogWithExceptions
End Function
Private Function GetErrorLogTarget(basePath As String) As FileTarget
Dim errorLog As New FileTarget() With {
.FileName = Path.Combine(basePath, FILE_NAME_FORMAT_ERROR),
.Name = TARGET_ERROR,
.Layout = LOG_FORMAT_DEFAULT,
.MaxArchiveFiles = MAX_ARCHIVE_FILES_DEFAULT,
.ArchiveEvery = ARCHIVE_EVERY,
.KeepFileOpen = KEEP_FILES_OPEN
}
Return errorLog
End Function
Private Function GetDetailLogTarget(basePath As String) As FileTarget
Dim detailLog As New FileTarget() With {
.FileName = Path.Combine(basePath, FILE_NAME_FORMAT_DETAIL),
.Name = TARGET_DETAIL,
.Layout = LOG_FORMAT_DEFAULT,
.MaxArchiveFiles = MAX_ARCHIVE_FILES_DEBUG_DETAIL,
.ArchiveEvery = ARCHIVE_EVERY,
.KeepFileOpen = KEEP_FILES_OPEN
}
Return detailLog
End Function
Private Function GetDebugLogTarget(basePath As String) As FileTarget
Dim debugLog As New FileTarget() With {
.FileName = Path.Combine(basePath, FILE_NAME_FORMAT_DEBUG),
.Name = TARGET_DEBUG,
.Layout = LOG_FORMAT_DEFAULT,
.MaxArchiveFiles = MAX_ARCHIVE_FILES_DEBUG_DETAIL,
.ArchiveEvery = ARCHIVE_EVERY,
.KeepFileOpen = KEEP_FILES_OPEN
}
Return debugLog
End Function
#End Region
End Class