using System; using System.IO; using System.Text; using SabreTools.Core; using SabreTools.IO; namespace SabreTools.Logging { /// /// Internal logging implementation /// public static class LoggerImpl { #region Fields /// /// Optional output filename for logs /// public static string Filename { get; set; } = null; /// /// Determines if we're logging to file or not /// public static bool LogToFile { get { return !string.IsNullOrWhiteSpace(Filename); } } /// /// Optional output log directory /// /// TODO: Make this either passed in or optional public static string LogDirectory { get; set; } = Path.Combine(Globals.ExeDir, "logs") + Path.DirectorySeparatorChar; /// /// Determines the lowest log level to output /// public static LogLevel LowestLogLevel { get; set; } = LogLevel.VERBOSE; /// /// Determines whether to prefix log lines with level and datetime /// public static bool AppendPrefix { get; set; } = true; /// /// Determines whether to throw if an exception is logged /// public static bool ThrowOnError { get; set; } = false; /// /// Logging start time for metrics /// public static DateTime StartTime { get; private set; } /// /// Determines if there were errors logged /// public static bool LoggedErrors { get; private set; } = false; /// /// Determines if there were warnings logged /// public static bool LoggedWarnings { get; private set; } = false; #endregion #region Private variables /// /// StreamWriter representing the output log file /// private static StreamWriter _log; /// /// Object lock for multithreaded logging /// private static readonly object _lock = new object(); #endregion #region Control /// /// Generate and set the log filename /// /// Base filename to use /// True to append a date to the filename, false otherwise public static void SetFilename(string filename, bool addDate = true) { // Set and create the output if (addDate) Filename = $"{Path.GetFileNameWithoutExtension(filename)} ({DateTime.Now:yyyy-MM-dd HH-mm-ss}).{PathExtensions.GetNormalizedExtension(filename)}"; else Filename = filename; } /// /// Start logging by opening output file (if necessary) /// /// True if the logging was started correctly, false otherwise public static bool Start() { // Setup the logging handler to always use the internal log LogEventHandler += HandleLogEvent; // Start the logging StartTime = DateTime.Now; if (!LogToFile) return true; // Setup file output and perform initial log try { if (!string.IsNullOrEmpty(LogDirectory) && !Directory.Exists(LogDirectory)) Directory.CreateDirectory(LogDirectory); FileStream logfile = File.Create(Path.Combine(LogDirectory, Filename)); _log = new StreamWriter(logfile, Encoding.UTF8, (int)(4 * Constants.KibiByte), true) { AutoFlush = true }; _log.WriteLine($"Logging started {StartTime:yyyy-MM-dd HH:mm:ss}"); _log.WriteLine($"Command run: {Globals.CommandLineArgs}"); } catch { return false; } return true; } /// /// End logging by closing output file (if necessary) /// /// True if all ending output is to be suppressed, false otherwise (default) /// True if the logging was ended correctly, false otherwise public static bool Close(bool suppress = false) { if (!suppress) { if (LoggedWarnings) Console.WriteLine("There were warnings in the last run! Check the log for more details"); if (LoggedErrors) Console.WriteLine("There were errors in the last run! Check the log for more details"); TimeSpan span = DateTime.Now.Subtract(StartTime); // Special case for multi-day runs string total; if (span >= TimeSpan.FromDays(1)) total = span.ToString(@"d\:hh\:mm\:ss"); else total = span.ToString(@"hh\:mm\:ss"); if (!LogToFile) { Console.WriteLine($"Total runtime: {total}"); return true; } try { _log.WriteLine($"Logging ended {DateTime.Now:yyyy-MM-dd HH:mm:ss}"); _log.WriteLine($"Total runtime: {total}"); Console.WriteLine($"Total runtime: {total}"); _log.Close(); } catch { return false; } } else { try { _log.Close(); } catch { return false; } } return true; } #endregion #region Event Handling /// /// Handler for log events /// public static event LogEventHandler LogEventHandler = delegate { }; /// /// Default log event handling /// public static void HandleLogEvent(object sender, LogEventArgs args) { // Null args means we can't handle it if (args == null) return; // If we have an exception and we're throwing on that if (ThrowOnError && args.Exception != null) throw args.Exception; // If we have a warning or error, set the flags accordingly if (args.LogLevel == LogLevel.WARNING) LoggedWarnings = true; if (args.LogLevel == LogLevel.ERROR) LoggedErrors = true; // Setup the statement based on the inputs string logLine; if (args.Exception != null) { logLine = $"{(args.Statement != null ? args.Statement + ": " : string.Empty)}{args.Exception}"; } else if (args.TotalCount != null && args.CurrentCount != null) { double percentage = ((double)args.CurrentCount.Value / args.TotalCount.Value) * 100; logLine = $"{percentage:N2}%{(args.Statement != null ? ": " + args.Statement : string.Empty)}"; } else { logLine = args.Statement ?? string.Empty; } // Then write to the log Log(logLine, args.LogLevel); } /// /// Write the given string to the log output /// /// String to be written log /// Severity of the information being logged private static void Log(string output, LogLevel loglevel) { // If the log level is less than the filter level, we skip it but claim we didn't if (loglevel < LowestLogLevel) return; // USER and ERROR writes to console if (loglevel == LogLevel.USER || loglevel == LogLevel.ERROR) Console.WriteLine((loglevel == LogLevel.ERROR && AppendPrefix ? loglevel.ToString() + " " : string.Empty) + output); // If we're writing to file, use the existing stream if (LogToFile) { try { lock (_lock) { _log.WriteLine((AppendPrefix ? $"{loglevel} - {DateTime.Now} - " : string.Empty) + output); } } catch (Exception ex) { Console.WriteLine(ex); Console.WriteLine("Could not write to log file!"); if (ThrowOnError) throw ex; return; } } return; } #endregion #region Log Event Triggers /// /// Write the given exception as a verbose message to the log output /// /// Instance object that's the source of logging /// Exception to be written log /// String to be written log /// True if the output could be written, false otherwise public static void Verbose(object instance, Exception ex, string output = null) { LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output, ex)); } /// /// Write the given string as a verbose message to the log output /// /// Instance object that's the source of logging /// String to be written log /// True if the output could be written, false otherwise public static void Verbose(object instance, string output) { LogEventHandler(instance, new LogEventArgs(LogLevel.VERBOSE, output, null)); } /// /// Write the given verbose progress message to the log output /// /// Instance object that's the source of logging /// Total count for progress /// Current count for progres /// String to be written log public static void Verbose(object instance, long total, long current, string output = null) { LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.VERBOSE, output)); } /// /// Write the given exception as a user message to the log output /// /// Instance object that's the source of logging /// Exception to be written log /// String to be written log /// True if the output could be written, false otherwise public static void User(object instance, Exception ex, string output = null) { LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output, ex)); } /// /// Write the given string as a user message to the log output /// /// Instance object that's the source of logging /// String to be written log /// True if the output could be written, false otherwise public static void User(object instance, string output) { LogEventHandler(instance, new LogEventArgs(LogLevel.USER, output, null)); } /// /// Write the given user progress message to the log output /// /// Instance object that's the source of logging /// Total count for progress /// Current count for progres /// String to be written log public static void User(object instance, long total, long current, string output = null) { LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.USER, output)); } /// /// Write the given exception as a warning to the log output /// /// Instance object that's the source of logging /// Exception to be written log /// String to be written log /// True if the output could be written, false otherwise public static void Warning(object instance, Exception ex, string output = null) { LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output, ex)); } /// /// Write the given string as a warning to the log output /// /// Instance object that's the source of logging /// String to be written log /// True if the output could be written, false otherwise public static void Warning(object instance, string output) { LogEventHandler(instance, new LogEventArgs(LogLevel.WARNING, output, null)); } /// /// Write the given warning progress message to the log output /// /// Instance object that's the source of logging /// Total count for progress /// Current count for progres /// String to be written log public static void Warning(object instance, long total, long current, string output = null) { LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.WARNING, output)); } /// /// Writes the given exception as an error in the log /// /// Instance object that's the source of logging /// Exception to be written log /// String to be written log /// True if the output could be written, false otherwise public static void Error(object instance, Exception ex, string output = null) { LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output, ex)); } /// /// Writes the given string as an error in the log /// /// Instance object that's the source of logging /// String to be written log /// True if the output could be written, false otherwise public static void Error(object instance, string output) { LogEventHandler(instance, new LogEventArgs(LogLevel.ERROR, output, null)); } /// /// Write the given error progress message to the log output /// /// Instance object that's the source of logging /// Total count for progress /// Current count for progres /// String to be written log public static void Error(object instance, long total, long current, string output = null) { LogEventHandler(instance, new LogEventArgs(total, current, LogLevel.ERROR, output)); } #endregion } }