diff --git a/Modules.Logging/LogConfig.vb b/Modules.Logging/LogConfig.vb index e4c88a26..99a6959c 100644 --- a/Modules.Logging/LogConfig.vb +++ b/Modules.Logging/LogConfig.vb @@ -71,9 +71,9 @@ Public Class LogConfig 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_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 FILE_NAME_FORMAT_DEFAULT As String = "${shortdate}-${var:product}${var:suffix}${event-properties:item=ModuleName}.log" + Private Const FILE_NAME_FORMAT_DEBUG As String = "${shortdate}-${var:product}${var:suffix}${event-properties:item=ModuleName}-Debug.log" + Private Const FILE_NAME_FORMAT_ERROR As String = "${shortdate}-${var:product}${var:suffix}${event-properties:item=ModuleName}-Error.log" Private Const TARGET_DEFAULT As String = "defaultTarget" Private Const TARGET_ERROR_EX As String = "errorExceptionTarget" @@ -93,11 +93,14 @@ Public Class LogConfig Private Const FILE_NAME_ACCESS_TEST = "accessTest.txt" Private Const FOLDER_NAME_LOG = "Log" - Private ReadOnly failSafePath As String = Path.GetTempPath() - Private ReadOnly basePath As String = failSafePath + Private Const FILE_KEEP_RANGE As Integer = 90 + + Private ReadOnly _failSafePath As String = Path.GetTempPath() + Private ReadOnly _basePath As String = _failSafePath + + Private _config As LoggingConfiguration + Private _isDebug As Boolean = False - Private config As LoggingConfiguration - Private isDebug As Boolean = False #End Region #Region "Public Properties" Public Enum PathType As Integer @@ -131,11 +134,10 @@ Public Class LogConfig ''' True, if debug log will be written. False otherwise. Public Property Debug As Boolean Get - Return isDebug + Return _isDebug End Get Set(isDebug As Boolean) - Me.isDebug = isDebug - 'GetLogger().Debug("=> Debug is now {0}", isDebug) + _isDebug = isDebug ReloadConfig(isDebug) End Set End Property @@ -147,19 +149,27 @@ Public Class LogConfig ''' A list of log messages Public ReadOnly Property Logs As List(Of String) Get - Dim oTarget = config.FindTargetByName(Of MemoryTarget)(TARGET_MEMORY) + Dim oTarget = _config.FindTargetByName(Of MemoryTarget)(TARGET_MEMORY) Return oTarget?.Logs.ToList() End Get End Property Public ReadOnly Property NLogConfig As LoggingConfiguration Get - Return config + Return _config End Get End Property #End Region + ''' + ''' Initializes a new LogConfig object with the options supplied as a LogOptions object + ''' + ''' + Public Sub New(Options As LogOptions) + MyClass.New(Options.LogPath, Options.CustomLogPath, Options.Suffix, Options.CompanyName, Options.ProductName, Options.FileKeepInterval) + End Sub + ''' ''' Initializes a new LogConfig object with a logpath and optinally a filename-suffix. ''' @@ -168,11 +178,13 @@ Public Class LogConfig ''' If set to anything other than Nothing, extends the logfile name with this suffix. ''' CompanyName is used to construct log-path in when LogPath is set to PathType:AppData ''' ProductName is used to construct log-path in when LogPath is set to PathType:AppData + ''' Amount of days where files are kept and not deleted. Public Sub New(LogPath As PathType, Optional CustomLogPath As String = Nothing, Optional Suffix As String = Nothing, Optional CompanyName As String = Nothing, - Optional ProductName As String = Nothing) + Optional ProductName As String = Nothing, + Optional FileKeepRangeInDays As Integer = FILE_KEEP_RANGE) If LogPath = PathType.AppData And (ProductName Is Nothing Or CompanyName Is Nothing) Then Throw New ArgumentException("Modules.Logging: PathType is AppData and either CompanyName or ProductName was not supplied!") @@ -184,26 +196,26 @@ Public Class LogConfig If LogPath = PathType.AppData Then Dim appDataDir = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) - basePath = Path.Combine(appDataDir, CompanyName, ProductName, FOLDER_NAME_LOG) + _basePath = Path.Combine(appDataDir, CompanyName, ProductName, FOLDER_NAME_LOG) ElseIf LogPath = PathType.Temp Then - basePath = failSafePath + _basePath = _failSafePath Else 'Custom Path - basePath = CustomLogPath + _basePath = CustomLogPath End If ' If directory does not exist, try to create it! - If Not Directory.Exists(basePath) Then + If Not Directory.Exists(_basePath) Then Try - Directory.CreateDirectory(basePath) + Directory.CreateDirectory(_basePath) Catch ex As Exception ' If creation fails, use failSafe path - basePath = failSafePath + _basePath = _failSafePath End Try End If ' Try to create a file in `basePath` to check write permissions Try - Dim fileAccessPath = Path.Combine(basePath, FILE_NAME_ACCESS_TEST) + Dim fileAccessPath = Path.Combine(_basePath, FILE_NAME_ACCESS_TEST) Using fs As FileStream = File.Create(fileAccessPath) fs.WriteByte(0) End Using @@ -211,7 +223,7 @@ Public Class LogConfig File.Delete(fileAccessPath) Catch ex As Exception ' If creation fails, use failSafe path - basePath = failSafePath + _basePath = _failSafePath End Try ' Set the suffix to the given string if it exists @@ -228,30 +240,69 @@ Public Class LogConfig End If ' Create config object and initalize it - config = GetConfig(oProductName, logFileSuffix) + _config = GetConfig(oProductName, logFileSuffix) ' Save config LogFactory = New LogFactory With { - .Configuration = config + .Configuration = _config } ' Save log paths for files/directory - LogDirectory = basePath + LogDirectory = _basePath LogFile = GetCurrentLogFilePath() + + ' Clear old Logfiles as defined in `FileKeepInterval` + ClearOldLogfiles(FileKeepRangeInDays) End Sub - Private Sub CheckMyApplication() - Dim oAssembly = Assembly.GetEntryAssembly() - Dim oMyApp = Nothing - For Each oType As Type In oAssembly.DefinedTypes - If oType.Name = "MyApplication" Then - oMyApp = oType - Exit For + ''' + ''' Clears old LogFiles from the configured logpath for compliance with the GDPR + ''' + ''' Days in which logfiles should be kept. All files older than `Now - FileKeepInterval` will be deleted. + ''' True, if files were deleted as expected or no files were deleted. Otherwise false. + Private Function ClearOldLogfiles(FileKeepRange As Integer) As Boolean + Dim oClassName As String = GetClassFullName() + Dim oLogger As Logger = GetLogger(oClassName) + + Try + Dim oContinueOnError = True + Dim oUnableToDeleteCounter = 0 + Dim oDirectory As New DirectoryInfo(LogDirectory) + Dim oDateLimit As Date = Date.Now.AddDays(-FileKeepRange) + Dim oFiles As List(Of FileInfo) = oDirectory. + EnumerateFiles(). + Where(Function(oFileInfo As FileInfo) oFileInfo.Extension = ".log" And oFileInfo.LastWriteTime < oDateLimit). + ToList() + + If oFiles.Count = 0 Then + oLogger.Info("No logfiles were marked for deletion in the range [last {0} days].", FileKeepRange) + Return True End If - Next - oMyApp.GetType().GetProperty("") - End Sub + oLogger.Info("Deleting [{0}] old logfiles that are marked for deletion in the range [last {1} days].", oFiles.Count, FileKeepRange) + + For Each oFile As FileInfo In oFiles + Try + oFile.Delete() + Catch ex As Exception + oUnableToDeleteCounter += 1 + oLogger.Warn("File {0} could not be deleted!") + End Try + Next + + If oUnableToDeleteCounter > 0 Then + oLogger.Info("Delete old logfiles partially. {0} files could not be deleted.", oUnableToDeleteCounter) + Else + oLogger.Info("Deleted [{0}] old logfiles.", oFiles.Count) + End If + + Return True + Catch ex As Exception + oLogger.Error(ex) + + Return False + End Try + End Function ''' ''' Returns the Logger for the calling class @@ -260,7 +311,25 @@ Public Class LogConfig Public Function GetLogger() As Logger Dim oClassName As String = GetClassFullName() - Return LogFactory.GetLogger(Of Logger)(oClassName) + Return GetLogger(oClassName, String.Empty) + End Function + + ''' + ''' Returns the Logger for the specified classname + ''' + ''' An object of Logging.Logger + + Public Function GetLogger(ClassName As String) As Logger + Return GetLogger(ClassName, String.Empty) + End Function + + ''' + ''' Returns the Logger for the specified module + ''' + ''' An object of Logging.Logger + Public Function GetLoggerWithModule(ModuleName As String) As Logger + Dim oClassName As String = GetClassFullName() + Return GetLogger(oClassName, ModuleName) End Function ''' @@ -269,15 +338,17 @@ Public Class LogConfig ''' The name of the class the logger belongs to ''' An object of Logging.Logger - Public Function GetLogger(ClassName As String) As Logger - Return LogFactory.GetLogger(Of Logger)(ClassName) + Public Function GetLogger(ClassName As String, ModuleName As String) As Logger + Dim oLogger = LogFactory.GetLogger(Of Logger)(ClassName) + oLogger.ModuleName = ModuleName + Return oLogger End Function ''' ''' Clears the internal log ''' Public Sub ClearLogs() - Dim oTarget = config.FindTargetByName(Of MemoryTarget)(TARGET_MEMORY) + Dim oTarget = _config.FindTargetByName(Of MemoryTarget)(TARGET_MEMORY) oTarget?.Logs.Clear() End Sub @@ -336,21 +407,21 @@ Public Class LogConfig ''' The chosen suffix ''' A NLog.LoggingConfiguration object Private Function GetConfig(productName As String, logFileSuffix As String) As LoggingConfiguration - config = New LoggingConfiguration() - config.Variables("product") = productName - config.Variables("suffix") = logFileSuffix + _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)) - config.AddTarget(TARGET_MEMORY, GetMemoryDebugTarget()) + _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)) + _config.AddTarget(TARGET_MEMORY, GetMemoryDebugTarget()) ' Add default rules - AddDefaultRules(config) + AddDefaultRules(_config) - Return config + Return _config End Function ''' @@ -371,7 +442,7 @@ Public Class LogConfig ''' Full path of the current default log file Private Function GetCurrentLogFilePath() Dim logEventInfo As New LogEventInfo() With {.TimeStamp = Date.Now} - Dim target As FileTarget = config.FindTargetByName(TARGET_DEFAULT) + Dim target As FileTarget = _config.FindTargetByName(TARGET_DEFAULT) Dim fileName As String = target.FileName.Render(logEventInfo) Return fileName @@ -383,15 +454,15 @@ Public Class LogConfig ''' Adds the Debug rule if true. Private Sub ReloadConfig(Optional Debug As Boolean = False) ' Clear Logging Rules - config.LoggingRules.Clear() + _config.LoggingRules.Clear() ' Add default rules - AddDefaultRules(config) + AddDefaultRules(_config) ' Add debug rule, if configured If Debug Then - config.AddRuleForAllLevels(TARGET_DEBUG) - config.AddRuleForAllLevels(TARGET_MEMORY) + _config.AddRuleForAllLevels(TARGET_DEBUG) + _config.AddRuleForAllLevels(TARGET_MEMORY) End If ' Reload all running loggers diff --git a/Modules.Logging/LogOptions.vb b/Modules.Logging/LogOptions.vb new file mode 100644 index 00000000..98c42933 --- /dev/null +++ b/Modules.Logging/LogOptions.vb @@ -0,0 +1,10 @@ +Imports DigitalData.Modules.Logging.LogConfig + +Public Class LogOptions + Property LogPath As PathType + Property CustomLogPath As String = Nothing + Property Suffix As String = Nothing + Property CompanyName As String = Nothing + Property ProductName As String = Nothing + Property FileKeepInterval As Integer = 0 +End Class diff --git a/Modules.Logging/Logger.vb b/Modules.Logging/Logger.vb index c4d978b1..42167a74 100644 --- a/Modules.Logging/Logger.vb +++ b/Modules.Logging/Logger.vb @@ -4,6 +4,59 @@ Public Class Logger Inherits NLog.Logger Implements ILogger + ''' + ''' Optional ModuleName to create different LogFiles for different Modules (arbitrary entities, User, File, etc.) + ''' + ''' The current ModuleName of the logger, if any. + Public Property ModuleName As String = Nothing + + Public Overloads Sub Info(Message As String) + LogWithModuleName(Message, LogLevel.Info) + End Sub + + Public Overloads Sub Warn(Message As String) + LogWithModuleName(Message, LogLevel.Warn) + End Sub + + Public Overloads Sub [Error](Exception As Exception) + LogWithModuleName(Exception) + End Sub + + Public Overloads Sub Debug(Message As String) + LogWithModuleName(Message, LogLevel.Debug) + End Sub + + Public Overloads Sub Trace(Message As String) + LogWithModuleName(Message, LogLevel.Trace) + End Sub + + Private Sub LogWithModuleName(Exception As Exception) + Dim oEventInfo As New LogEventInfo() With { + .Exception = Exception, + .Level = LogLevel.Error, + .LoggerName = Name, + .Message = Exception.Message + } + oEventInfo = MaybeSetModuleName(oEventInfo) + + Log(oEventInfo) + End Sub + + Private Sub LogWithModuleName(Message As String, LogLevel As LogLevel) + Dim oEventInfo As New LogEventInfo(LogLevel, Name, Message) + oEventInfo = MaybeSetModuleName(oEventInfo) + + Log(oEventInfo) + End Sub + + Private Function MaybeSetModuleName(LogEventInfo As LogEventInfo) As LogEventInfo + If ModuleName IsNot Nothing AndAlso ModuleName <> String.Empty Then + LogEventInfo.Properties.Item("ModuleName") = ModuleName + End If + + Return LogEventInfo + End Function + ''' ''' Prints a preformatted Block including a block identifier ''' diff --git a/Modules.Logging/Logging.vbproj b/Modules.Logging/Logging.vbproj index ad3e27ef..3281a6f9 100644 --- a/Modules.Logging/Logging.vbproj +++ b/Modules.Logging/Logging.vbproj @@ -74,6 +74,7 @@ + True