diff --git a/src/Echo/src/EchoTranslationEngine/Usings.cs b/src/Echo/src/EchoTranslationEngine/Usings.cs index 0404305b..c02c0f22 100644 --- a/src/Echo/src/EchoTranslationEngine/Usings.cs +++ b/src/Echo/src/EchoTranslationEngine/Usings.cs @@ -1,5 +1,4 @@ global using System.Threading.Channels; -global using Bugsnag.AspNet.Core; global using EchoTranslationEngine; global using Google.Protobuf.WellKnownTypes; global using Grpc.Core; diff --git a/src/Machine/src/Serval.Machine.EngineServer/Program.cs b/src/Machine/src/Serval.Machine.EngineServer/Program.cs index b03f6575..72a62b25 100644 --- a/src/Machine/src/Serval.Machine.EngineServer/Program.cs +++ b/src/Machine/src/Serval.Machine.EngineServer/Program.cs @@ -1,4 +1,3 @@ -using Bugsnag.AspNet.Core; using Hangfire; using OpenTelemetry.Trace; diff --git a/src/Machine/src/Serval.Machine.JobServer/Program.cs b/src/Machine/src/Serval.Machine.JobServer/Program.cs index 99388a62..f1ad384d 100644 --- a/src/Machine/src/Serval.Machine.JobServer/Program.cs +++ b/src/Machine/src/Serval.Machine.JobServer/Program.cs @@ -1,4 +1,3 @@ -using Bugsnag.AspNet.Core; using OpenTelemetry.Trace; var builder = WebApplication.CreateBuilder(args); diff --git a/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs b/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs index 711826d5..86bcbe75 100644 --- a/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs +++ b/src/Machine/src/Serval.Machine.Shared/Utils/AsyncTimer.cs @@ -57,6 +57,7 @@ private void StopTimer() protected override async ValueTask DisposeAsyncCore() { + await base.DisposeAsyncCore(); await StopAsync(); _timer.Dispose(); } diff --git a/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj b/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj index 3ea7d09f..7089533f 100644 --- a/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj +++ b/src/Serval/src/Serval.ApiServer/Serval.ApiServer.csproj @@ -30,12 +30,12 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + + diff --git a/src/Serval/src/Serval.ApiServer/Usings.cs b/src/Serval/src/Serval.ApiServer/Usings.cs index 13f508d8..8f4b6446 100644 --- a/src/Serval/src/Serval.ApiServer/Usings.cs +++ b/src/Serval/src/Serval.ApiServer/Usings.cs @@ -2,7 +2,6 @@ global using System.Security.Claims; global using System.Text.Json.Serialization; global using Asp.Versioning; -global using Bugsnag.AspNet.Core; global using Hangfire; global using Hangfire.Mongo; global using Hangfire.Mongo.Migration.Strategies; diff --git a/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj b/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj index 81b1b5a4..692caedf 100644 --- a/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj +++ b/src/Serval/src/Serval.Assessment/Serval.Assessment.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj b/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj index 146b0824..efc26d52 100644 --- a/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj +++ b/src/Serval/src/Serval.DataFiles/Serval.DataFiles.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Serval/src/Serval.Translation/Serval.Translation.csproj b/src/Serval/src/Serval.Translation/Serval.Translation.csproj index 81b1b5a4..692caedf 100644 --- a/src/Serval/src/Serval.Translation/Serval.Translation.csproj +++ b/src/Serval/src/Serval.Translation/Serval.Translation.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj b/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj index 4f9fa6d8..bb1af79a 100644 --- a/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj +++ b/src/Serval/src/Serval.Webhooks/Serval.Webhooks.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs index d5a6424f..db6f1c01 100644 --- a/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs +++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Configuration/IServiceCollectionsExtensions.cs @@ -8,4 +8,25 @@ public static IServiceCollection AddParallelCorpusPreprocessor(this IServiceColl services.AddSingleton(); return services; } + + /// + /// Add Bugsnag to your application. Configures the required bugsnag + /// services and attaches the Bugsnag middleware to catch unhandled + /// exceptions. + /// + /// + /// + public static IServiceCollection AddBugsnag(this IServiceCollection services) + { + services.TryAddSingleton(); + + return services + .AddSingleton() + .AddScoped(context => + { + var configuration = context.GetService>(); + var client = new Bugsnag.Client(configuration!.Value); + return client; + }); + } } diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj b/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj index ced38ebc..3c5964fe 100644 --- a/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj +++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/SIL.ServiceToolkit.csproj @@ -11,12 +11,13 @@ - + + - + diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs new file mode 100644 index 00000000..660787c8 --- /dev/null +++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagMiddleware.cs @@ -0,0 +1,60 @@ +namespace SIL.ServiceToolkit.Services; + +/// +/// The Bugsnag AspNetCore middleware. +/// +public class BugsnagMiddleware(RequestDelegate requestDelegate) +{ + public const string HttpContextItemsKey = "Bugsnag.Client"; + + private readonly RequestDelegate _next = requestDelegate; + + public async Task Invoke(HttpContext context, Bugsnag.IClient client) + { + if (client.Configuration.AutoCaptureSessions) + client.SessionTracking.CreateSession(); + + // capture the request information now as the http context + // may be changed by other error handlers after an exception + // has occurred + Bugsnag.Payload.Request bugsnagRequestInformation = ToRequest(context); + + client.BeforeNotify(report => + { + report.Event.Request = bugsnagRequestInformation; + }); + + context.Items[HttpContextItemsKey] = client; + + if (client.Configuration.AutoNotify) + { + try + { + await _next(context); + } + catch (Exception exception) + { + client.Notify(exception, Bugsnag.Payload.HandledState.ForUnhandledException()); + throw; + } + } + else + { + await _next(context); + } + } + + private static Bugsnag.Payload.Request ToRequest(HttpContext httpContext) + { + IPAddress? ip = httpContext.Connection.RemoteIpAddress ?? httpContext.Connection.LocalIpAddress; + + return new Bugsnag.Payload.Request + { + ClientIp = ip?.ToString(), + Headers = httpContext.Request.Headers.ToDictionary(x => x.Key, x => string.Join(",", x.Value!)), + HttpMethod = httpContext.Request.Method, + Url = httpContext.Request.GetDisplayUrl(), + Referer = httpContext.Request.Headers[HeaderNames.Referer], + }; + } +} diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs new file mode 100644 index 00000000..11b705e0 --- /dev/null +++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Services/BugsnagStartupFilter.cs @@ -0,0 +1,71 @@ +namespace SIL.ServiceToolkit.Services; + +/// +/// A startup filter to ensure that the Bugsnag middleware is +/// executed at the start of the middleware stack. +/// +public class BugsnagStartupFilter : IStartupFilter +{ + static BugsnagStartupFilter() + { + // populate the env variable that the client expects with the netcore + // provided value unless it has already been specified + if (Environment.GetEnvironmentVariable("BUGSNAG_RELEASE_STAGE") == null) + { + Environment.SetEnvironmentVariable( + "BUGSNAG_RELEASE_STAGE", + Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") + ); + } + } + + public Action Configure(Action next) + { + return builder => + { + builder + .ApplicationServices.GetService() + ?.SubscribeWithAdapter(new DiagnosticSubscriber()); + builder.UseMiddleware(); + next(builder); + }; + } + + private class DiagnosticSubscriber + { + /// + /// Handles exceptions that the Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddleware + /// swallows. + /// + /// + /// + [DiagnosticName("Microsoft.AspNetCore.Diagnostics.HandledException")] + public virtual void OnHandledException(Exception exception, HttpContext httpContext) + { + LogException(exception, httpContext); + } + + /// + /// Handles exceptions that the Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware + /// swallows. + /// + /// + /// + [DiagnosticName("Microsoft.AspNetCore.Diagnostics.UnhandledException")] + public virtual void OnUnhandledException(Exception exception, HttpContext httpContext) + { + LogException(exception, httpContext); + } + + private static void LogException(Exception exception, HttpContext httpContext) + { + httpContext.Items.TryGetValue(BugsnagMiddleware.HttpContextItemsKey, out object? clientObject); + + if (clientObject is Bugsnag.IClient client) + { + if (client.Configuration.AutoNotify) + client.Notify(exception, Bugsnag.Payload.HandledState.ForUnhandledException()); + } + } + } +} diff --git a/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs b/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs index a5800d9f..e3d986c6 100644 --- a/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs +++ b/src/ServiceToolkit/src/SIL.ServiceToolkit/Usings.cs @@ -1,15 +1,24 @@ -global using System.Diagnostics.CodeAnalysis; +global using System.Diagnostics; +global using System.Diagnostics.CodeAnalysis; +global using System.Net; global using System.Text; global using System.Text.Json.Nodes; global using System.Text.RegularExpressions; global using Grpc.Core; global using Grpc.Core.Interceptors; global using Hangfire; +global using Microsoft.AspNetCore.Builder; +global using Microsoft.AspNetCore.Hosting; +global using Microsoft.AspNetCore.Http; +global using Microsoft.AspNetCore.Http.Extensions; global using Microsoft.Extensions.DependencyInjection; +global using Microsoft.Extensions.DependencyInjection.Extensions; +global using Microsoft.Extensions.DiagnosticAdapter; global using Microsoft.Extensions.Diagnostics.HealthChecks; global using Microsoft.Extensions.Hosting; global using Microsoft.Extensions.Logging; global using Microsoft.Extensions.Options; +global using Microsoft.Net.Http.Headers; global using SIL.Machine.Corpora; global using SIL.ServiceToolkit.Models; global using SIL.ServiceToolkit.Services;