PageRenderTime 106ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/App/StackExchange.SimpleErrorHandler/SimpleErrorHandler/XmlErrorLog.cs

https://code.google.com/p/stack-exchange-data-explorer/
C# | 342 lines | 237 code | 51 blank | 54 comment | 30 complexity | ab0f917b900a171b5dc2bc70c180b512 MD5 | raw file
Possible License(s): CC-BY-SA-3.0, Apache-2.0
  1. /*
  2. This file is derived off ELMAH:
  3. http://code.google.com/p/elmah/
  4. http://www.apache.org/licenses/LICENSE-2.0
  5. */
  6. using System;
  7. using System.Collections;
  8. using System.IO;
  9. using System.Text.RegularExpressions;
  10. using System.Web;
  11. using System.Xml;
  12. namespace SimpleErrorHandler
  13. {
  14. /// <summary>
  15. /// An <see cref="ErrorLog"/> implementation that uses XML files stored on disk as its backing store.
  16. /// </summary>
  17. public class XmlErrorLog : ErrorLog
  18. {
  19. private string _logPath;
  20. private int _maxFiles = 200;
  21. /// <summary>
  22. /// When set in config, any new exceptions will be compared to existing exceptions within this time window. If new exceptions match, they will be discarded.
  23. /// Useful for when a deluge of errors comes down upon your head.
  24. /// </summary>
  25. private TimeSpan? _ignoreSimilarExceptionsThreshold;
  26. public string LogPath
  27. {
  28. get { return _logPath; }
  29. set
  30. {
  31. if (value.StartsWith(@"~\"))
  32. {
  33. _logPath = AppDomain.CurrentDomain.GetData("APPBASE").ToString() + value.Substring(2);
  34. }
  35. else
  36. {
  37. _logPath = value;
  38. }
  39. }
  40. }
  41. public override bool DeleteError(string id)
  42. {
  43. FileInfo f;
  44. if (!TryGetErrorFile(id, out f))
  45. return false;
  46. // remove the read-only before deletion
  47. if (f.IsReadOnly)
  48. f.Attributes ^= FileAttributes.ReadOnly;
  49. f.Delete();
  50. return true;
  51. }
  52. public override bool ProtectError(string id)
  53. {
  54. FileInfo f;
  55. if (!TryGetErrorFile(id, out f))
  56. return false;
  57. f.Attributes |= FileAttributes.ReadOnly;
  58. return true;
  59. }
  60. /// <summary>
  61. /// Initializes a new instance of the <see cref="ErrorLog"/> class
  62. /// using a dictionary of configured settings.
  63. /// </summary>
  64. public XmlErrorLog(IDictionary config)
  65. {
  66. if (config["LogPath"] != null)
  67. {
  68. LogPath = (string)config["LogPath"];
  69. }
  70. else
  71. {
  72. throw new Exception("Log Path is missing for the XML error log.");
  73. }
  74. if (config["MaxFiles"] != null)
  75. {
  76. _maxFiles = Convert.ToInt32(config["MaxFiles"]);
  77. }
  78. if (config["IgnoreSimilarExceptionsThreshold"] != null)
  79. {
  80. // the config file value will be a positive time span, but we'll be subtracting this value from "Now" - negate it
  81. _ignoreSimilarExceptionsThreshold = TimeSpan.Parse(config["IgnoreSimilarExceptionsThreshold"].ToString()).Negate();
  82. }
  83. }
  84. /// <summary>
  85. /// Initializes a new instance of the <see cref="ErrorLog"/> class to use a specific path to store/load XML files.
  86. /// </summary>
  87. public XmlErrorLog(string logPath)
  88. {
  89. LogPath = logPath;
  90. }
  91. /// <summary>
  92. /// Gets the name of this error log implementation.
  93. /// </summary>
  94. public override string Name
  95. {
  96. get { return "Xml File Error Log"; }
  97. }
  98. /// <summary>
  99. /// Logs an error to the database.
  100. /// </summary>
  101. /// <remarks>
  102. /// Logs an error as a single XML file stored in a folder. XML files are named with a
  103. /// sortable date and a unique identifier. Currently the XML files are stored indefinately.
  104. /// As they are stored as files, they may be managed using standard scheduled jobs.
  105. /// </remarks>
  106. public override void Log(Error error)
  107. {
  108. // will allow fast comparisons of messages to see if we can ignore an incoming exception
  109. string messageHash = error.Detail;
  110. messageHash = messageHash.HasValue() ? messageHash.GetHashCode().ToString() : "no-stack-trace";
  111. Error original;
  112. // before we persist 'error', see if there are any existing errors that it could be a duplicate of
  113. if (_ignoreSimilarExceptionsThreshold.HasValue && TryFindOriginalError(error, messageHash, out original))
  114. {
  115. // just update the existing file after incrementing its "duplicate count"
  116. original.DuplicateCount = original.DuplicateCount.GetValueOrDefault(0) + 1;
  117. UpdateError(original);
  118. }
  119. else
  120. {
  121. LogNewError(error, messageHash);
  122. }
  123. }
  124. private void UpdateError(Error error)
  125. {
  126. FileInfo f;
  127. if (!TryGetErrorFile(error.Id, out f))
  128. throw new ArgumentOutOfRangeException("Unable to find a file for error with Id = " + error.Id);
  129. using (var stream = f.OpenWrite())
  130. using (var writer = new StreamWriter(stream))
  131. {
  132. LogError(error, writer);
  133. }
  134. }
  135. private void LogNewError(Error error, string messageHash)
  136. {
  137. error.Id = FriendlyGuid(Guid.NewGuid());
  138. string timeStamp = DateTime.Now.ToString("u").Replace(":", "").Replace(" ", "");
  139. string fileName = string.Format(@"{0}\error-{1}-{2}-{3}.xml", _logPath, timeStamp, messageHash, error.Id);
  140. FileInfo outfile = new FileInfo(fileName);
  141. using (StreamWriter outstream = outfile.CreateText())
  142. {
  143. LogError(error, outstream);
  144. }
  145. // we added a new file, so clean up old smack over our max errors limit
  146. RemoveOldErrors();
  147. }
  148. private void LogError(Error error, StreamWriter outstream)
  149. {
  150. using (XmlTextWriter w = new XmlTextWriter(outstream))
  151. {
  152. w.Formatting = Formatting.Indented;
  153. w.WriteStartElement("error");
  154. error.ToXml(w);
  155. w.WriteEndElement();
  156. w.Flush();
  157. }
  158. }
  159. /// <summary>
  160. /// Answers the older exception that 'possibleDuplicate' matches, returning null if no match is found.
  161. /// </summary>
  162. private bool TryFindOriginalError(SimpleErrorHandler.Error possibleDuplicate, string messageHash, out SimpleErrorHandler.Error original)
  163. {
  164. string[] files = Directory.GetFiles(LogPath);
  165. if (files.Length > 0)
  166. {
  167. var earliestDate = DateTime.Now.Add(_ignoreSimilarExceptionsThreshold.Value);
  168. // order by newest
  169. Array.Sort(files);
  170. Array.Reverse(files);
  171. foreach (var filename in files)
  172. {
  173. if (File.GetCreationTime(filename) >= earliestDate)
  174. {
  175. var match = Regex.Match(filename, @"error[-\d]+Z-(?<hashCode>((?<!\d)-|\d)+)-(?<id>.+)\.xml", RegexOptions.IgnoreCase);
  176. if (match.Success)
  177. {
  178. var existingHash = match.Groups["hashCode"].Value;
  179. if (messageHash.Equals(existingHash))
  180. {
  181. original = GetError(match.Groups["id"].Value).Error;
  182. return true;
  183. }
  184. }
  185. }
  186. else
  187. break; // no other files are newer, no use checking
  188. }
  189. }
  190. original = null;
  191. return false;
  192. }
  193. private string FriendlyGuid(Guid g)
  194. {
  195. string s = Convert.ToBase64String(g.ToByteArray());
  196. return s
  197. .Replace("/", "")
  198. .Replace("+", "")
  199. .Replace("=", "");
  200. }
  201. private void RemoveOldErrors()
  202. {
  203. string[] fileList = Directory.GetFiles(LogPath, "error*.*");
  204. // we'll start deleting once we're over the max
  205. if (fileList.Length <= _maxFiles) return;
  206. // file name contains timestamp - sort by creation date, ascending
  207. Array.Sort(fileList);
  208. // we'll remove any errors with index less than this upper bound
  209. int upperBound = fileList.Length - _maxFiles;
  210. for (int i = 0; i < upperBound && i < fileList.Length; i++)
  211. {
  212. var file = new FileInfo(fileList[i]);
  213. // have we protected this error from deletion?
  214. if ((file.Attributes & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
  215. {
  216. // we'll skip this error file and raise our search bounds up one
  217. upperBound++;
  218. }
  219. else
  220. {
  221. file.Delete();
  222. }
  223. }
  224. }
  225. /// <summary>
  226. /// Returns a page of errors from the folder in descending order of logged time as defined by the sortable filenames.
  227. /// </summary>
  228. public override int GetErrors(int pageIndex, int pageSize, IList errorEntryList)
  229. {
  230. if (pageIndex < 0) pageIndex = 0;
  231. if (pageSize < 0) pageSize = 25;
  232. string[] fileList = Directory.GetFiles(LogPath, "*.xml");
  233. if (fileList.Length < 1) return 0;
  234. Array.Sort(fileList);
  235. Array.Reverse(fileList);
  236. int currentItem = pageIndex * pageSize;
  237. int lastItem = (currentItem + pageSize < fileList.Length) ? currentItem + pageSize : fileList.Length;
  238. for (int i = currentItem; i < lastItem; i++)
  239. {
  240. FileInfo f = new FileInfo(fileList[i]);
  241. FileStream s = f.OpenRead();
  242. XmlTextReader r = new XmlTextReader(s);
  243. try
  244. {
  245. while (r.IsStartElement("error"))
  246. {
  247. SimpleErrorHandler.Error error = new SimpleErrorHandler.Error();
  248. error.FromXml(r);
  249. error.IsProtected = f.IsReadOnly; // have we "protected" this file from deletion?
  250. errorEntryList.Add(new ErrorLogEntry(this, error.Id, error));
  251. }
  252. }
  253. finally
  254. {
  255. r.Close();
  256. }
  257. }
  258. return fileList.Length;
  259. }
  260. /// <summary>
  261. /// Returns the specified error from the filesystem, or throws an exception if it does not exist.
  262. /// </summary>
  263. public override ErrorLogEntry GetError(string id)
  264. {
  265. string[] fileList = Directory.GetFiles(LogPath, string.Format("*{0}.xml", id));
  266. if (fileList.Length < 1)
  267. throw new Exception(string.Format("Can't locate error file for errorId {0}", id));
  268. FileInfo f = new FileInfo(fileList[0]);
  269. FileStream s = f.OpenRead();
  270. XmlTextReader r = new XmlTextReader(s);
  271. SimpleErrorHandler.Error error = new SimpleErrorHandler.Error();
  272. error.FromXml(r);
  273. r.Close();
  274. return new ErrorLogEntry(this, id, error);
  275. }
  276. private bool TryGetErrorFile(string id, out FileInfo file)
  277. {
  278. string[] fileList = Directory.GetFiles(LogPath, string.Format("*{0}.xml", id));
  279. if (fileList.Length != 1)
  280. {
  281. file = null;
  282. return false;
  283. }
  284. file = new FileInfo(fileList[0]);
  285. return true;
  286. }
  287. }
  288. }