static void Format(ExceptionWithCapturedContext ctx, StringBuilder sb, int indent) { if (ctx == null) return; string indentStr = IndentString(indent); string nlIndent = nl + indentStr; sb.Append(indentStr); string tmp = ctx.Exception.ToString().Replace("\r\n", nl).Replace(nl, nlIndent); sb.Append(tmp); if (ctx.InnerException != null) { sb.Append(nlIndent); sb.Append("Inner:"); sb.Append(nl); Format(ctx.InnerException, sb, indent + 1); } }
async Task<HashedLogIdentifier> LogExceptionRecursively(SqlConnectionContext conn, ExceptionWithCapturedContext ctx, int? parentInstanceID = null) { // Create the exTargetSite if it does not exist: var ts = ctx.TargetSite; SHA1Hash? exTargetSiteID = null; Task tskTargetSite = null; if (ts != null) { exTargetSiteID = ts.TargetSiteID; tskTargetSite = conn.ExecNonQuery( @"MERGE [dbo].[exTargetSite] WITH (HOLDLOCK) AS target USING (SELECT @exTargetSiteID) AS source (exTargetSiteID) ON (target.exTargetSiteID = source.exTargetSiteID) WHEN NOT MATCHED THEN INSERT ([exTargetSiteID], [AssemblyName], [TypeName], [MethodName], [ILOffset], [FileName], [FileLineNumber], [FileColumnNumber]) VALUES (@exTargetSiteID, @AssemblyName, @TypeName , @MethodName , @ILOffset , @FileName , @FileLineNumber , @FileColumnNumber );", prms => prms.AddInParamSHA1("@exTargetSiteID", exTargetSiteID.GetValueOrDefault()) .AddInParamSize("@AssemblyName", SqlDbType.NVarChar, 256, ts.AssemblyName) .AddInParamSize("@TypeName", SqlDbType.NVarChar, 256, ts.TypeName) .AddInParamSize("@MethodName", SqlDbType.NVarChar, 256, ts.MethodName) .AddInParam("@ILOffset", SqlDbType.Int, ts.ILOffset) .AddInParamSize("@FileName", SqlDbType.NVarChar, 256, ts.FileName) .AddInParam("@FileLineNumber", SqlDbType.Int, ts.FileLineNumber) .AddInParam("@FileColumnNumber", SqlDbType.Int, ts.FileColumnNumber) ); } SHA1Hash? userStateCollectionID = null; if (ctx.UserState != null) { userStateCollectionID = CalcCollectionID(ctx.UserState); } // Create the exException record if it does not exist: var tskGetPolicy = conn.ExecReader( @"MERGE [dbo].[exException] WITH (HOLDLOCK) AS target USING (SELECT @exExceptionID) AS source (exExceptionID) ON (target.exExceptionID = source.exExceptionID) WHEN NOT MATCHED THEN INSERT ([exExceptionID], [AssemblyName], [TypeName], [StackTrace], [exTargetSiteID]) VALUES (@exExceptionID, @AssemblyName, @TypeName, @StackTrace, @exTargetSiteID ); SELECT excpol.[LogWebContext], excpol.[LogWebRequestHeaders] FROM [dbo].[exExceptionPolicy] excpol WITH (NOLOCK) WHERE excpol.[exExceptionID] = @exExceptionID;", prms => prms.AddInParamSHA1("@exExceptionID", ctx.ExceptionID) .AddInParamSize("@AssemblyName", SqlDbType.NVarChar, 256, ctx.AssemblyName) .AddInParamSize("@TypeName", SqlDbType.NVarChar, 256, ctx.TypeName) .AddInParamSize("@StackTrace", SqlDbType.NVarChar, -1, ctx.StackTrace) .AddInParamSHA1("@exTargetSiteID", exTargetSiteID), // Read the SELECT result set into an ExceptionPolicy, or use the default policy: async dr => !await dr.ReadAsync() ? ExceptionPolicy.Default : new ExceptionPolicy( logWebContext: dr.GetBoolean(dr.GetOrdinal("LogWebContext")), logWebRequestHeaders: dr.GetBoolean(dr.GetOrdinal("LogWebRequestHeaders")) ) ); // Create the exException record if it does not exist: var exApplicationID = CalcApplicationID(cfg); var tskApplication = conn.ExecNonQuery( @"MERGE [dbo].[exApplication] WITH (HOLDLOCK) AS target USING (SELECT @exApplicationID) AS source (exApplicationID) ON (target.exApplicationID = source.exApplicationID) WHEN NOT MATCHED THEN INSERT ([exApplicationID], [MachineName], [ApplicationName], [EnvironmentName], [ProcessPath]) VALUES (@exApplicationID, @MachineName, @ApplicationName, @EnvironmentName, @ProcessPath );", prms => prms.AddInParamSHA1("@exApplicationID", exApplicationID) .AddInParamSize("@MachineName", SqlDbType.VarChar, 64, cfg.MachineName) .AddInParamSize("@ApplicationName", SqlDbType.VarChar, 96, cfg.ApplicationName) .AddInParamSize("@EnvironmentName", SqlDbType.VarChar, 32, cfg.EnvironmentName) .AddInParamSize("@ProcessPath", SqlDbType.NVarChar, 256, cfg.ProcessPath) ); // Create the instance record: var tskInstance = conn.ExecNonQuery( @"INSERT INTO [dbo].[exInstance] ([exExceptionID], [exApplicationID], [LoggedTimeUTC], [SequenceNumber], [IsHandled], [ApplicationIdentity], [ParentInstanceID], [CorrelationID], [ManagedThreadId], [UserStateCollectionID], [Message]) VALUES (@exExceptionID, @exApplicationID, @LoggedTimeUTC, @SequenceNumber, @IsHandled, @ApplicationIdentity, @ParentInstanceID, @CorrelationID, @ManagedThreadId, @UserStateCollectionID, @Message ); SET @exInstanceID = SCOPE_IDENTITY();", prms => prms.AddOutParam("@exInstanceID", SqlDbType.Int) .AddInParamSHA1("@exExceptionID", ctx.ExceptionID) .AddInParamSHA1("@exApplicationID", exApplicationID) .AddInParam("@LoggedTimeUTC", SqlDbType.DateTime2, ctx.LoggedTimeUTC) .AddInParam("@SequenceNumber", SqlDbType.Int, ctx.SequenceNumber) .AddInParam("@IsHandled", SqlDbType.Bit, ctx.IsHandled) .AddInParamSize("@ApplicationIdentity", SqlDbType.NVarChar, 128, cfg.ApplicationIdentity) .AddInParam("@ParentInstanceID", SqlDbType.Int, parentInstanceID) .AddInParam("@CorrelationID", SqlDbType.UniqueIdentifier, ctx.CorrelationID) .AddInParam("@ManagedThreadId", SqlDbType.Int, ctx.ManagedThreadID) .AddInParamSHA1("@UserStateCollectionID", userStateCollectionID) .AddInParamSize("@Message", SqlDbType.NVarChar, 256, ctx.Exception.Message), (prms, rc) => { return (int)prms["@exInstanceID"].Value; } ); // Await the exInstance record creation: int exInstanceID = await tskInstance; // Await the exception policy result: var policy = await tskGetPolicy; // If logging the web context is enabled and we have a web context to work with, log it: Task tskLoggingWeb = null; if (policy.LogWebContext && ctx.CapturedHttpContext != null) { tskLoggingWeb = LogWebContext(conn, policy, ctx, exInstanceID); } // Log the UserState collection: if (userStateCollectionID != null) await LogCollection(conn, userStateCollectionID.Value, ctx.UserState); // Wait for any outstanding logging tasks: if (tskLoggingWeb != null) await tskLoggingWeb; if (tskTargetSite != null) await tskTargetSite; await tskApplication; // Recursively log inner exceptions: var inner = ctx.InnerException; if (inner != null) // Return the inner-most exInstanceID because you can easily drill down through the ParentInstanceID columns to reach the root level. return await LogExceptionRecursively(conn, inner, exInstanceID); return new HashedLogIdentifier(ctx.ExceptionID, exInstanceID); }
async Task LogWebContext(SqlConnectionContext conn, ExceptionPolicy policy, ExceptionWithCapturedContext ctx, int exInstanceID) { var http = ctx.CapturedHttpContext; var host = ctx.WebHostingContext; // We require both HTTP and Host context: if (http == null || host == null) return; // Try to get the authenticated user for the HTTP context: string authUserName = null; if (http.User != null && http.User.Identity != null) authUserName = http.User.Identity.Name; // Compute the IDs: var requestURLQueryID = CalcURLQueryID(http.Url); var referrerURLQueryID = http.UrlReferrer == null ? (SHA1Hash?)null : CalcURLQueryID(http.UrlReferrer); var exWebApplicationID = CalcWebApplicationID(host); // Log the web application details: var tskWebApplication = conn.ExecNonQuery( @"MERGE [dbo].[exWebApplication] WITH (HOLDLOCK) AS target USING (SELECT @exWebApplicationID) AS source (exWebApplicationID) ON (target.exWebApplicationID = source.exWebApplicationID) WHEN NOT MATCHED THEN INSERT ([exWebApplicationID], [MachineName], [ApplicationID], [PhysicalPath], [VirtualPath], [SiteName]) VALUES (@exWebApplicationID, @MachineName, @ApplicationID, @PhysicalPath, @VirtualPath, @SiteName );", prms => prms.AddInParamSHA1("@exWebApplicationID", exWebApplicationID) .AddInParamSize("@MachineName", SqlDbType.NVarChar, 96, host.MachineName) .AddInParamSize("@ApplicationID", SqlDbType.VarChar, 96, host.ApplicationID) .AddInParamSize("@PhysicalPath", SqlDbType.NVarChar, 256, host.PhysicalPath) .AddInParamSize("@VirtualPath", SqlDbType.NVarChar, 256, host.VirtualPath) .AddInParamSize("@SiteName", SqlDbType.VarChar, 96, host.SiteName) ); // Log the request headers collection, if requested and available: Task tskCollection = null; SHA1Hash? exCollectionID = null; if (policy.LogWebRequestHeaders && http.Headers != null) { var tmpDict = new NameValueCollectionDictionary(http.Headers); // Compute the collection hash (must be done BEFORE `tskContextWeb`): exCollectionID = CalcCollectionID(tmpDict); // Store all records for the headers collection: tskCollection = LogCollection(conn, exCollectionID.Value, tmpDict); } // Log the web context: var tskContextWeb = conn.ExecNonQuery( @"INSERT INTO [dbo].[exContextWeb] ([exInstanceID], [exWebApplicationID], [AuthenticatedUserName], [HttpVerb], [RequestURLQueryID], [ReferrerURLQueryID], [RequestHeadersCollectionID]) VALUES (@exInstanceID, @exWebApplicationID, @AuthenticatedUserName, @HttpVerb, @RequestURLQueryID, @ReferrerURLQueryID, @RequestHeadersCollectionID );", prms => prms.AddInParam("@exInstanceID", SqlDbType.Int, exInstanceID) // Hosting environment: .AddInParamSHA1("@exWebApplicationID", exWebApplicationID) // Request details: .AddInParamSize("@AuthenticatedUserName", SqlDbType.VarChar, 96, authUserName) .AddInParamSize("@HttpVerb", SqlDbType.VarChar, 16, http.HttpMethod) .AddInParamSHA1("@RequestURLQueryID", requestURLQueryID) .AddInParamSHA1("@ReferrerURLQueryID", referrerURLQueryID) .AddInParamSHA1("@RequestHeadersCollectionID", exCollectionID) ); // Log the URLs: Task tskRequestURL, tskReferrerURL; tskRequestURL = LogURLQuery(conn, http.Url); if (http.UrlReferrer != null) tskReferrerURL = LogURLQuery(conn, http.UrlReferrer); else tskReferrerURL = null; // Await the completion of the tasks: await Task.WhenAll(tskRequestURL, tskWebApplication, tskContextWeb); if (tskReferrerURL != null) await tskReferrerURL; if (tskCollection != null) await tskCollection; }
public static string FormatException(ExceptionWithCapturedContext ctx) { if (ctx == null) return "(null)"; var sb = new StringBuilder(); Format(ctx, sb, 0); return sb.ToString(); }
/// <summary> /// Record the exception to the various database tables. /// </summary> /// <param name="ctx"></param> /// <returns></returns> async Task<ILogIdentifier> WriteDatabase(ExceptionWithCapturedContext ctx, int? parentInstanceID = null) { using (var conn = new SqlConnection(cfg.ConnectionString)) { bool attempt = true; if (noConnection) attempt = TryReconnect(); if (!attempt) return null; try { // Open connection and check connectivity: await conn.OpenAsync(); } catch (Exception inex) { // No connectivity; reset attempt timer: noConnection = true; lastConnectAttempt = DateTime.UtcNow; FailoverWrite(new ExceptionWithCapturedContext(inex, isHandled: true)); return null; } if (cfg.IsTransactional) { // Transactional logging: using (var tran = conn.BeginTransaction(IsolationLevel.Snapshot)) { var connCtx = new SqlConnectionContext(conn, tran); try { var logged = await LogExceptionRecursively(connCtx, ctx, null); tran.Commit(); return logged; } catch (Exception ex) { tran.Rollback(); FailoverWrite(new ExceptionWithCapturedContext(ex, isHandled: true)); return null; } } } else { // Non-transactional logging: var connCtx = new SqlConnectionContext(conn); var logged = await LogExceptionRecursively(connCtx, ctx, null); return logged; } } }
/// <summary> /// The failover case for when nothing seems to be set up as expected. /// </summary> /// <param name="ex"></param> /// <returns></returns> public void FailoverWrite(ExceptionWithCapturedContext ctx) { var sb = new StringBuilder(); Format(ctx, sb, 0); string output = sb.ToString(); FailoverWriteString(output); }
/// <summary> /// Wrapper method to catch and log exceptions thrown by <paramref name="a"/>. /// </summary> /// <param name="a">Asynchronous task to catch exceptions from.</param> /// <param name="isHandled">Whether the exception is explicitly handled or not.</param> /// <param name="correlationID">A unique identifier to tie two or more exception reports together.</param> /// <returns></returns> public async Task HandleExceptions(Func<Task> a, bool isHandled = false, Guid? correlationID = null) { ExceptionWithCapturedContext exToLog = null; try { await a(); } catch (Exception ex) { // NOTE(jsd): `await` is not allowed in catch blocks. exToLog = new ExceptionWithCapturedContext( ex, isHandled, correlationID, CurrentWebHost, new CapturedHttpContext(System.Web.HttpContext.Current) ); } if (exToLog != null) { await Write(exToLog); } }
/// <summary> /// Write the exception to the database. /// </summary> /// <returns></returns> public async Task<ILogIdentifier> Write(ExceptionWithCapturedContext thrown) { try { return await WriteDatabase(thrown); } catch (Exception ourEx) { var actual = new ExceptionWithCapturedContext(ourEx, isHandled: true); var report = new LoggerExceptionWithCapturedContext(actual, thrown); FailoverWrite(report); return (ILogIdentifier)null; } }
/// <summary> /// Represents a thrown exception and some context. /// </summary> /// <param name="ex">The exception that was thrown.</param> /// <param name="isHandled">Whether the exception is explicitly handled or not.</param> /// <param name="correlationID">A unique identifier to tie two or more exception reports together.</param> /// <param name="webHostingContext">The current web application hosting context, if applicable.</param> /// <param name="capturedHttpContext">Captured values from the current HTTP context being handled, if applicable.</param> public ExceptionWithCapturedContext( Exception ex, bool isHandled = false, Guid? correlationID = null, WebHostingContext webHostingContext = null, CapturedHttpContext capturedHttpContext = null ) { // Check for a WrappedException to pull out custom Exception info: WrappedException wex = ex as WrappedException; Exception thrownEx = ex; if (wex != null) { UserState = wex.UserState; ex = wex.Wrapped; } Exception = ex; IsHandled = isHandled; CorrelationID = correlationID; WebHostingContext = webHostingContext; CapturedHttpContext = capturedHttpContext; RealStackTrace = new StackTrace(thrownEx, true); LoggedTimeUTC = DateTime.UtcNow; ManagedThreadID = Thread.CurrentThread.ManagedThreadId; SequenceNumber = Interlocked.Increment(ref runningSequenceNumber); // Capture some details about the exception: var exType = ex.GetType(); AssemblyName = exType.Assembly.FullName; TypeName = exType.FullName; // TODO(jsd): Sanitize embedded file paths in stack trace StackTrace = thrownEx.StackTrace; // SHA1 hash the `assemblyName:typeName:stackTrace[:targetSite]`: string hashableData = String.Concat(AssemblyName, ":", TypeName, ":", StackTrace); if (RealStackTrace != null && RealStackTrace.FrameCount > 0) { // Add in TargetSite data: TargetSite = new ExceptionTargetSite(RealStackTrace); hashableData += ":" + TargetSite.GetHashableData(); } ExceptionID = Hash.SHA1(hashableData); if (ex.InnerException != null) InnerException = new ExceptionWithCapturedContext(ex.InnerException, isHandled, correlationID, webHostingContext, capturedHttpContext); }
internal LoggerExceptionWithCapturedContext(ExceptionWithCapturedContext actual, ExceptionWithCapturedContext symptom) { Actual = actual; Symptom = symptom; }