PageRenderTime 38ms CodeModel.GetById 13ms app.highlight 18ms RepoModel.GetById 1ms app.codeStats 1ms

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