// Copyright (C) 2024, The Duplicati Team // https://duplicati.com, hello@duplicati.com // // Permission is hereby granted, free of charge, to any person obtaining a // copy of this software and associated documentation files (the "Software"), // to deal in the Software without restriction, including without limitation // the rights to use, copy, modify, merge, publish, distribute, sublicense, // and/or sell copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER // DEALINGS IN THE SOFTWARE. using System; using System.Collections.Generic; using System.Text; namespace Duplicati.Library.Logging { /// /// The different types of messages /// public enum LogMessageType { /// /// The message should only be shown if it is explicitly requested /// ExplicitOnly, /// /// The message is a profiling message /// Profiling, /// /// Messages that are normally not wanted for display /// Verbose, /// /// The message is from a retry /// Retry, /// /// The message is informative but does not indicate problems /// Information, /// /// The message is from dry-run output /// DryRun, /// /// The message is a warning, meaning that later errors may be related to this message /// Warning, /// /// The message indicates an error /// Error, } /// /// This static class is used to write log messages /// public static class Log { /// /// The key used to assign the current scope into the current call-context /// private const string LOGICAL_CONTEXT_KEY = "Duplicati:LoggingEntry"; /// /// The root scope /// private static readonly LogScope m_root = new LogScope(null, new LogTagFilter(LogMessageType.Error, null, null), null, true); /// /// The stored log instances /// private static readonly Dictionary m_log_instances = new Dictionary(); /// /// Static lock object to provide thread safe logging /// private static readonly object m_lock = new object(); /// /// Gets the lock instance used to protect the logging calls /// public static object Lock { get { return m_lock; } } /// /// Gets a log tag that reflects the type /// /// The log-tag for the type. /// The type to get the tag for. public static string LogTagFromType() { return LogTagFromType(typeof(T)); } /// /// Gets a log tag that reflects the type /// /// The log-tag for the type. /// The type to get the tag for. public static string LogTagFromType(Type t) { return t.Namespace + "." + t.Name; } /// /// Gets a common list of items to include in the help-url /// /// The basic help url items. /// A flag indicating if the link is for commandline private static List GetBasicHelpUrlItems(bool fromCommandLine) { var items = new List(); items.Add("version=" + System.Uri.EscapeDataString(typeof(Log).Assembly.GetName().Version.ToString())); items.Add("cli=" + (fromCommandLine ? "t" : "f")); // TODO: Add OS type? mono version? install id? app-name? return items; } /// /// Encodes a list of query string values and appends them to the help url /// /// The help URL. /// The items to include. private static string EncodeHelpUrl(IEnumerable items) { var query = string.Join("&", items); if (!string.IsNullOrWhiteSpace(query)) query = "?" + query; return "https://help.duplicati.com/" + query; } /// /// Creates a link to look up further information on a particular message /// /// The help link. /// The id to use. /// A flag indicating if the link is for commandline public static string CreateHelpLink(string id, bool fromCommandLine) { var items = GetBasicHelpUrlItems(fromCommandLine); if (!string.IsNullOrWhiteSpace(id)) items.Add("id=" + System.Uri.EscapeDataString(id)); return EncodeHelpUrl(items); } /// /// Creates a link to look up further information on a particular message /// /// The help link. /// The log tag, if any. /// The message id, if any. /// The exception id, if any. /// A flag indicating if the link is for commandline public static string CreateHelpLink(string tag, string messageid, string exceptionid, bool fromCommandLine) { var items = GetBasicHelpUrlItems(fromCommandLine); if (!string.IsNullOrWhiteSpace(tag)) items.Add("tag=" + System.Uri.EscapeDataString(tag)); if (!string.IsNullOrWhiteSpace(messageid)) items.Add("msgid=" + System.Uri.EscapeDataString(messageid)); if (!string.IsNullOrWhiteSpace(exceptionid)) items.Add("exid=" + System.Uri.EscapeDataString(exceptionid)); return EncodeHelpUrl(items); } /// /// Writes an explicit message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The message format arguments public static void WriteExplicitMessage(string tag, string id, string message, params object[] arguments) { WriteMessage(LogMessageType.ExplicitOnly, tag, id, null, message, arguments); } /// /// Writes an explicit message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The exception to log /// The message format arguments public static void WriteExplicitMessage(string tag, string id, Exception ex, string message, params object[] arguments) { WriteMessage(LogMessageType.ExplicitOnly, tag, id, ex, message, arguments); } /// /// Writes a verbose message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The message format arguments public static void WriteVerboseMessage(string tag, string id, string message, params object[] arguments) { WriteMessage(LogMessageType.Verbose, tag, id, null, message, arguments); } /// /// Writes a verbose message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The exception to log /// The message format arguments public static void WriteVerboseMessage(string tag, string id, Exception ex, string message, params object[] arguments) { WriteMessage(LogMessageType.Verbose, tag, id, ex, message, arguments); } /// /// Writes a profiling message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The message format arguments public static void WriteProfilingMessage(string tag, string id, string message, params object[] arguments) { WriteMessage(LogMessageType.Profiling, tag, id, null, message, arguments); } /// /// Writes a dry-run message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The message format arguments public static void WriteDryrunMessage(string tag, string id, string message, params object[] arguments) { WriteMessage(LogMessageType.DryRun, tag, id, null, message, arguments); } /// /// Writes a retry message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The exception to attach /// The message format arguments public static void WriteRetryMessage(string tag, string id, Exception ex, string message, params object[] arguments) { WriteMessage(LogMessageType.Retry, tag, id, ex, message, arguments); } /// /// Writes an information message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The message format arguments public static void WriteInformationMessage(string tag, string id, string message, params object[] arguments) { WriteMessage(LogMessageType.Information, tag, id, null, message, arguments); } /// /// Writes a warning message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The exception to attach /// The message format arguments public static void WriteWarningMessage(string tag, string id, Exception ex, string message, params object[] arguments) { WriteMessage(LogMessageType.Warning, tag, id, ex, message, arguments); } /// /// Writes an error message to the current log destination /// /// The message to write /// The tag-type for this message /// The message id /// The exception to attach /// The message format arguments public static void WriteErrorMessage(string tag, string id, Exception ex, string message, params object[] arguments) { WriteMessage(LogMessageType.Error, tag, id, ex, message, arguments); } /// /// Writes a message to the current log destination /// /// The message to write /// The type of the message /// The tag-type for this message /// The message id public static void WriteMessage(LogMessageType type, string tag, string id, string message, params object[] arguments) { WriteMessage(type, tag, id, null, message, arguments); } /// /// Writes a message to the current log destination /// /// The message to write /// The type of the message /// An exception value /// The tag-type for this message /// The message id /// The arguments to format the log message with public static void WriteMessage(LogMessageType type, string tag, string id, Exception ex, string message, params object[] arguments) { var msg = new LogEntry(message, arguments, type, tag, id, ex); lock (m_lock) { var cs = CurrentScope; while (cs != null && !cs.IsolatingScope) { cs.WriteMessage(msg); cs = cs.Parent; } } } /// /// Starts a new scope, that can be closed by disposing the returned instance /// /// Flag indicating if the scope should be detached from the parent /// The new scope. public static IDisposable StartIsolatingScope(bool detached) { lock (m_lock) { var scope = StartScope(null, null, true); if (detached) DetachCurrentScope(scope); return scope; } } /// /// Detaches the current scope, such that new scopes do not chain onto this /// /// The current scope. public static IDisposable DetachCurrentScope(IDisposable scope) { lock (m_lock) { if (CurrentScope == scope && scope != null && CurrentScope.Parent != null) CurrentScope = CurrentScope.Parent; } return scope; } /// /// Starts a new scope, that can be closed by disposing the returned instance /// /// The new scope. public static IDisposable StartScope() { return StartScope((ILogDestination)null, null); } /// /// Starts a new scope, that can be stopped by disposing the returned instance /// /// The log target /// The log level /// The new scope. public static IDisposable StartScope(ILogDestination log, LogMessageType level) { return StartScope(log, new LogTagFilter(level, null, null)); } /// /// Starts a new scope, that can be stopped by disposing the returned instance /// /// The log target /// The log filter /// The new scope. public static IDisposable StartScope(ILogDestination log, ILogFilter filter = null, bool isolating = false) { return new LogScope(log, filter, CurrentScope, isolating); } /// /// Starts a new scope, that can be stopped by disposing the returned instance /// /// The log target /// The log filter /// The new scope. public static IDisposable StartScope(Action log, Func filter = null) { return new LogScope(new FunctionLogDestination(log), filter == null ? null : new FunctionFilter(filter), CurrentScope, false); } /// /// Starts the scope. /// /// The scope to start. internal static void StartScope(LogScope scope) { CurrentScope = scope; } /// /// Closes the scope. /// /// The scope to finish. internal static void CloseScope(LogScope scope) { lock(m_lock) { if (CurrentScope == scope && scope != m_root) CurrentScope = scope.Parent; m_log_instances.Remove(scope.InstanceID); } } /// /// Gets or sets the current log destination in a call-context aware fashion /// internal static LogScope CurrentScope { get { lock (m_lock) { var cur = CallContext.GetData(LOGICAL_CONTEXT_KEY) as string; if (cur == null || cur == m_root.InstanceID) return m_root; LogScope sc; if (!m_log_instances.TryGetValue(cur, out sc)) throw new Exception("Unable to find log in lookup table, this may be caused by attempting to transport call contexts between AppDomains (eg. with remoting calls)"); return sc; } } private set { lock (m_lock) { if (value != null) { m_log_instances[value.InstanceID] = value; CallContext.SetData(LOGICAL_CONTEXT_KEY, value.InstanceID); } else { CallContext.SetData(LOGICAL_CONTEXT_KEY, null); } } } } } }