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