Upgrade the logger component
[master-thesis.git] / Parasitemia / Logger / Logger.fs
index 7d54843..b964517 100644 (file)
 namespace Logger
 
 open System
-open System.Text
-open System.IO
-open System.IO.Compression
 open System.Diagnostics
+open System.IO
 open System.Threading
-open System.Collections.Generic
-
-type Severity = DEBUG = 1 | INFO = 2 | WARNING = 3 | ERROR = 4 | FATAL = 5
-
-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<unit>
+open Logger
+open Logger.Types
 
 [<Sealed>]
 type Log () =
+    static let mutable writer : IWriter = new ConsoleWriter () :> IWriter
+    static let monitor = obj ()
+    static let listeners = Listeners ()
 
-    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 [<Literal>] MAX_SIZE_FILE = 52428800L // [byte] (50 MB).
-    let [<Literal>] 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 [<Literal>] COMPRESS_ARCHIVED_FILES = true
-    let [<Literal>] FILENAME_FORMAT = "{0:D4}.log"
-    let [<Literal>] 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 monitor = Object ()
-
-    let listeners = List<IListener> ()
-
-    let debug =
-#if DEBUG
-        true
-#else
-        false
-#endif
-
-    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})",
-                DateTime.Now,
-                string msg.Severity,
-                msg.ModuleCaller,
-                (if String.IsNullOrEmpty msg.ThreadName then string msg.ThreadId else sprintf "%s-%i" msg.ThreadName msg.ThreadId)
+    /// <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)
             )
 
-        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<Command> (
-            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
+    /// <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
         )
 
-    do
-        writeAgent.Start ()
+    /// <summary>
+    /// Return all log files (the current one and the archived) as full paths.
+    /// </summary>
+    static member LogFiles = writer.LogFiles
 
-    let setLogDirectory (dir : string) =
-        lock monitor (
-            fun () ->
-                logDir <- dir
-
-                if not <| isNull stream then
-                    stream.Close ()
-                    stream <- null
-
-                try
-                    if not <| Directory.Exists logDir
-                    then
-                        Directory.CreateDirectory logDir |> ignore
-                with
-                | _ -> Console.Error.WriteLine ("Unable to create the log directory: {0}", logDir)
-       )
-
-    interface IDisposable with
-        member this.Dispose () =
-            if not (isNull stream)
-            then
-                stream.Dispose ()
-            (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
-
-    /// <summary>
-    /// Will stop and wait a reply. Used to flush the remaining messages.
-    /// </summary>
-    member private this.Stop () =
-        writeAgent.PostAndReply (
-            fun replyChannel ->
-                Stop replyChannel
-        )
+    /// <summary>
+    /// Wait that all the previous messages are written.
+    /// </summary>
+    static member Flush () = writer.Flush ()
 
-    member this.LogDirectory
-        with get () = logDir
-        and set value = setLogDirectory value
+    static member DebugLoggingEnabled
+        with get () = writer.DebugLoggingEnabled
+        and set value = writer.DebugLoggingEnabled <- value
 
-    static member SetLogDirectory (dir : string) =
-        instance.LogDirectory <- dir
+    /// <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
 
-    member this.AddListener (listener : IListener) =
-        lock monitor (
-            fun () ->
-                if not <| listeners.Contains listener
-                then
-                    listeners.Add listener
-        )
+    /// <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
 
-    member this.RmListener (listener : IListener) =
-        lock monitor (fun () -> listeners.Remove listener |> ignore)
+    static member ClearLogFilesOlderThan (timeOld : TimeSpan) =
+        writer.ClearLogFilesOlderThan timeOld
 
-    static member AddListener (listener : IListener) = instance.AddListener listener
-    static member RmListener (listener : IListener) = instance.RmListener listener
+    /// <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 ()
-        Printf.kprintf (fun s -> instance.Write (s + sprintf " (time: %d ms)" sw.ElapsedMilliseconds) severity; res) format
+        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 DEBUG
-        Printf.kprintf (fun s -> instance.Write s Severity.DEBUG) format
-#else
-        Printf.kprintf (fun _ -> ()) format // TODO: can it be simplify?
-#endif
+        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 -> instance.Write s Severity.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 -> instance.Write s Severity.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 -> instance.Write s Severity.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 -> instance.Write s Severity.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, [| |])
 
-    static member Shutdown () =
-        instance.Stop ()
+    /// <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