From e8d14548fc5b127970fccb19bad146512f7ac6d9 Mon Sep 17 00:00:00 2001 From: Greg Burri Date: Thu, 4 May 2017 21:13:53 +0200 Subject: [PATCH] Update the Logger component. --- Parasitemia/Logger/Logger.fs | 267 ++++++++++++------ Parasitemia/ParasitemiaCore/Analysis.fs | 6 +- .../ParasitemiaCore/ParasitemiaCore.fsproj | 6 +- Parasitemia/ParasitemiaCore/packages.config | 6 +- Parasitemia/ParasitemiaUI/Analysis.fs | 6 +- Parasitemia/ParasitemiaUI/GUI.fs | 8 +- .../ParasitemiaUI/ParasitemiaUI.fsproj | 4 +- Parasitemia/ParasitemiaUI/Program.fs | 14 +- Parasitemia/ParasitemiaUI/Utils.fs | 4 +- Parasitemia/ParasitemiaUI/packages.config | 4 +- 10 files changed, 205 insertions(+), 120 deletions(-) diff --git a/Parasitemia/Logger/Logger.fs b/Parasitemia/Logger/Logger.fs index 1b4c332..df2144f 100644 --- a/Parasitemia/Logger/Logger.fs +++ b/Parasitemia/Logger/Logger.fs @@ -3,30 +3,61 @@ open System open System.Text open System.IO +open System.IO.Compression open System.Diagnostics open System.Threading open System.Collections.Generic -type Severity = DEBUG = 1 | USER = 2 | WARNING = 3 | ERROR = 4 | FATAL = 5 +type Severity = DEBUG = 1 | INFO = 2 | WARNING = 3 | ERROR = 4 | FATAL = 5 -type IListener = abstract NewEntry : Severity -> string -> unit +type IListener = + abstract NewEntry : severity : Severity -> header : string -> message : string -> unit + +type private Message = + { + Message : string + ThreadName : string + ThreadId : int + ModuleCaller : string + Severity : Severity + } + +type private Command = + | Write of Message + | Stop of AsyncReplyChannel [] type Log () = - let maxSizeFile = 10L * 1024L * 1024L // [byte] (10 MB). - let nbEntriesCheckSize = 100; // Each 100 entries added we check the size of the log file to test if it is greater than 'MAX_SIZE_FILE'. - let LogDefaultDirectory = "Parasitemia\\Log" - let filenameFormat = "{0:D4}.log" + + let extractNumberFromLogfilepath (path : string) : int option = + if isNull path then + None + else + let filename = path.Substring(path.LastIndexOf(Path.DirectorySeparatorChar) + 1) + let filenameWithoutExtension = filename.Remove(filename.IndexOf('.')) + match Int32.TryParse(filenameWithoutExtension) with + | (true, n) -> Some n + | _ -> None + + let [] MAX_SIZE_FILE = 52428800L // [byte] (50 MB). + let [] NB_ENTRIES_CHECK_SIZE = 100; // Each 100 entries added we check the size of the log file to test if it is greater than 'MAX_SIZE_FILE'. + let [] COMPRESS_ARCHIVED_FILES = true + let [] FILENAME_FORMAT = "{0:D4}.log" + let [] COMPRESSED_FILE_POSTFIX = ".gzip" let encoding = Encoding.GetEncoding("UTF-8") + let compress (filename : string) = + use inputStream = new FileStream(filename, FileMode.Open, FileAccess.Read) + let filenameCompressed = filename + COMPRESSED_FILE_POSTFIX + use compressedStream = new GZipStream(new FileStream(filenameCompressed, FileMode.Create, FileAccess.Write), CompressionLevel.Optimal) + inputStream.CopyTo(compressedStream) + let moduleName = System.Diagnostics.StackFrame(1).GetMethod().Module.Name let mutable stream : StreamWriter = null + let mutable filename : string = null let mutable logDir : string = null - let mutable absoluteDir : string = null - - let mutable nbEntries = 0L let monitor = Object() @@ -41,120 +72,174 @@ type Log () = static let instance = new Log() + let openLogFile (entryNumber : int64) = + if not (isNull logDir) then + try + if isNull stream || (entryNumber % (int64 NB_ENTRIES_CHECK_SIZE) = 0L) && stream.BaseStream.Length > MAX_SIZE_FILE + then + if not (isNull stream) + then + stream.Close() + if COMPRESS_ARCHIVED_FILES then + compress filename + File.Delete(filename) + + // Search the last id among the log files. + let mutable n = 1 + for existingFile in Directory.GetFiles(logDir) do + match extractNumberFromLogfilepath existingFile with + | Some n' when n' > n -> n <- n' + | _ -> () + + filename <- Path.Combine(logDir, String.Format(FILENAME_FORMAT, n)) + try + if File.Exists(filename + COMPRESSED_FILE_POSTFIX) || FileInfo(filename).Length > MAX_SIZE_FILE + then + filename <- Path.Combine(logDir, String.Format(FILENAME_FORMAT, n + 1)) + with + | :? FileNotFoundException -> () // The file may not exist. + + stream <- new StreamWriter(filename, true, encoding) + with + | ex -> Console.Error.WriteLine("Can't open the file log: {0}", ex) + + let write (msg : Message) (entryNumber : int64) = + openLogFile entryNumber + + let header = + String.Format( + "{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {{{2}}} ({3})", + TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow), + string msg.Severity, + msg.ModuleCaller, + (if String.IsNullOrEmpty(msg.ThreadName) then string msg.ThreadId else sprintf "%s-%i" msg.ThreadName msg.ThreadId) + ) + + for listener in listeners do + listener.NewEntry msg.Severity header msg.Message + + if not (isNull stream) + then + try + stream.WriteLine("{0} : {1}", header, msg.Message) + stream.Flush() + with + | :? IOException as ex -> Console.Error.WriteLine("Unable to write to the log file: {0}", ex) + + let writeAgent = + new MailboxProcessor( + fun inbox -> + let rec loop (nbEntries : int64) = + async { + let! command = inbox.Receive() + match command with + | Write message -> + write message nbEntries + return! loop (nbEntries + 1L) + | Stop replyChannel -> + replyChannel.Reply () + } + loop 1L + ) + + do + writeAgent.Start() + let setLogDirectory (dir : string) = lock monitor (fun () -> logDir <- dir - absoluteDir <- Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), logDir) - if stream <> null then + if not <| isNull stream then stream.Close() stream <- null try - if not <| Directory.Exists(absoluteDir) then - Directory.CreateDirectory(absoluteDir) |> ignore + if not <| Directory.Exists(logDir) + then + Directory.CreateDirectory(logDir) |> ignore with - | _ as ex -> Console.Error.WriteLine("Unable to create the log directory: {0}", absoluteDir)) - - let openLogFile () = - try - if stream = null || (nbEntries % (int64 nbEntriesCheckSize) = 0L) && stream.BaseStream.Length > maxSizeFile then - if stream <> null then - stream.Close() - - let mutable n = 1 - for existingFile in Directory.GetFiles(absoluteDir) do - let current_n = ref 0 - if Int32.TryParse(existingFile.Remove(existingFile.LastIndexOf('.')), current_n) && !current_n > n then - n <- !current_n - - let mutable filename = Path.Combine(absoluteDir, String.Format(filenameFormat, n)) - try - if (FileInfo(filename).Length > maxSizeFile) then - filename <- Path.Combine(absoluteDir, String.Format(filenameFormat, n + 1)) - with - | :? FileNotFoundException -> () // The file may not exist. - - stream <- new StreamWriter(filename, true, encoding) - with - | _ as ex -> Console.Error.WriteLine("Can't open the file log: {0}", ex) - - do - setLogDirectory LogDefaultDirectory + | _ -> Console.Error.WriteLine("Unable to create the log directory: {0}", logDir)) interface IDisposable with member this.Dispose () = - if stream <> null then + if not (isNull stream) + then stream.Dispose() - - member private this.Write (message : string, severity : Severity) = - lock monitor (fun () -> - nbEntries <- nbEntries + 1L - openLogFile () - - if stream <> null then - let mutable moduleNameCaller = moduleName - match StackTrace().GetFrames() |> Array.tryPick (fun frame -> let name = frame.GetMethod().Module.Name - if name <> moduleName then Some name else None) with - | Some name -> moduleNameCaller <- name - | _ -> () - - let threadName = Thread.CurrentThread.Name - - for listener in listeners do - listener.NewEntry severity message - - try - stream.WriteLine( - "{0:yyyy-MM-dd HH:mm:ss.fff} [{1}] {{{2}}} ({3}) : {4}", - TimeZone.CurrentTimeZone.ToLocalTime(DateTime.UtcNow), - severity.ToString(), - moduleNameCaller, - (if String.IsNullOrEmpty(threadName) then Thread.CurrentThread.ManagedThreadId.ToString() else String.Format("{0}-{1}", threadName, Thread.CurrentThread.ManagedThreadId)), - message - ) - stream.Flush() - with - | :? IOException as ex -> Console.Error.WriteLine("Unable to write to the log file: {0}", ex)) - - - member private this.AddListener (listener : IListener) = + (writeAgent :> IDisposable).Dispose() + + member private this.Write (message : string) (severity : Severity) = + let moduleNameCaller = + match StackTrace().GetFrames() |> Array.tryPick (fun frame -> let name = frame.GetMethod().Module.Name + if name <> moduleName then Some name else None) with + | Some name -> name + | _ -> moduleName + + let command = + Write + { + Message = message + ThreadName = Thread.CurrentThread.Name + ThreadId = Thread.CurrentThread.ManagedThreadId + ModuleCaller = moduleNameCaller + Severity = severity + } + + writeAgent.Post command + + /// + /// Will stop and wait a reply. Used to flush the remaining messages. + /// + member private this.Stop () = + writeAgent.PostAndReply( + fun replyChannel -> + Stop replyChannel + ) + + member this.LogDirectory + with get () = logDir + and set value = setLogDirectory value + + static member SetLogDirectory (dir : string) = + instance.LogDirectory <- dir + + member this.AddListener (listener : IListener) = lock monitor (fun () -> - if not <| listeners.Contains(listener) then + if not <| listeners.Contains(listener) + then listeners.Add(listener)) - member private this.RmListener (listener : IListener) = + member this.RmListener (listener : IListener) = lock monitor (fun () -> listeners.Remove(listener) |> ignore) static member AddListener (listener : IListener) = instance.AddListener(listener) static member RmListener (listener : IListener) = instance.RmListener(listener) - static member LogWithTime (message : string, severity : Severity, f : unit -> 'a option, [] args: Object[]) : 'a option = + static member LogWithTime (severity : Severity) (f : unit -> 'a) (format : Printf.StringFormat<'b, 'a>) : 'b = let sw = Stopwatch() sw.Start() let res = f () sw.Stop() - if res.IsSome then - instance.Write(String.Format(message, args) + sprintf " (time: %d ms)" sw.ElapsedMilliseconds, severity) - res + Printf.kprintf (fun s -> instance.Write (s + sprintf " (time: %d ms)" sw.ElapsedMilliseconds) severity; res) format - static member Debug (message : string, [] args : Object[]) = + static member Debug format = #if DEBUG - instance.Write(String.Format(message, args), Severity.DEBUG) + Printf.kprintf (fun s -> instance.Write s Severity.DEBUG) format #else - () + Printf.kprintf (fun _ -> ()) format // TODO: can it be simplify? #endif - static member User (message : string, [] args : Object[]) = - instance.Write(String.Format(message, args), Severity.USER) + static member Info format = + Printf.kprintf (fun s -> instance.Write s Severity.INFO) format - static member Warning (message : string, [] args : Object[]) = - instance.Write(String.Format(message, args), Severity.WARNING) + static member Warning format = + Printf.kprintf (fun s -> instance.Write s Severity.WARNING) format - static member Error (message : string, [] args : Object[]) = - instance.Write(String.Format(message, args), Severity.ERROR) + static member Error format = + Printf.kprintf (fun s -> instance.Write s Severity.ERROR) format - static member Fatal (message : string, [] args : Object[]) = - instance.Write(String.Format(message, args), Severity.FATAL) + static member Fatal format = + Printf.kprintf (fun s -> instance.Write s Severity.FATAL) format + static member Shutdown () = + instance.Stop() diff --git a/Parasitemia/ParasitemiaCore/Analysis.fs b/Parasitemia/ParasitemiaCore/Analysis.fs index afcf4df..f607760 100644 --- a/Parasitemia/ParasitemiaCore/Analysis.fs +++ b/Parasitemia/ParasitemiaCore/Analysis.fs @@ -40,8 +40,8 @@ let doAnalysis (img : Image) (name : string) (config : Config) (repor reportWithVal percent () let inline buildLogWithName (text : string) = sprintf "№ %s: %s" name text - let logWithName mess = Log.User(buildLogWithName mess) - let inline logTimeWithName (text : string) (f : unit -> 'a option) : 'a option = Log.LogWithTime((buildLogWithName text), Severity.USER, f) + let logWithName mess = Log.Info "%s" (buildLogWithName mess) + let inline logTimeWithName (text : string) (f : unit -> 'a option) : 'a option = Log.LogWithTime Severity.INFO f "%s" (buildLogWithName text) // Monadic construction to be able to abort the progress when running. maybe { @@ -186,7 +186,7 @@ let doMultipleAnalysis (imgs : (string * Config * Image) list) (repor | None -> None with | ex -> - Log.Error("Analysis {0} failed: {1}", id, ex) + Log.Error "Analysis %s failed: %O" id ex None ) |> PSeq.withDegreeOfParallelism n diff --git a/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj b/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj index 8275c95..77ee775 100644 --- a/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj +++ b/Parasitemia/ParasitemiaCore/ParasitemiaCore.fsproj @@ -79,13 +79,13 @@ True - ..\packages\FSharp.Core.4.1.2\lib\net45\FSharp.Core.dll + ..\packages\FSharp.Core.4.1.17\lib\net45\FSharp.Core.dll - ..\packages\MathNet.Numerics.3.17.0\lib\net40\MathNet.Numerics.dll + ..\packages\MathNet.Numerics.3.19.0\lib\net40\MathNet.Numerics.dll - ..\packages\MathNet.Numerics.FSharp.3.17.0\lib\net40\MathNet.Numerics.FSharp.dll + ..\packages\MathNet.Numerics.FSharp.3.19.0\lib\net40\MathNet.Numerics.FSharp.dll diff --git a/Parasitemia/ParasitemiaCore/packages.config b/Parasitemia/ParasitemiaCore/packages.config index cbdda29..d8ab14a 100644 --- a/Parasitemia/ParasitemiaCore/packages.config +++ b/Parasitemia/ParasitemiaCore/packages.config @@ -1,8 +1,8 @@  - - - + + + \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/Analysis.fs b/Parasitemia/ParasitemiaUI/Analysis.fs index ceb61be..e276be6 100644 --- a/Parasitemia/ParasitemiaUI/Analysis.fs +++ b/Parasitemia/ParasitemiaUI/Analysis.fs @@ -27,7 +27,7 @@ let showWindow (parent : Window) (state : State.State) : bool = let logListener = { new Logger.IListener with - member this.NewEntry severity mess = + member this.NewEntry severity _header mess = win.Dispatcher.Invoke(fun () -> win.textLog.Inlines.Add(Documents.Run(mess)) win.textLog.Inlines.Add(Documents.LineBreak()) @@ -140,11 +140,11 @@ let showWindow (parent : Window) (state : State.State) : bool = | Some results -> for id, cells in results do state.SetResult (int id) cells - Logger.Log.User("All analyses terminated successfully") + Logger.Log.Info "All analyses terminated successfully" atLeastOneAnalysisPerformed <- true analysisPerformed <- true | None -> - Logger.Log.User("Analysis aborted") + Logger.Log.Info "Analysis aborted" win.Dispatcher.Invoke(fun () -> win.progress.Value <- if maybeResults.IsSome then 100. else 0. diff --git a/Parasitemia/ParasitemiaUI/GUI.fs b/Parasitemia/ParasitemiaUI/GUI.fs index 0a93d90..35bc2ad 100644 --- a/Parasitemia/ParasitemiaUI/GUI.fs +++ b/Parasitemia/ParasitemiaUI/GUI.fs @@ -266,7 +266,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) = displayStatusMessage "Document saved" with | :? IOException as ex -> - Log.Error(ex.ToString()) + Log.Error "%O" ex MessageBox.Show(sprintf "The document cannot be save in \"%s\"" state.FilePath, "Error saving the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore let saveCurrentDocumentAsNewFile () = @@ -384,7 +384,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) = updateGUI () with | :? IOException as ex -> - Log.Error(ex.ToString()) + Log.Error "%O" ex state.FilePath <- previousFilePath MessageBox.Show(sprintf "The document cannot be loaded from \"%s\"" filepath, "Error loading the document", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore @@ -414,7 +414,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) = Export.exportResults state dialog.FileName with | :? IOException as ex -> - Log.Error(ex.ToString()) + Log.Error "%O" ex MessageBox.Show(sprintf "The results cannot be exported in \"%s\"" state.FilePath, "Error exporting the files", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore let importImage () = @@ -429,7 +429,7 @@ let run (defaultConfig : Config) (fileToOpen : string option) = addPreview srcImg with | _ as ex -> - Log.Error(ex.ToString()) + Log.Error "%O" ex MessageBox.Show(sprintf "Unable to read the image from \"%s\"" filename, "Error adding an image", MessageBoxButton.OK, MessageBoxImage.Error) |> ignore updateGlobalParasitemia () diff --git a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj index c5bb1e2..801eac2 100644 --- a/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj +++ b/Parasitemia/ParasitemiaUI/ParasitemiaUI.fsproj @@ -100,7 +100,7 @@ ..\..\..\Emgu\emgucv-windesktop 3.1.0.2282\bin\Emgu.CV.World.dll - ..\packages\FSharp.Core.4.1.2\lib\net45\FSharp.Core.dll + ..\packages\FSharp.Core.4.1.17\lib\net45\FSharp.Core.dll ..\packages\FSharp.ViewModule.Core.1.0.7.0\lib\portable-net45+netcore45+wpa81+wp8+MonoAndroid1+MonoTouch1\FSharp.ViewModule.dll @@ -113,7 +113,7 @@ - ..\packages\Newtonsoft.Json.10.0.1\lib\net45\Newtonsoft.Json.dll + ..\packages\Newtonsoft.Json.10.0.2\lib\net45\Newtonsoft.Json.dll diff --git a/Parasitemia/ParasitemiaUI/Program.fs b/Parasitemia/ParasitemiaUI/Program.fs index 4cc4840..0f6df12 100644 --- a/Parasitemia/ParasitemiaUI/Program.fs +++ b/Parasitemia/ParasitemiaUI/Program.fs @@ -67,7 +67,7 @@ let main args = 0 else try - Log.User("Starting of Parasitemia UI ...") + Log.Info "Starting of Parasitemia UI ..." let result = match parseArgs args with @@ -82,10 +82,10 @@ let main args = Directory.CreateDirectory output |> ignore use logFile = new StreamWriter(new FileStream(Path.Combine(output, "log.txt"), FileMode.Append, FileAccess.Write)) - let listener = { new IListener with member this.NewEntry severity mess = logFile.WriteLine(mess) } + let listener = { new IListener with member this.NewEntry severity header mess = logFile.WriteLine(header + " : " + mess) } Log.AddListener(listener) - Log.User (sprintf "=== New run : %O %s ===" DateTime.Now (if debug then "[DEBUG]" else "[RELEASE]")) + Log.Info "=== New run : %O %s ===" DateTime.Now (if debug then "[DEBUG]" else "[RELEASE]") let files = match input with | File file -> [ file ] @@ -95,7 +95,7 @@ let main args = let images = [ for file in files -> Path.GetFileNameWithoutExtension(FileInfo(file).Name), config.Copy(), new Image(file) ] - Log.LogWithTime("Whole analyze", Severity.USER, (fun () -> + Log.LogWithTime Severity.INFO (fun () -> match ParasitemiaCore.Analysis.doMultipleAnalysis images None with | Some results -> for id, cells in results do @@ -105,7 +105,7 @@ let main args = fprintf resultFile "File: %s %d %d %.2f (diameter: %O)\n" id total infected (100. * (float infected) / (float total)) config.RBCRadius | None -> fprintf resultFile "Analysis aborted" - Some ())) |> ignore + Some ()) "Whole analyze" |> ignore Log.RmListener(listener) 0 @@ -114,10 +114,10 @@ let main args = if debug then config.Debug <- DebugOn "." GUI.run config fileToOpen - Log.User("Parasitemia UI closed") + Log.Info "Parasitemia UI closed" result with | ex -> - Log.Fatal("Error: {0}", ex) + Log.Fatal "Error: %A" ex 1 \ No newline at end of file diff --git a/Parasitemia/ParasitemiaUI/Utils.fs b/Parasitemia/ParasitemiaUI/Utils.fs index 5b3ba71..fa4af25 100644 --- a/Parasitemia/ParasitemiaUI/Utils.fs +++ b/Parasitemia/ParasitemiaUI/Utils.fs @@ -32,7 +32,7 @@ let private savePredefinedPPIToFile (predefinedPPI : PredefinedPPI list) = file.Write(JsonConvert.SerializeObject(predefinedPPI, JsonSerializerSettings(Formatting = Formatting.Indented))) with ex -> - Logger.Log.Error("Unable to save predefined PPI to file \"{0}\": {1}", predefinedPPIFilepath, ex) + Logger.Log.Error "Unable to save predefined PPI to file \"%s\": %O" predefinedPPIFilepath ex let private saveSensorSizesToFile (sensorSizes : SensorSize list) = try @@ -40,7 +40,7 @@ let private saveSensorSizesToFile (sensorSizes : SensorSize list) = file.Write(JsonConvert.SerializeObject(sensorSizes, JsonSerializerSettings(Formatting = Formatting.Indented))) with ex -> - Logger.Log.Error("Unable to save sensor sizes to file \"{0}\": {1}", sensorSizesFilepath, ex) + Logger.Log.Error "Unable to save sensor sizes to file \"%s\": %O" sensorSizesFilepath ex let predefinedPPI : PredefinedPPI list = try diff --git a/Parasitemia/ParasitemiaUI/packages.config b/Parasitemia/ParasitemiaUI/packages.config index 432eb3c..a2d59d7 100644 --- a/Parasitemia/ParasitemiaUI/packages.config +++ b/Parasitemia/ParasitemiaUI/packages.config @@ -1,9 +1,9 @@  - + - + \ No newline at end of file -- 2.43.0