Logger.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System; 6 using System.Diagnostics; 7 using System.Globalization; 8 using System.IO; 9 using System.Linq; 10 using System.Reflection; 11 using System.Runtime.CompilerServices; 12 using System.Threading.Tasks; 13 using PowerToys.Interop; 14 15 namespace ManagedCommon 16 { 17 public static class Logger 18 { 19 private static readonly string Error = "Error"; 20 private static readonly string Warning = "Warning"; 21 private static readonly string Info = "Info"; 22 #if DEBUG 23 private static readonly string Debug = "Debug"; 24 #endif 25 private static readonly string TraceFlag = "Trace"; 26 27 private static readonly string Version = Assembly.GetExecutingAssembly().GetCustomAttribute<AssemblyFileVersionAttribute>()?.Version ?? "Unknown"; 28 29 /// <summary> 30 /// Gets the path to the log directory for the current version of the app. 31 /// </summary> 32 public static string CurrentVersionLogDirectoryPath { get; private set; } 33 34 /// <summary> 35 /// Gets the path to the current log file. 36 /// </summary> 37 public static string CurrentLogFile { get; private set; } 38 39 /// <summary> 40 /// Gets the path to the log directory for the app. 41 /// </summary> 42 public static string AppLogDirectoryPath { get; private set; } 43 44 /// <summary> 45 /// Initializes the logger and sets the path for logging. 46 /// </summary> 47 /// <example>InitializeLogger("\\FancyZones\\Editor\\Logs")</example> 48 /// <param name="applicationLogPath">The path to the log files folder.</param> 49 /// <param name="isLocalLow">If the process using Logger is a low-privilege process.</param> 50 public static void InitializeLogger(string applicationLogPath, bool isLocalLow = false) 51 { 52 string versionedPath = LogDirectoryPath(applicationLogPath, isLocalLow); 53 string basePath = Path.GetDirectoryName(versionedPath); 54 55 if (!Directory.Exists(versionedPath)) 56 { 57 Directory.CreateDirectory(versionedPath); 58 } 59 60 AppLogDirectoryPath = basePath; 61 CurrentVersionLogDirectoryPath = versionedPath; 62 63 var logFile = "Log_" + DateTime.Now.ToString(@"yyyy-MM-dd", CultureInfo.InvariantCulture) + ".log"; 64 var logFilePath = Path.Combine(versionedPath, logFile); 65 CurrentLogFile = logFilePath; 66 67 Trace.Listeners.Add(new TextWriterTraceListener(logFilePath)); 68 69 Trace.AutoFlush = true; 70 71 // Clean up old version log folders 72 Task.Run(() => DeleteOldVersionLogFolders(basePath, versionedPath)); 73 } 74 75 public static string LogDirectoryPath(string applicationLogPath, bool isLocalLow = false) 76 { 77 string basePath; 78 if (isLocalLow) 79 { 80 basePath = Environment.GetEnvironmentVariable("userprofile") + "\\appdata\\LocalLow\\Microsoft\\PowerToys" + applicationLogPath; 81 } 82 else 83 { 84 basePath = Constants.AppDataPath() + applicationLogPath; 85 } 86 87 string versionedPath = Path.Combine(basePath, Version); 88 return versionedPath; 89 } 90 91 /// <summary> 92 /// Deletes old version log folders, keeping only the current version's folder. 93 /// </summary> 94 /// <param name="basePath">The base path to the log files folder.</param> 95 /// <param name="currentVersionPath">The path to the current version's log folder.</param> 96 private static void DeleteOldVersionLogFolders(string basePath, string currentVersionPath) 97 { 98 try 99 { 100 if (!Directory.Exists(basePath)) 101 { 102 return; 103 } 104 105 var dirs = Directory.GetDirectories(basePath) 106 .Select(d => new DirectoryInfo(d)) 107 .OrderBy(d => d.CreationTime) 108 .Where(d => !string.Equals(d.FullName, currentVersionPath, StringComparison.OrdinalIgnoreCase)) 109 .Take(3) 110 .ToList(); 111 112 foreach (var directory in dirs) 113 { 114 try 115 { 116 Directory.Delete(directory.FullName, true); 117 LogInfo($"Deleted old log directory: {directory.FullName}"); 118 Task.Delay(500).Wait(); 119 } 120 catch (Exception ex) 121 { 122 LogError($"Failed to delete old log directory: {directory.FullName}", ex); 123 } 124 } 125 } 126 catch (Exception ex) 127 { 128 LogError("Error cleaning up old log folders", ex); 129 } 130 } 131 132 public static void LogError(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 133 { 134 Log(message, Error, memberName, sourceFilePath, sourceLineNumber); 135 } 136 137 public static void LogError(string message, Exception ex, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 138 { 139 if (ex == null) 140 { 141 Log(message, Error, memberName, sourceFilePath, sourceLineNumber); 142 } 143 else 144 { 145 var exMessage = 146 message + Environment.NewLine + 147 ex.GetType() + " (" + ex.HResult + "): " + ex.Message + Environment.NewLine; 148 149 if (ex.InnerException != null) 150 { 151 exMessage += 152 "Inner exception: " + Environment.NewLine + 153 ex.InnerException.GetType() + " (" + ex.InnerException.HResult + "): " + ex.InnerException.Message + Environment.NewLine; 154 } 155 156 exMessage += 157 "Stack trace: " + Environment.NewLine + 158 ex.StackTrace; 159 160 Log(exMessage, Error, memberName, sourceFilePath, sourceLineNumber); 161 } 162 } 163 164 public static void LogWarning(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 165 { 166 Log(message, Warning, memberName, sourceFilePath, sourceLineNumber); 167 } 168 169 public static void LogInfo(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 170 { 171 Log(message, Info, memberName, sourceFilePath, sourceLineNumber); 172 } 173 174 public static void LogDebug(string message, [System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 175 { 176 #if DEBUG 177 Log(message, Debug, memberName, sourceFilePath, sourceLineNumber); 178 #endif 179 } 180 181 public static void LogTrace([System.Runtime.CompilerServices.CallerMemberName] string memberName = "", [System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "", [System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0) 182 { 183 Log(string.Empty, TraceFlag, memberName, sourceFilePath, sourceLineNumber); 184 } 185 186 private static void Log(string message, string type, string memberName, string sourceFilePath, int sourceLineNumber) 187 { 188 Trace.WriteLine("[" + DateTime.Now.TimeOfDay + "] [" + type + "] " + GetCallerInfo(memberName, sourceFilePath, sourceLineNumber)); 189 Trace.Indent(); 190 if (message != string.Empty) 191 { 192 Trace.WriteLine(message); 193 } 194 195 Trace.Unindent(); 196 } 197 198 private static string GetCallerInfo(string memberName, string sourceFilePath, int sourceLineNumber) 199 { 200 string callerFileName = "Unknown"; 201 202 try 203 { 204 string fileName = Path.GetFileName(sourceFilePath); 205 if (!string.IsNullOrEmpty(fileName)) 206 { 207 callerFileName = fileName; 208 } 209 } 210 catch (Exception) 211 { 212 callerFileName = "Unknown"; 213 #if DEBUG 214 throw; 215 #endif 216 } 217 218 return $"{callerFileName}::{memberName}::{sourceLineNumber}"; 219 } 220 } 221 }