TextFileLogWriter.cs 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Text;
  4. using System.IO;
  5. using Wayne.Lib.IO;
  6. namespace Wayne.Lib.Log
  7. {
  8. /// <summary>
  9. /// The textfile log writer is a log writer that has its output in a text log file.
  10. /// </summary>
  11. internal class TextFileLogWriter : LogWriter
  12. {
  13. #region Fields
  14. private bool disposed;
  15. private readonly Dictionary<string, Stream> logFileDict = new Dictionary<string, Stream>();
  16. private readonly object logFileDictLock = new object();
  17. private readonly Dictionary<string, int> currentFileCircleIndex = new Dictionary<string, int>();
  18. private readonly LogConfigTextFilePath logPath;
  19. private readonly LogConfigTextFileMaxSize maxSize;
  20. private readonly LogTextWritingParameters writingParameters;
  21. private Dictionary<string, string> openedRealFileNames = new Dictionary<string, string>();
  22. private Queue<Dictionary<string, string>> openedFileSets = new Queue<Dictionary<string, string>>();
  23. private bool shortFileMessages;
  24. private static Dictionary<string, LogConfigTextFilePath> cleanedLogs = new Dictionary<string, LogConfigTextFilePath>();
  25. #endregion
  26. #region Construction
  27. public TextFileLogWriter(string logName, LogConfigTextFileOutput output)
  28. : base(logName)
  29. {
  30. shortFileMessages = output.ShortFileMessages;
  31. logPath = output.LogConfigTextFilePath;
  32. maxSize = output.MaxSize;
  33. writingParameters = new LogTextWritingParameters(output.DateTimeFormat, output.EntityLogKind, output.SuppressCategory, output.KeepFileOpened);
  34. if (maxSize.CleanOnStartup)
  35. {
  36. MaybeClean(logPath);
  37. }
  38. }
  39. /// <summary>
  40. /// Cleans up files matching a log path once per process instantiation - checks a static dictionary for the path
  41. /// </summary>
  42. /// <param name="logPath"></param>
  43. protected void MaybeClean(LogConfigTextFilePath logPath)
  44. {
  45. string prefix = logPath.BaseName;
  46. if (cleanedLogs.ContainsKey(prefix)) return;
  47. try
  48. {
  49. string directory = Path.GetDirectoryName(prefix);
  50. string filePrefix = Path.GetFileNameWithoutExtension(prefix);
  51. DirectoryInfo di = new DirectoryInfo(directory);
  52. if (di.Exists)
  53. {
  54. FileInfo[] fiArray = di.GetFiles();
  55. foreach (FileInfo fi in fiArray)
  56. {
  57. if (fi.Name.StartsWith(filePrefix))
  58. {
  59. fi.Delete();
  60. }
  61. }
  62. }
  63. }
  64. catch
  65. {
  66. // ignore all errors
  67. }
  68. }
  69. #endregion
  70. #region IDisposable Members
  71. /// <summary>
  72. /// Internal dispose method.
  73. /// </summary>
  74. /// <param name="disposing"></param>
  75. /// <param name="reason"></param>
  76. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  77. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  78. protected override void Dispose(bool disposing, string reason)
  79. {
  80. if (!disposed)
  81. {
  82. try
  83. {
  84. CloseAllFiles(reason);
  85. }
  86. catch (Exception) { }
  87. disposed = true;
  88. }
  89. }
  90. #endregion
  91. #region Public Properties
  92. /// <summary>
  93. /// File path of the log.
  94. /// </summary>
  95. public LogConfigTextFilePath LogConfigTextFilePath
  96. {
  97. get { return logPath; }
  98. }
  99. #endregion
  100. #region Public Methods
  101. /// <summary>
  102. /// Checks so the file stream is open and writes the specified log entry to the log.
  103. /// </summary>
  104. /// <param name="logEntry"></param>
  105. public override void PerformWrite(LogEntry logEntry)
  106. {
  107. if (disposed)
  108. return;
  109. string text = LogTextWriting.GetLogEntryText(this, logEntry, writingParameters);
  110. DebugMarkerLogEntry debugMarkerLogEntry = logEntry as DebugMarkerLogEntry;
  111. if (debugMarkerLogEntry != null)
  112. {
  113. lock (logFileDictLock)
  114. {
  115. string[] fileNames = new string[logFileDict.Keys.Count];
  116. logFileDict.Keys.CopyTo(fileNames, 0); // Use copy of list since dict may change in WriteTextLine!
  117. foreach (string fileName in fileNames)
  118. WriteTextLine(fileName, text, true);
  119. }
  120. }
  121. else
  122. {
  123. string fileName = CheckStream(logEntry.EntityCategory);
  124. WriteTextLine(fileName, text, true);
  125. }
  126. if (!writingParameters.KeepFileOpened)
  127. CloseAllFiles("Should be kept closed...");
  128. }
  129. internal override void PerformListCleaning(DateTime oldestAllowedTouch)
  130. {
  131. base.PerformListCleaning(oldestAllowedTouch);
  132. logPath.PerformListCleaning(oldestAllowedTouch);
  133. }
  134. #endregion
  135. #region Private Methods: Streams and files
  136. /// <summary>
  137. /// Checks if so the file stream is open and that it has the correct date suffix in the filename.
  138. /// </summary>
  139. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  140. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  141. private string CheckStream(EntityCategory entityCategory)
  142. {
  143. string fileName = logPath.ToString(entityCategory);
  144. if (logPath.IsTimeForNewFileName())
  145. {
  146. // Close all log files.
  147. CloseAllFiles("Creating new log file.");
  148. openedFileSets.Enqueue(openedRealFileNames);
  149. openedRealFileNames = new Dictionary<string, string>();
  150. MaybeCleanOutDatedFiles();
  151. // Get the new file name.
  152. fileName = logPath.ToString(entityCategory);
  153. }
  154. lock (logFileDictLock)
  155. {
  156. if (!logFileDict.ContainsKey(fileName))
  157. {
  158. try
  159. {
  160. // Check so the folder exists.
  161. string parsedFileName = Paths.Parse(fileName);
  162. string directoryName = Path.GetDirectoryName(parsedFileName);
  163. FileSupport.EnsureDirectoryExists(directoryName);
  164. string realFileName = parsedFileName;
  165. // Get real file name if we are using a file circle.
  166. if (maxSize.MaxFileCircleCount > 1)
  167. realFileName = GetNextCircularFileName(parsedFileName);
  168. Stream fileStream = FileSupport.Open(realFileName, FileMode.Append, FileAccess.Write, FileShare.Read, 100, 20);
  169. if (!openedRealFileNames.ContainsKey(realFileName))
  170. {
  171. openedRealFileNames.Add(realFileName, realFileName);
  172. }
  173. if (fileStream != null)
  174. {
  175. if (fileStream.Position > maxSize.MaxSizePerFile) //File size checking.
  176. {
  177. fileStream.Close();
  178. fileStream = null;
  179. if (maxSize.MaxFileCircleCount > 1) //If we are using circular logfiles, try to open the next file. This should not be appended, but overwritten.
  180. fileStream = OpenNextCircularFile(parsedFileName);
  181. }
  182. logFileDict[fileName] = fileStream;
  183. }
  184. }
  185. catch (Exception exception)
  186. {
  187. Logger.FireOnThreadException(new LogException(LogExceptionType.GeneralThreadException,
  188. string.Format(System.Globalization.CultureInfo.InvariantCulture, "An Exception is caught in the Logger thread when trying to open the log file \"{0}\": {1}", fileName, exception.Message),
  189. exception));
  190. }
  191. WriteFileStart(fileName);
  192. }
  193. }
  194. return fileName;
  195. }
  196. /// <summary>
  197. /// Delete filesets older than our specified limits
  198. /// </summary>
  199. private void MaybeCleanOutDatedFiles()
  200. {
  201. if (openedFileSets.Count > maxSize.MaxDays)
  202. {
  203. Dictionary<string, string> filesToPurge = openedFileSets.Dequeue();
  204. foreach (string f in filesToPurge.Values)
  205. {
  206. try
  207. {
  208. File.Delete(f);
  209. }
  210. catch { }
  211. }
  212. }
  213. }
  214. /// <summary>
  215. /// Closes all log files
  216. /// </summary>
  217. /// <param name="reason"></param>
  218. /// <returns>list of strings of closed files</returns>
  219. private List<string> CloseAllFiles(string reason)
  220. {
  221. List<String> closedFiles = new List<String>();
  222. lock (logFileDictLock)
  223. {
  224. foreach (KeyValuePair<string, Stream> pair in logFileDict)
  225. {
  226. WriteFileEnd(pair.Key, reason);
  227. if (pair.Value != null)
  228. pair.Value.Close();
  229. closedFiles.Add(pair.Key);
  230. }
  231. logFileDict.Clear();
  232. currentFileCircleIndex.Clear();
  233. }
  234. return closedFiles;
  235. }
  236. #endregion
  237. #region Private methods: Circular file buffering methods
  238. /// <summary>
  239. /// Opens a circular file for the specified file name. ( Adds an appropriate sequence number before the last extension )
  240. /// </summary>
  241. /// <param name="fileName"></param>
  242. /// <returns></returns>
  243. private Stream OpenNextCircularFile(string fileName)
  244. {
  245. string realFileName = GetNextCircularFileName(fileName);
  246. if (!openedRealFileNames.ContainsKey(realFileName))
  247. {
  248. openedRealFileNames.Add(realFileName, realFileName);
  249. }
  250. return FileSupport.Open(realFileName, FileMode.Create, FileAccess.Write, FileShare.Read, 100, 20);
  251. }
  252. /// <summary>
  253. /// Creates the file name for the next circular file.
  254. /// </summary>
  255. /// <param name="fileName"></param>
  256. /// <returns></returns>
  257. private string GetNextCircularFileName(string fileName)
  258. {
  259. int seq = 1;
  260. //Parts of the file name that should be used to recompile the file name, but including a sequence number.
  261. string dir = Path.GetDirectoryName(fileName);
  262. string fileNameStart = Path.GetFileNameWithoutExtension(fileName);
  263. string fileNameEnd = Path.GetExtension(fileName);
  264. //If we already have a sequence number for this file
  265. if (currentFileCircleIndex.TryGetValue(fileName, out seq))
  266. {
  267. //Increase it
  268. seq++;
  269. //Wrap around if reached max.
  270. if (seq > maxSize.MaxFileCircleCount)
  271. seq = 1;
  272. }
  273. else
  274. {
  275. //First time for this file name since program start. We see if there are any files on the hard disk and pick the sequence number of the latest written.
  276. string searchPath = fileNameStart + "*" + fileNameEnd;
  277. DirectoryInfo di = new DirectoryInfo(Path.GetDirectoryName(fileName));
  278. List<FileInfo> existingFileInfos = new List<FileInfo>(di.GetFiles(searchPath));
  279. if (existingFileInfos.Count > 0)
  280. {
  281. //Sort the files, to get them in backwards last-written order
  282. existingFileInfos.Sort(new Comparison<FileInfo>(CompareFileInfos));
  283. int lastSeq = 0;
  284. //We must now get the sequence number of the newest file that has one.
  285. for (int i = 0; i < existingFileInfos.Count; i++)
  286. {
  287. if (TryGetSequenceNumberFromFileName(existingFileInfos[i], ref lastSeq))
  288. {
  289. seq = lastSeq;
  290. break;
  291. }
  292. }
  293. }
  294. }
  295. currentFileCircleIndex[fileName] = seq;
  296. return Path.Combine(dir, string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}.{1}{2}", fileNameStart, seq, fileNameEnd));
  297. }
  298. /// <summary>
  299. /// Parses a file name and tries to find the circular file sequence number from that.
  300. /// </summary>
  301. /// <param name="lastWrittenFileInfo"></param>
  302. /// <param name="sequenceNumber"></param>
  303. /// <returns></returns>
  304. private static bool TryGetSequenceNumberFromFileName(FileInfo lastWrittenFileInfo, ref int sequenceNumber)
  305. {
  306. string[] splitFileName = lastWrittenFileInfo.Name.Split('.');
  307. for (int i = splitFileName.Length - 1; i >= 0; i--)
  308. {
  309. if (char.IsNumber(splitFileName[i], 0))
  310. {
  311. try
  312. {
  313. sequenceNumber = int.Parse(splitFileName[i], System.Globalization.CultureInfo.InvariantCulture);
  314. return true;
  315. }
  316. catch (FormatException) { }
  317. catch (OverflowException) { }
  318. }
  319. }
  320. sequenceNumber = 0;
  321. return false;
  322. }
  323. /// <summary>
  324. /// Compare the file infos, in reverse last write time order.
  325. /// </summary>
  326. /// <param name="f1"></param>
  327. /// <param name="f2"></param>
  328. /// <returns></returns>
  329. static int CompareFileInfos(FileInfo f1, FileInfo f2)
  330. {
  331. return f2.LastWriteTime.CompareTo(f1.LastWriteTime);
  332. }
  333. #endregion
  334. #region Private Methods: Writing
  335. /// <summary>
  336. /// Writes a message to the log that the file has been appended
  337. /// </summary>
  338. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileStart(System.String)")]
  339. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  340. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  341. private void WriteFileStart(string fileName)
  342. {
  343. if (writingParameters.KeepFileOpened)
  344. {
  345. try
  346. {
  347. DateTime dateTime = DateTime.Now;
  348. string dateTimeText;
  349. if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
  350. dateTimeText = string.Empty;
  351. else
  352. dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
  353. if (shortFileMessages)
  354. {
  355. WriteTextLine(fileName,
  356. dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "TextFileLogWriter: FileAppended {0}. ------------------------------------------------\r\n",
  357. dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff",
  358. System.Globalization.CultureInfo.InvariantCulture)),
  359. false);
  360. }
  361. else
  362. {
  363. WriteTextLine(fileName, string.Concat(
  364. dateTimeText + "----------------------------------------------------\r\n",
  365. dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "File Appended {0}.\r\n", dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
  366. dateTimeText + "----------------------------------------------------\r\n"), false);
  367. }
  368. }
  369. catch (Exception)
  370. {
  371. }
  372. }
  373. }
  374. /// <summary>
  375. /// Writes a message to the log that the file has been closed.
  376. /// </summary>
  377. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Globalization", "CA1303:DoNotPassLiteralsAsLocalizedParameters", MessageId = "Wayne.Lib.Log.TextFileLogWriter.WriteFileEnd(System.String)")]
  378. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
  379. [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2102:CatchNonClsCompliantExceptionsInGeneralHandlers")]
  380. private void WriteFileEnd(string fileName, string reason)
  381. {
  382. if (writingParameters.KeepFileOpened)
  383. {
  384. try
  385. {
  386. DateTime dateTime = DateTime.Now;
  387. string dateTimeText;
  388. if (string.IsNullOrEmpty(writingParameters.DateTimeFormat))
  389. dateTimeText = string.Empty;
  390. else
  391. dateTimeText = Logger.DateTimeToString(dateTime, writingParameters.DateTimeFormat) + " ";
  392. if (shortFileMessages)
  393. {
  394. WriteTextLine(fileName,
  395. dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "TextFileLogWriter: FileClosed Reason: {0} {1}. ------------------------------------------------\r\n", reason, dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
  396. false);
  397. }
  398. else
  399. {
  400. WriteTextLine(fileName, string.Concat(
  401. dateTimeText + "----------------------------------------------------\r\n",
  402. dateTimeText + string.Format(System.Globalization.CultureInfo.InvariantCulture, "File Closed. Reason: {0} {1}.\r\n", reason, dateTime.ToString("yyyy-MM-dd, HH:mm:ss:fff", System.Globalization.CultureInfo.InvariantCulture)),
  403. dateTimeText + "----------------------------------------------------\r\n"), false);
  404. }
  405. }
  406. catch (Exception)
  407. {
  408. }
  409. }
  410. }
  411. /// <summary>
  412. /// Writes the specified text line to the log.
  413. /// </summary>
  414. /// <param name="fileName"></param>
  415. /// <param name="text"></param>
  416. /// <param name="checkFileSize">Dont check file size for internal loggings, like close file etc.</param>
  417. private void WriteTextLine(string fileName, string text, bool checkFileSize)
  418. {
  419. lock (logFileDictLock)
  420. {
  421. Stream fileStream;
  422. if (logFileDict.TryGetValue(fileName, out fileStream) && (fileStream != null) && fileStream.CanWrite)
  423. {
  424. byte[] textBytes = Encoding.UTF8.GetBytes(text);
  425. fileStream.Write(textBytes, 0, textBytes.Length);
  426. fileStream.Flush();
  427. if (checkFileSize && (fileStream.Position > maxSize.MaxSizePerFile))
  428. {
  429. WriteFileEnd(fileName, "Max size of file (" + maxSize.MaxSizePerFile.ToString(System.Globalization.CultureInfo.InvariantCulture) + ") reached");
  430. fileStream.Close();
  431. logFileDict[fileName] = null;
  432. //If we are working with circular file buffer, whe should just open the next file.
  433. if (maxSize.MaxFileCircleCount > 1)
  434. logFileDict[fileName] = OpenNextCircularFile(fileName);
  435. }
  436. }
  437. }
  438. }
  439. #endregion
  440. }
  441. }