Example #1
0
        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);
            }
        }
Example #2
0
        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);
        }
Example #3
0
        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;
        }
Example #4
0
        public static string FormatException(ExceptionWithCapturedContext ctx)
        {
            if (ctx == null) return "(null)";

            var sb = new StringBuilder();
            Format(ctx, sb, 0);
            return sb.ToString();
        }
Example #5
0
        /// <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;
                }
            }
        }
Example #6
0
        /// <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);
        }
Example #7
0
        /// <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);
            }
        }
Example #8
0
 /// <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;
 }