Upgrade the logger component
[master-thesis.git] / Parasitemia / Logger / FileWriter.fs
diff --git a/Parasitemia/Logger/FileWriter.fs b/Parasitemia/Logger/FileWriter.fs
new file mode 100644 (file)
index 0000000..4183237
--- /dev/null
@@ -0,0 +1,187 @@
+namespace Logger
+
+open System
+open System.Collections.Generic
+open System.IO
+open System.Reflection
+open System.Threading
+
+open Logger.Constants
+open Logger.Types
+
+type internal FileWriter (logDir : string) =
+    let mutable maxSizeFile = DEFAULT_MAX_SIZE_FILE
+    let mutable stream : StreamWriter = null
+    let mutable filename : string = null
+
+    let mutable avoidRepeatingIdenticalMessages : bool = false
+    let lastMessageHashes = Queue<int * DateTime> ()
+
+    do
+        try
+            if not <| Directory.Exists logDir then
+                Directory.CreateDirectory logDir |> ignore
+        with
+        | _ -> Console.Error.WriteLine ("Unable to create the log directory: {0}", logDir)
+
+    // Open a file to log into. Each file has a number ('entryNumber').
+    let openLogFile (entryNumber : int64) =
+        if not (isNull logDir) then
+            try
+                if isNull stream || (entryNumber % (int64 NB_ENTRIES_CHECK_SIZE) = 0L) && stream.BaseStream.Length > maxSizeFile then
+                    if not (isNull stream) then
+                        stream.Close ()
+                        if COMPRESS_ARCHIVED_FILES then
+                            Utils.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 Utils.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 > maxSizeFile then
+                            filename <- Path.Combine (logDir, String.Format (FILENAME_FORMAT, n + 1))
+                    with
+                    | :? FileNotFoundException -> () // The file may not exist.
+
+                    let fileStream = File.Open (filename, FileMode.Append, FileAccess.Write, FileShare.Read)
+                    stream <- new StreamWriter (fileStream, encoding)
+            with
+            | ex -> Console.Error.WriteLine ("Can't open the file log: {0}", ex)
+
+    let write (msg : Message) (entryNumber : int64) =
+        openLogFile entryNumber
+
+        let header = Utils.formatHeader msg
+
+        if not <| isNull stream then
+            try
+                let writeAndFlush (header : string) (message : string) =
+                    stream.WriteLine (Utils.formatMessage header message)
+                    stream.Flush ()
+
+                if avoidRepeatingIdenticalMessages && msg.Message.Length > Constants.MIN_MESSAGE_LENGTH_TO_NOT_BE_REPEATED then
+                    let newMessageHash = hash msg.Message
+                    match lastMessageHashes |> Seq.tryPick (fun (hash, dateTime) -> if hash = newMessageHash then Some dateTime else None) with
+                    | Some dateTime ->
+                        writeAndFlush header (String.Format ("<Same as {0:yyyy-MM-dd HH:mm:ss.fff}>", dateTime))
+                    | None ->
+                        lastMessageHashes.Enqueue (hash msg.Message, msg.DateTime)
+                        if lastMessageHashes.Count > Constants.MAX_NB_OF_MESSAGE_HASHES_WHEN_AVOIDING_REPEATING_IDENTICAL_MESSAGES then
+                            lastMessageHashes.Dequeue () |> ignore
+                        writeAndFlush header msg.Message
+                else
+                    writeAndFlush header msg.Message
+            with
+            | :? IOException as ex -> Console.Error.WriteLine ("Unable to write to the log file: {0}", ex)
+
+    let closeCurrentStream () =
+        if not <| isNull stream then
+            stream.Dispose ()
+            stream <- null
+
+    let logFiles () =
+        Directory.EnumerateFiles logDir
+        |> Seq.filter (fun file -> FILENAME_PATTERN.IsMatch file || FILENAME_PATTERN_COMPRESSED.IsMatch file)
+        |> Seq.sort
+
+    let clearLogFiles (timeOld : TimeSpan) =
+        closeCurrentStream ()
+
+        for file in logFiles () do
+            let fileInfo = FileInfo file
+            if fileInfo.LastWriteTime.Add timeOld <= DateTime.Now then
+                // Special case for 0001.log to keep the file opened in text editor (developer convenience)
+                if fileInfo.Name = String.Format (FILENAME_FORMAT, 1) then
+                    File.WriteAllText (file, "")
+                else
+                    fileInfo.Delete ()
+
+    let logInternalErrorMessage (message : string) (entryNumber : int64) =
+        write
+            {
+                Message = sprintf "Logger internal error: %s" message
+                ThreadName = Thread.CurrentThread.Name
+                ThreadId = Thread.CurrentThread.ManagedThreadId
+                ModuleCaller = Assembly.GetExecutingAssembly().GetName().Name
+                Severity = Severity.ERROR
+                DateTime = TimeZone.CurrentTimeZone.ToLocalTime DateTime.UtcNow
+            }
+            entryNumber
+
+    let mailbox =
+        new MailboxProcessor<Command> (
+            fun inbox ->
+                let rec loop (nbEntries : int64) =
+                    async {
+                        try
+                            let! command = inbox.Receive ()
+                            match command with
+                            | Write message ->
+                                write message nbEntries
+                                return! loop (nbEntries + 1L)
+
+                            | Flush replyChannel ->
+                                replyChannel.Reply ()
+                                return! loop nbEntries
+
+                            | Shutdown replyChannel ->
+                                closeCurrentStream ()
+                                replyChannel.Reply ()
+
+                            | ClearLogFilesOlderThan timeOld ->
+                                clearLogFiles timeOld
+                                return! loop 1L
+
+                            | SetAvoidRepeatingIdenticalMessage enabled ->
+                                avoidRepeatingIdenticalMessages <- enabled
+                                if not avoidRepeatingIdenticalMessages then
+                                    lastMessageHashes.Clear ()
+                                return! loop nbEntries
+                        with
+                        | ex ->
+                            logInternalErrorMessage (string ex) nbEntries
+                            return! loop (nbEntries + 1L)
+                    }
+                loop 1L
+        )
+
+    do
+        mailbox.Start ()
+
+    interface IWriter with
+        member val DebugLoggingEnabled = false with get, set
+
+        member this.AvoidRepeatingIdenticalMessages
+            with get () = avoidRepeatingIdenticalMessages
+            and set value = mailbox.Post (SetAvoidRepeatingIdenticalMessage value)
+
+        member this.LogDir = logDir
+
+        member this.MaxSizeFile
+            with get () = maxSizeFile
+            and set value = maxSizeFile <- value
+
+        member this.LogFiles = logFiles ()
+
+        member this.Write (message : Message) =
+            mailbox.Post (Write message)
+
+        member this.Flush () =
+            mailbox.PostAndReply (fun replyChannel -> Flush replyChannel)
+
+        member this.ClearLogFilesOlderThan (timeOld : TimeSpan) =
+            mailbox.Post (ClearLogFilesOlderThan timeOld)
+
+    interface IDisposable with
+        member this.Dispose () =
+            mailbox.PostAndReply (
+                fun replyChannel ->
+                    Shutdown replyChannel
+            )
+            (mailbox :> IDisposable).Dispose()
\ No newline at end of file