PageRenderTime 60ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 0ms

/StackExchange.Exceptional/Error.cs

https://github.com/djseng/StackExchange.Exceptional
C# | 471 lines | 252 code | 61 blank | 158 comment | 39 complexity | ede4134225d56515e61b02448f78368c MD5 | raw file
Possible License(s): Apache-2.0
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Diagnostics;
  5. using System.Web;
  6. using System.Collections.Specialized;
  7. using System.Web.Script.Serialization;
  8. using StackExchange.Exceptional.Extensions;
  9. namespace StackExchange.Exceptional
  10. {
  11. /// <summary>
  12. /// Represents a logical application error (as opposed to the actual exception it may be representing).
  13. /// </summary>
  14. [Serializable]
  15. public class Error
  16. {
  17. internal const string CollectionErrorKey = "CollectionFetchError";
  18. private static ConcurrentDictionary<string, string> _formLogFilters;
  19. private static readonly object initLock = new object();
  20. /// <summary>
  21. /// Filters on form values *not * to log, because they contain sensitive data
  22. /// </summary>
  23. public static ConcurrentDictionary<string, string> FormLogFilters
  24. {
  25. get
  26. {
  27. if (_formLogFilters == null)
  28. {
  29. lock (initLock)
  30. {
  31. if (_formLogFilters != null) return _formLogFilters;
  32. _formLogFilters = new ConcurrentDictionary<string, string>();
  33. Settings.Current.LogFilters.FormFilters.All.ForEach(flf => _formLogFilters[flf.Name] = flf.ReplaceWith ?? "");
  34. }
  35. }
  36. return _formLogFilters;
  37. }
  38. }
  39. /// <summary>
  40. /// The Id on this error, strictly for primary keying on persistent stores
  41. /// </summary>
  42. [ScriptIgnore]
  43. public long Id { get; set; }
  44. /// <summary>
  45. /// Unique identifier for this error, gernated on the server it came from
  46. /// </summary>
  47. public Guid GUID { get; set; }
  48. /// <summary>
  49. /// Initializes a new instance of the <see cref="Error"/> class.
  50. /// </summary>
  51. public Error() { }
  52. /// <summary>
  53. /// Initializes a new instance of the <see cref="Error"/> class from a given <see cref="Exception"/> instance.
  54. /// </summary>
  55. public Error(Exception e): this(e, null) { }
  56. /// <summary>
  57. /// Initializes a new instance of the <see cref="Error"/> class
  58. /// from a given <see cref="Exception"/> instance and
  59. /// <see cref="HttpContext"/> instance representing the HTTP
  60. /// context during the exception.
  61. /// </summary>
  62. public Error(Exception e, HttpContext context, string applicationName = null)
  63. {
  64. if (e == null) throw new ArgumentNullException("e");
  65. Exception = e;
  66. var baseException = e;
  67. // if it's not a .Net core exception, usually more information is being added
  68. // so use the wrapper for the message, type, etc.
  69. // if it's a .Net core exception type, drill down and get the innermost exception
  70. if (IsBuiltInException(e))
  71. baseException = e.GetBaseException();
  72. GUID = Guid.NewGuid();
  73. ApplicationName = applicationName ?? ErrorStore.ApplicationName;
  74. MachineName = Environment.MachineName;
  75. Type = baseException.GetType().FullName;
  76. Message = baseException.Message;
  77. Source = baseException.Source;
  78. Detail = e.ToString();
  79. CreationDate = DateTime.UtcNow;
  80. DuplicateCount = 1;
  81. var httpException = e as HttpException;
  82. if (httpException != null)
  83. {
  84. StatusCode = httpException.GetHttpCode();
  85. }
  86. SetContextProperties(context);
  87. ErrorHash = GetHash();
  88. }
  89. /// <summary>
  90. /// Sets Error properties pulled from HttpContext, if present
  91. /// </summary>
  92. /// <param name="context">The HttpContext related to the request</param>
  93. private void SetContextProperties(HttpContext context)
  94. {
  95. if (context == null) return;
  96. var request = context.Request;
  97. Func<Func<HttpRequest, NameValueCollection>, NameValueCollection> tryGetCollection = getter =>
  98. {
  99. try
  100. {
  101. return new NameValueCollection(getter(request));
  102. }
  103. catch (HttpRequestValidationException e)
  104. {
  105. Trace.WriteLine("Error parsing collection: " + e.Message);
  106. return new NameValueCollection {{CollectionErrorKey, e.Message}};
  107. }
  108. };
  109. ServerVariables = tryGetCollection(r => r.ServerVariables);
  110. QueryString = tryGetCollection(r => r.QueryString);
  111. Form = tryGetCollection(r => r.Form);
  112. // Filter form variables for sensitive information
  113. if (FormLogFilters.Count > 0)
  114. {
  115. foreach (var k in FormLogFilters.Keys)
  116. {
  117. if (Form[k] != null)
  118. Form[k] = FormLogFilters[k];
  119. }
  120. }
  121. try
  122. {
  123. Cookies = new NameValueCollection(request.Cookies.Count);
  124. for (var i = 0; i < request.Cookies.Count; i++)
  125. {
  126. Cookies.Add(request.Cookies[i].Name, request.Cookies[i].Value);
  127. }
  128. }
  129. catch (HttpRequestValidationException e)
  130. {
  131. Trace.WriteLine("Error parsing cookie collection: " + e.Message);
  132. }
  133. }
  134. /// <summary>
  135. /// returns if the type of the exception is built into .Net core
  136. /// </summary>
  137. /// <param name="e">The exception to check</param>
  138. /// <returns>True if the exception is a type from within the CLR, false if it's a user/third party type</returns>
  139. private bool IsBuiltInException(Exception e)
  140. {
  141. return e.GetType().Module.ScopeName == "CommonLanguageRuntimeLibrary";
  142. }
  143. /// <summary>
  144. /// Gets a unique-enough hash of this error. Stored as a quick comparison mehanism to rollup duplicate errors.
  145. /// </summary>
  146. /// <returns>"Unique" hash for this error</returns>
  147. private int? GetHash()
  148. {
  149. if (!Detail.HasValue()) return null;
  150. var result = Detail.GetHashCode();
  151. if (RollupPerServer && MachineName.HasValue())
  152. result = (result * 397)^ MachineName.GetHashCode();
  153. return result;
  154. }
  155. /// <summary>
  156. /// Reflects if the error is protected from deletion
  157. /// </summary>
  158. public bool IsProtected { get; set; }
  159. /// <summary>
  160. /// Gets the <see cref="Exception"/> instance used to create this error
  161. /// </summary>
  162. [ScriptIgnore]
  163. public Exception Exception { get; set; }
  164. /// <summary>
  165. /// Gets the name of the application that threw this exception
  166. /// </summary>
  167. public string ApplicationName { get; set; }
  168. /// <summary>
  169. /// Gets the hostname of where the exception occured
  170. /// </summary>
  171. public string MachineName { get; set; }
  172. /// <summary>
  173. /// Get the type of error
  174. /// </summary>
  175. public string Type { get; set; }
  176. /// <summary>
  177. /// Gets the source of this error
  178. /// </summary>
  179. public string Source { get; set; }
  180. /// <summary>
  181. /// Gets the exception message
  182. /// </summary>
  183. public string Message { get; set; }
  184. /// <summary>
  185. /// Gets the detail/stack trace of this error
  186. /// </summary>
  187. public string Detail { get; set; }
  188. /// <summary>
  189. /// The hash that describes this error
  190. /// </summary>
  191. public int? ErrorHash { get; set; }
  192. /// <summary>
  193. /// Gets the time in UTC that the error occured
  194. /// </summary>
  195. public DateTime CreationDate { get; set; }
  196. /// <summary>
  197. /// Gets the HTTP Status code associated with the request
  198. /// </summary>
  199. public int? StatusCode { get; set; }
  200. /// <summary>
  201. /// Gets the server variables collection for the request
  202. /// </summary>
  203. [ScriptIgnore]
  204. public NameValueCollection ServerVariables { get; set; }
  205. /// <summary>
  206. /// Gets the query string collection for the request
  207. /// </summary>
  208. [ScriptIgnore]
  209. public NameValueCollection QueryString { get; set; }
  210. /// <summary>
  211. /// Gets the form collection for the request
  212. /// </summary>
  213. [ScriptIgnore]
  214. public NameValueCollection Form { get; set; }
  215. /// <summary>
  216. /// Gets a collection representing the client cookies of the request
  217. /// </summary>
  218. [ScriptIgnore]
  219. public NameValueCollection Cookies { get; set; }
  220. /// <summary>
  221. /// Gets a collection of custom data added at log time
  222. /// </summary>
  223. public Dictionary<string, string> CustomData { get; set; }
  224. /// <summary>
  225. /// The number of newer Errors that have been discarded because they match this Error and fall within the configured
  226. /// "IgnoreSimilarExceptionsThreshold" TimeSpan value.
  227. /// </summary>
  228. public int? DuplicateCount { get; set; }
  229. /// <summary>
  230. /// Gets the SQL command text assocaited with this error
  231. /// </summary>
  232. public string SQL { get; set; }
  233. /// <summary>
  234. /// Date this error was deleted (for stores that support deletion and retention, e.g. SQL)
  235. /// </summary>
  236. public DateTime? DeletionDate { get; set; }
  237. /// <summary>
  238. /// The URL host of the request causing this error
  239. /// </summary>
  240. public string Host { get { return _host ?? (_host = ServerVariables == null ? "" : ServerVariables["HTTP_HOST"]); } set { _host = value; } }
  241. private string _host;
  242. /// <summary>
  243. /// The URL path of the request causing this error
  244. /// </summary>
  245. public string Url { get { return _url ?? (_url = ServerVariables == null ? "" : ServerVariables["URL"]); } set { _url = value; } }
  246. private string _url;
  247. /// <summary>
  248. /// The HTTP Method causing this error, e.g. GET or POST
  249. /// </summary>
  250. public string HTTPMethod { get { return _httpMethod ?? (_httpMethod = ServerVariables == null ? "" : ServerVariables["REQUEST_METHOD"]); } set { _httpMethod = value; } }
  251. private string _httpMethod;
  252. /// <summary>
  253. /// The IPAddress of the request causing this error
  254. /// </summary>
  255. public string IPAddress { get { return _ipAddress ?? (_ipAddress = ServerVariables == null ? "" : ServerVariables.GetRemoteIP()); } set { _ipAddress = value; } }
  256. private string _ipAddress;
  257. /// <summary>
  258. /// Json populated from database stored, deserialized after if needed
  259. /// </summary>
  260. [ScriptIgnore]
  261. public string FullJson { get; set; }
  262. /// <summary>
  263. /// Whether to roll up errors per-server. E.g. should an identical error happening on 2 separate servers be a DuplicateCount++, or 2 separate errors.
  264. /// </summary>
  265. [ScriptIgnore]
  266. public bool RollupPerServer { get; set; }
  267. /// <summary>
  268. /// Returns the value of the <see cref="Message"/> property.
  269. /// </summary>
  270. public override string ToString()
  271. {
  272. return Message;
  273. }
  274. /// <summary>
  275. /// Create a copy of the error and collections so if it's modified in memory logging is not affected
  276. /// </summary>
  277. /// <returns>A clone of this error</returns>
  278. public Error Clone()
  279. {
  280. var copy = (Error) MemberwiseClone();
  281. if (ServerVariables != null) copy.ServerVariables = new NameValueCollection(ServerVariables);
  282. if (QueryString != null) copy.QueryString = new NameValueCollection(QueryString);
  283. if (Form != null) copy.Form = new NameValueCollection(Form);
  284. if (Cookies != null) copy.Cookies = new NameValueCollection(Cookies);
  285. if (CustomData != null) copy.CustomData = new Dictionary<string, string>(CustomData);
  286. return copy;
  287. }
  288. /// <summary>
  289. /// Caribles strictly for JSON serialziation, to maintain non-dictonary behavior
  290. /// </summary>
  291. public List<NameValuePair> ServerVariablesSerialzable
  292. {
  293. get { return GetPairs(ServerVariables); }
  294. set { ServerVariables = GetNameValueCollection(value); }
  295. }
  296. /// <summary>
  297. /// Caribles strictly for JSON serialziation, to maintain non-dictonary behavior
  298. /// </summary>
  299. public List<NameValuePair> QueryStringSerialzable
  300. {
  301. get { return GetPairs(QueryString); }
  302. set { QueryString = GetNameValueCollection(value); }
  303. }
  304. /// <summary>
  305. /// Caribles strictly for JSON serialziation, to maintain non-dictonary behavior
  306. /// </summary>
  307. public List<NameValuePair> FormSerialzable
  308. {
  309. get { return GetPairs(Form); }
  310. set { Form = GetNameValueCollection(value); }
  311. }
  312. /// <summary>
  313. /// Caribles strictly for JSON serialziation, to maintain non-dictonary behavior
  314. /// </summary>
  315. public List<NameValuePair> CookiesSerialzable
  316. {
  317. get { return GetPairs(Cookies); }
  318. set { Cookies = GetNameValueCollection(value); }
  319. }
  320. /// <summary>
  321. /// Gets a JSON representation for this error
  322. /// </summary>
  323. public string ToJson()
  324. {
  325. var serializer = new JavaScriptSerializer();
  326. return serializer.Serialize(this);
  327. }
  328. /// <summary>
  329. /// Gets a JSON representation for this error suitable for cross-domain
  330. /// </summary>
  331. /// <returns></returns>
  332. public string ToDetailedJson()
  333. {
  334. var serializer = new JavaScriptSerializer();
  335. return serializer.Serialize(new
  336. {
  337. GUID,
  338. ApplicationName,
  339. CreationDate = CreationDate.ToEpochTime(),
  340. CustomData,
  341. DeletionDate = DeletionDate.ToEpochTime(),
  342. Detail,
  343. DuplicateCount,
  344. ErrorHash,
  345. HTTPMethod,
  346. Host,
  347. IPAddress,
  348. IsProtected,
  349. MachineName,
  350. Message,
  351. SQL,
  352. Source,
  353. StatusCode,
  354. Type,
  355. Url,
  356. QueryString = ServerVariables != null ? ServerVariables["QUERY_STRING"] : null,
  357. ServerVariables = ServerVariablesSerialzable.ToJsonDictionary(),
  358. CookieVariables = CookiesSerialzable.ToJsonDictionary(),
  359. QueryStringVariables = QueryStringSerialzable.ToJsonDictionary(),
  360. FormVariables = FormSerialzable.ToJsonDictionary()
  361. });
  362. }
  363. /// <summary>
  364. /// Deserializes provided JSON into an Error object
  365. /// </summary>
  366. /// <param name="json">JSON representing an Error</param>
  367. /// <returns>The Error object</returns>
  368. public static Error FromJson(string json)
  369. {
  370. var serializer = new JavaScriptSerializer();
  371. var result = serializer.Deserialize<Error>(json);
  372. return result;
  373. }
  374. /// <summary>
  375. /// Serialization class in place of the NameValueCollection pairs
  376. /// </summary>
  377. /// <remarks>This exists because things like a querystring can havle multiple values, they are not a dictionary</remarks>
  378. public class NameValuePair
  379. {
  380. /// <summary>
  381. /// The name for this variable
  382. /// </summary>
  383. public string Name { get; set; }
  384. /// <summary>
  385. /// The value for this variable
  386. /// </summary>
  387. public string Value { get; set; }
  388. }
  389. private List<NameValuePair> GetPairs(NameValueCollection nvc)
  390. {
  391. var result = new List<NameValuePair>();
  392. if (nvc == null) return result;
  393. for (int i = 0; i < nvc.Count; i++)
  394. {
  395. result.Add(new NameValuePair {Name = nvc.GetKey(i), Value = nvc.Get(i)});
  396. }
  397. return result;
  398. }
  399. private NameValueCollection GetNameValueCollection(List<NameValuePair> pairs)
  400. {
  401. var result = new NameValueCollection();
  402. if (pairs == null) return result;
  403. foreach(var p in pairs)
  404. {
  405. result.Add(p.Name, p.Value);
  406. }
  407. return result;
  408. }
  409. }
  410. }