Upgrade the logger component
[master-thesis.git] / Parasitemia / Logger / Logger.fs
index 1f029d5..b964517 100644 (file)
 namespace Logger
 
 open System
-open System.Text
-open System.IO
 open System.Diagnostics
+open System.IO
 open System.Threading
-open System.Collections.Generic
-
-type Severity = DEBUG = 1 | USER = 2 | WARNING = 3 | ERROR = 4 | FATAL = 5
 
-type IListener = abstract NewEntry : Severity -> string -> unit
+open Logger
+open Logger.Types
 
 [<Sealed>]
 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 encoding = Encoding.GetEncoding("UTF-8")
-
-    let moduleName = System.Diagnostics.StackFrame(1).GetMethod().Module.Name
-
-    let mutable stream: StreamWriter = null
-
-    let mutable logDir: string = null
-    let mutable absoluteDir: string = null
-
-    let mutable nbEntries = 0L
-
-    let monitor = Object()
-
-    let listeners = List<IListener>()
-
-    let debug =
-#if DEBUG
-        true
-#else
-        false
-#endif
-
-    static let instance = new Log()
-
-    let setLogDirectory (dir: string) =
-        lock monitor (fun () ->
-            logDir <- dir
-            absoluteDir <- Path.Combine(System.Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), logDir)
-
-            if stream <> null
-            then
-                stream.Close()
-                stream <- null
-
-            try
-                if not <| Directory.Exists(absoluteDir)
-                then
-                    Directory.CreateDirectory(absoluteDir) |> 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
-
-    interface IDisposable with
-        member this.Dispose () =
-            if stream <> null
-            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) =
-        lock monitor (fun () ->
-            if not <| listeners.Contains(listener)
-            then
-                listeners.Add(listener))
-
-    member private 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, [<ParamArray>] args: Object[]) : 'a option =
-        let sw = Stopwatch()
-        sw.Start()
+    static let mutable writer : IWriter = new ConsoleWriter () :> IWriter
+    static let monitor = obj ()
+    static let listeners = Listeners ()
+
+    /// <summary>
+    /// Must be called first before any other action.
+    /// </summary>
+    static member LogDirectory
+        with get () = lock monitor (fun () -> writer.LogDir)
+        and set value =
+            lock monitor (
+                fun () ->
+                    Log.Close ()
+                    if String.IsNullOrWhiteSpace value then
+                        writer <- new ConsoleWriter ()
+                    else
+                        writer <- new FileWriter (value)
+            )
+
+    /// <summary>
+    /// Close the log. 'LogDirectory' must be set again to reopen it.
+    /// </summary>
+    static member Close () =
+        lock monitor (
+             fun () ->
+                writer.Flush ()
+                (writer :> IDisposable).Dispose ()
+                writer <- new ConsoleWriter () :> IWriter
+        )
+
+    /// <summary>
+    /// Return all log files (the current one and the archived) as full paths.
+    /// </summary>
+    static member LogFiles = writer.LogFiles
+
+    /// <summary>
+    /// Wait that all the previous messages are written.
+    /// </summary>
+    static member Flush () = writer.Flush ()
+
+    static member DebugLoggingEnabled
+        with get () = writer.DebugLoggingEnabled
+        and set value = writer.DebugLoggingEnabled <- value
+
+    /// <summary>
+    /// Avoid to repeat a message by writting a reference to a previous message instead of the message.
+    /// 'false' by default.
+    /// </summary>
+    static member AvoidRepeatingIdenticalMessages
+        with get () = writer.AvoidRepeatingIdenticalMessages
+        and set value = writer.AvoidRepeatingIdenticalMessages <- value
+
+    /// <summary>
+    /// The maximum size of the current file log. If the file exceed this value it will be zipped and a new file will be created.
+    /// The file size is only tested each time a certain number of messages have been written so the file may exceed this value a bit.
+    /// </summary>
+    /// <param name="size"></param>
+    static member SetLogFilesMaxSize (size : int64) =
+        writer.MaxSizeFile <- size
+
+    static member ClearLogFilesOlderThan (timeOld : TimeSpan) =
+        writer.ClearLogFilesOlderThan timeOld
+
+    /// <summary>
+    /// Remove all archived log files and empty the current one.
+    /// </summary>
+    static member ClearLogFiles () =
+        Log.ClearLogFilesOlderThan (TimeSpan 0L)
+
+    /// <summary>
+    /// Total size in bytes.
+    /// </summary>
+    static member CurrentLogSize () : int64 =
+        Log.LogFiles
+        |> Seq.map (fun file -> try (FileInfo file).Length with | _ex -> 0L)
+        |> Seq.sum
+
+    static member AddListener (listener : IListener) =
+        listeners.Add listener
+
+    static member RemoveListener (listener : IListener) =
+        listeners.Remove listener
+
+    static member private Write (message : string) (severity : Severity) =
+        let msg =
+            {
+                Message = message
+                ThreadName = Thread.CurrentThread.Name
+                ThreadId = Thread.CurrentThread.ManagedThreadId
+                ModuleCaller = Utils.callerModuleName ()
+                Severity = severity
+                DateTime = TimeZone.CurrentTimeZone.ToLocalTime DateTime.UtcNow
+            }
+        listeners.NewEntry msg
+        writer.Write msg
+
+    /// <summary>
+    /// [F#] Execute the given function and measure its time.
+    /// </summary>
+    /// <param name="severity">Severity for writing to log</param>
+    /// <param name="f">Function to test</param>
+    /// <param name="format">Format string for output</param>
+    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
-
-    static member Debug (message: string, [<ParamArray>] args: Object[]) =
-#if DEBUG
-        instance.Write(String.Format(message, args), Severity.DEBUG)
-#else
-        ()
-#endif
-
-    static member User (message: string, [<ParamArray>] args: Object[]) =
-        instance.Write(String.Format(message, args), Severity.USER)
-
-    static member Warning (message: string, [<ParamArray>] args: Object[]) =
-        instance.Write(String.Format(message, args), Severity.WARNING)
-
-    static member Error (message: string, [<ParamArray>] args: Object[]) =
-        instance.Write(String.Format(message, args), Severity.ERROR)
-
-    static member Fatal (message: string, [<ParamArray>] args: Object[]) =
-        instance.Write(String.Format(message, args), Severity.FATAL)
-
+        sw.Stop ()
+        Printf.kprintf (fun s -> Log.Write (s + sprintf " (time: %d ms)" sw.ElapsedMilliseconds) severity; res) format
+
+    /// <summary>
+    /// [F#] Write Debug message to log (if DebugLoggingEnabled = true)
+    /// </summary>
+    static member Debug format =
+        if writer.DebugLoggingEnabled then
+            Printf.kprintf (fun s -> Log.Write s Severity.DEBUG) format
+        else
+            // [BGR] FIXME: is it possible to simplify a bit here? It's more CPU consuming than the C# couterpart.
+            Printf.kprintf (fun _ -> ()) format
+
+    /// <summary>
+    /// [F#] Write Info message to log
+    /// </summary>
+    static member Info format =
+        Printf.kprintf (fun s -> Log.Write s Severity.INFO) format
+
+    /// <summary>
+    /// [F#] Write Warning message to log
+    /// </summary>
+    static member Warning format =
+        Printf.kprintf (fun s -> Log.Write s Severity.WARNING) format
+
+    /// <summary>
+    /// [F#] Write Error message to log
+    /// </summary>
+    static member Error format =
+        Printf.kprintf (fun s -> Log.Write s Severity.ERROR) format
+
+    /// <summary>
+    /// [F#] Write Fatal message to log
+    /// </summary>
+    static member Fatal format =
+        Printf.kprintf (fun s -> Log.Write s Severity.FATAL) format
+
+    /// <summary>
+    /// Write DEBUG message to log (if DebugLoggingEnabled = true)
+    /// </summary>
+    static member DEBUG (message : string, [<ParamArray>] args : obj array) =
+        if writer.DebugLoggingEnabled then
+            if isNull args || args.Length = 0 then
+                Log.Write message Severity.DEBUG
+            else
+                Log.Write (String.Format (message, args)) Severity.DEBUG
+
+    /// <summary>
+    /// Write DEBUG message to log (if DebugLoggingEnabled = true)
+    /// </summary>
+    static member DEBUG (message : string) = Log.DEBUG (message, [| |])
+
+    /// <summary>
+    /// Write INFO message to log
+    /// </summary>
+    static member  INFO (message : string, [<ParamArray>] args : obj array) =
+        if isNull args || args.Length = 0 then
+            Log.Write message Severity.INFO
+        else
+            Log.Write (String.Format (message, args)) Severity.INFO
+
+    /// <summary>
+    /// Write INFO message to log
+    /// </summary>
+    static member INFO (message : string) = Log.INFO (message, [| |])
+
+    /// <summary>
+    /// Write WARNING message to log
+    /// </summary>
+    static member WARNING (message : string, [<ParamArray>] args : obj array) =
+        if isNull args || args.Length = 0 then
+            Log.Write message Severity.WARNING
+        else
+            Log.Write (String.Format (message, args)) Severity.WARNING
+
+    /// <summary>
+    /// Write WARNING message to log
+    /// </summary>
+    static member WARNING (message : string) = Log.WARNING (message, [| |])
+
+    /// <summary>
+    /// Write ERROR message to log
+    /// </summary>
+    static member ERROR (message : string, [<ParamArray>] args : obj array) =
+        if isNull args || args.Length = 0 then
+            Log.Write message Severity.ERROR
+        else
+            Log.Write (String.Format (message, args)) Severity.ERROR
+
+    /// <summary>
+    /// Write ERROR message to log
+    /// </summary>
+    static member ERROR (message : string) = Log.ERROR (message, [| |])
+
+    /// <summary>
+    /// Write FATAL message to log
+    /// </summary>
+    static member FATAL (message : string, [<ParamArray>] args : obj array) =
+        if isNull args || args.Length = 0 then
+            Log.Write message Severity.FATAL
+        else
+            Log.Write (String.Format (message, args)) Severity.FATAL
+
+    /// <summary>
+    /// Write FATAL message to log
+    /// </summary>
+    static member FATAL (message : string) = Log.FATAL (message, [| |])
\ No newline at end of file