// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.IO; using osu.Framework.Platform; using System.Linq; using System.Threading; using osu.Framework.Development; using osu.Framework.Statistics; using osu.Framework.Threading; namespace osu.Framework.Logging { /// /// This class allows statically (globally) configuring and using logging functionality. /// public class Logger { private static readonly object static_sync_lock = new object(); // separate locking object for flushing so that we don't lock too long on the staticSyncLock object, since we have to // hold this lock for the entire duration of the flush (waiting for I/O etc) before we can resume scheduling logs // but other operations like GetLogger(), ApplyFilters() etc. can still be executed while a flush is happening. private static readonly object flush_sync_lock = new object(); /// /// Whether logging is enabled. Setting this to false will disable all logging. /// public static bool Enabled = true; /// /// The minimum log-level a logged message needs to have to be logged. Default is . Please note that setting this to will log input events, including keypresses when entering a password. /// public static LogLevel Level = DebugUtils.IsDebugBuild ? LogLevel.Debug : LogLevel.Verbose; /// /// An identifier used in log file headers to figure where the log file came from. /// public static string UserIdentifier = Environment.UserName; /// /// An identifier for the game written to log file headers to indicate where the log file came from. /// public static string GameIdentifier = @"game"; /// /// An identifier for the version written to log file headers to indicate where the log file came from. /// public static string VersionIdentifier = @"unknown"; private static Storage storage; /// /// The storage to place logs inside. /// public static Storage Storage { private get => storage; set { storage = value ?? throw new ArgumentNullException(nameof(value)); // clear static loggers so they are correctly purged at the new storage location. static_loggers.Clear(); } } /// /// The target for which this logger logs information. This will only be null if the logger has a name. /// public LoggingTarget? Target { get; } /// /// The name of the logger. /// public string Name { get; } /// /// Gets the name of the file that this logger is logging to. /// public string Filename => $@"{Name}.log"; private readonly GlobalStatistic logCount; private static readonly HashSet reserved_names = new HashSet(Enum.GetNames(typeof(LoggingTarget)).Select(n => n.ToLower())); private Logger(LoggingTarget target = LoggingTarget.Runtime) : this(target.ToString(), false) { Target = target; } private Logger(string name, bool checkedReserved) { name = name.ToLower(); if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("The name of a logger must be non-null and may not contain only white space.", nameof(name)); if (checkedReserved && reserved_names.Contains(name)) throw new ArgumentException($"The name \"{name}\" is reserved. Please use the {nameof(LoggingTarget)}-value corresponding to the name instead."); Name = name; logCount = GlobalStatistics.Get(nameof(Logger), Name); } /// /// Add a plain-text phrase which should always be filtered from logs. The filtered phrase will be replaced with asterisks (*). /// Useful for avoiding logging of credentials. /// See also . /// public static void AddFilteredText(string text) { if (string.IsNullOrEmpty(text)) return; lock (static_sync_lock) filters.Add(text); } /// /// Removes phrases which should be filtered from logs. /// Useful for avoiding logging of credentials. /// See also . /// public static string ApplyFilters(string message) { lock (static_sync_lock) { foreach (string f in filters) message = message.Replace(f, string.Empty.PadRight(f.Length, '*')); } return message; } /// /// Logs the given exception with the given description to the specified logging target. /// /// The exception that should be logged. /// The description of the error that should be logged with the exception. /// The logging target (file). /// Whether the inner exceptions of the given exception should be logged recursively. public static void Error(Exception e, string description, LoggingTarget target = LoggingTarget.Runtime, bool recursive = false) { error(e, description, target, null, recursive); } /// /// Logs the given exception with the given description to the logger with the given name. /// /// The exception that should be logged. /// The description of the error that should be logged with the exception. /// The logger name (file). /// Whether the inner exceptions of the given exception should be logged recursively. public static void Error(Exception e, string description, string name, bool recursive = false) { error(e, description, null, name, recursive); } private static void error(Exception e, string description, LoggingTarget? target, string name, bool recursive) { log($@"{description}", target, name, LogLevel.Error, e); if (recursive && e.InnerException != null) error(e.InnerException, $"{description} (inner)", target, name, true); } /// /// Log an arbitrary string to the specified logging target. /// /// The message to log. Can include newline (\n) characters to split into multiple lines. /// The logging target (file). /// The verbosity level. /// Whether the message should be sent to listeners of and . True by default. public static void Log(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose, bool outputToListeners = true) { log(message, target, null, level, outputToListeners: outputToListeners); } /// /// Log an arbitrary string to the logger with the given name. /// /// The message to log. Can include newline (\n) characters to split into multiple lines. /// The logger name (file). /// The verbosity level. /// Whether the message should be sent to listeners of and . True by default. public static void Log(string message, string name, LogLevel level = LogLevel.Verbose, bool outputToListeners = true) { log(message, null, name, level, outputToListeners: outputToListeners); } private static void log(string message, LoggingTarget? target, string loggerName, LogLevel level, Exception exception = null, bool outputToListeners = true) { try { if (target.HasValue) GetLogger(target.Value).Add(message, level, exception, outputToListeners); else GetLogger(loggerName).Add(message, level, exception, outputToListeners); } catch { } } /// /// Logs a message to the specified logging target and also displays a print statement. /// /// The message to log. Can include newline (\n) characters to split into multiple lines. /// The logging target (file). /// The verbosity level. public static void LogPrint(string message, LoggingTarget target = LoggingTarget.Runtime, LogLevel level = LogLevel.Verbose) { if (Enabled && DebugUtils.IsDebugBuild) System.Diagnostics.Debug.Print(message); Log(message, target, level); } /// /// Logs a message to the logger with the given name and also displays a print statement. /// /// The message to log. Can include newline (\n) characters to split into multiple lines. /// The logger name (file). /// The verbosity level. public static void LogPrint(string message, string name, LogLevel level = LogLevel.Verbose) { if (Enabled && DebugUtils.IsDebugBuild) System.Diagnostics.Debug.Print(message); Log(message, name, level); } /// /// For classes that regularly log to the same target, this method may be preferred over the static Log method. /// /// The logging target. /// The logger responsible for the given logging target. public static Logger GetLogger(LoggingTarget target = LoggingTarget.Runtime) => GetLogger(target.ToString()); /// /// For classes that regularly log to the same target, this method may be preferred over the static Log method. /// /// The name of the custom logger. /// The logger responsible for the given logging target. public static Logger GetLogger(string name) { lock (static_sync_lock) { var nameLower = name.ToLower(); if (!static_loggers.TryGetValue(nameLower, out Logger l)) { static_loggers[nameLower] = l = Enum.TryParse(name, true, out LoggingTarget target) ? new Logger(target) : new Logger(name, true); l.clear(); } return l; } } /// /// Logs a new message with the and will only be logged if your project is built in the Debug configuration. Please note that the default setting for is so unless you increase the to messages printed with this method will not appear in the output. /// /// The message that should be logged. [Conditional("DEBUG")] public void Debug(string message = @"") { Add(message, LogLevel.Debug); } /// /// Log an arbitrary string to current log. /// /// The message to log. Can include newline (\n) characters to split into multiple lines. /// The verbosity level. /// An optional related exception. /// Whether the message should be sent to listeners of and . True by default. public void Add(string message = @"", LogLevel level = LogLevel.Verbose, Exception exception = null, bool outputToListeners = true) => add(message, level, exception, outputToListeners && OutputToListeners); private readonly RollingTime debugOutputRollingTime = new RollingTime(50, 10000); private void add(string message = @"", LogLevel level = LogLevel.Verbose, Exception exception = null, bool outputToListeners = true) { if (!Enabled || level < Level) return; ensureHeader(); logCount.Value++; message = ApplyFilters(message); string logOutput = message; if (exception != null) // add exception output to console / logfile output (but not the LogEntry's message). logOutput += $"\n{ApplyFilters(exception.ToString())}"; IEnumerable lines = logOutput .Replace(@"\r\n", @"\n") .Split('\n') .Select(s => $@"{DateTime.UtcNow.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture)} [{level.ToString().ToLower()}]: {s.Trim()}"); if (outputToListeners) { NewEntry?.Invoke(new LogEntry { Level = level, Target = Target, LoggerName = Name, Message = message, Exception = exception }); if (DebugUtils.IsDebugBuild) { static void consoleLog(string msg) { // fire to all debug listeners (like visual studio's output window) System.Diagnostics.Debug.Print(msg); // fire for console displays (appveyor/CI). Console.WriteLine(msg); } bool bypassRateLimit = level >= LogLevel.Verbose; foreach (var line in lines) { if (bypassRateLimit || debugOutputRollingTime.RequestEntry()) { consoleLog($"[{Name.ToLower()}] {line}"); if (!bypassRateLimit && debugOutputRollingTime.IsAtLimit) consoleLog($"Console output is being limited. Please check {Filename} for full logs."); } } } } if (Target == LoggingTarget.Information) // don't want to log this to a file return; lock (flush_sync_lock) { // we need to check if the logger is still enabled here, since we may have been waiting for a // flush and while the flush was happening, the logger might have been disabled. In that case // we want to make sure that we don't accidentally write anything to a file after that flush. if (!Enabled) return; scheduler.Add(delegate { try { using (var stream = Storage.GetStream(Filename, FileAccess.Write, FileMode.Append)) using (var writer = new StreamWriter(stream)) { foreach (var line in lines) writer.WriteLine(line); } } catch { } }); writer_idle.Reset(); } } /// /// Whether the output of this logger should be sent to listeners of and . /// Defaults to true. /// public bool OutputToListeners { get; set; } = true; /// /// Fires whenever any logger tries to log a new entry, but before the entry is actually written to the logfile. /// public static event Action NewEntry; /// /// Deletes log file from disk. /// private void clear() { lock (flush_sync_lock) { scheduler.Add(() => Storage.Delete(Filename)); writer_idle.Reset(); } } private bool headerAdded; private void ensureHeader() { if (headerAdded) return; headerAdded = true; add("----------------------------------------------------------", outputToListeners: false); add($"{Name} Log for {UserIdentifier} (LogLevel: {Level})", outputToListeners: false); add($"Running {GameIdentifier} {VersionIdentifier} on .NET {Environment.Version}", outputToListeners: false); add($"Environment: {RuntimeInfo.OS} ({Environment.OSVersion}), {Environment.ProcessorCount} cores ", outputToListeners: false); add("----------------------------------------------------------", outputToListeners: false); } private static readonly List filters = new List(); private static readonly Dictionary static_loggers = new Dictionary(); private static readonly Scheduler scheduler = new Scheduler(); private static readonly ManualResetEvent writer_idle = new ManualResetEvent(true); static Logger() { Timer timer = null; // timer has a very low overhead. timer = new Timer(_ => { if ((Storage != null ? scheduler.Update() : 0) == 0) writer_idle.Set(); // reschedule every 50ms. avoids overlapping callbacks. // ReSharper disable once AccessToModifiedClosure timer?.Change(50, Timeout.Infinite); }, null, 0, Timeout.Infinite); } /// /// Pause execution until all logger writes have completed and file handles have been closed. /// This will also unbind all handlers bound to . /// public static void Flush() { lock (flush_sync_lock) { writer_idle.WaitOne(2000); NewEntry = null; } } } /// /// Captures information about a logged message. /// public class LogEntry { /// /// The level for which the message was logged. /// public LogLevel Level; /// /// The target to which this message is being logged, or null if it is being logged to a custom named logger. /// public LoggingTarget? Target; /// /// The name of the logger to which this message is being logged, or null if it is being logged to a specific . /// public string LoggerName; /// /// The message that was logged. /// public string Message; /// /// An optional related exception. /// public Exception Exception; } /// /// The level on which a log-message is logged. /// public enum LogLevel { /// /// Log-level for debugging-related log-messages. This is the lowest level (highest verbosity). Please note that this will log input events, including keypresses when entering a password. /// Debug, /// /// Log-level for most log-messages. This is the second-lowest level (second-highest verbosity). /// Verbose, /// /// Log-level for important log-messages. This is the second-highest level (second-lowest verbosity). /// Important, /// /// Log-level for error messages. This is the highest level (lowest verbosity). /// Error } /// /// The target for logging. Different targets can have different logfiles, are displayed differently in the LogOverlay and are generally useful for organizing logs into groups. /// public enum LoggingTarget { /// /// Logging target for general information. Everything logged with this target will not be written to a logfile. /// Information, /// /// Logging target for information about the runtime. /// Runtime, /// /// Logging target for network-related events. /// Network, /// /// Logging target for performance-related information. /// Performance, /// /// Logging target for database-related events. /// Database } }