From 70525fc2de76b43523ec695916497d9e0684290e Mon Sep 17 00:00:00 2001 From: "Eric J. Smith" Date: Fri, 8 Jan 2021 15:21:28 -0600 Subject: [PATCH] Adding better support for serverless functions and sample projects to show how to use them (#243) * Adding better support for serverless functions and sample projects to show how to use them * Some pr feedback. * Change name to ProcessQueueDeferred. Couple more lambda sample updates. * Minor * Change local server port from 50000 to 5000 Co-authored-by: Blake Niemyjski --- Exceptionless.Net.sln | 21 ++++++ .../Exceptionless.SampleAspNetCore.csproj | 2 - .../appsettings.json | 2 +- .../Exceptionless.SampleConsole/Program.cs | 2 +- .../Exceptionless.SampleHosting.csproj | 11 +++ .../Exceptionless.SampleHosting/Program.cs | 74 +++++++++++++++++++ .../Properties/launchSettings.json | 12 +++ .../SampleService.cs | 52 +++++++++++++ .../appsettings.json | 24 ++++++ .../Exceptionless.SampleLambda.csproj | 19 +++++ .../Exceptionless.SampleLambda/Function.cs | 35 +++++++++ samples/Exceptionless.SampleLambda/Readme.md | 49 ++++++++++++ .../aws-lambda-tools-defaults.json | 16 ++++ .../Controllers/ValuesController.cs | 49 ++++++++++++ ...xceptionless.SampleLambdaAspNetCore.csproj | 15 ++++ .../LambdaEntryPoint.cs | 19 +++++ .../Program.cs | 22 ++++++ .../Properties/launchSettings.json | 12 +++ .../Startup.cs | 43 +++++++++++ .../appsettings.json | 28 +++++++ .../aws-lambda-tools-defaults.json | 18 +++++ samples/Exceptionless.SampleMvc/Web.config | 2 +- samples/Exceptionless.SampleWcf/Web.config | 2 +- samples/Exceptionless.SampleWeb/Web.config | 4 +- samples/Exceptionless.SampleWebApi/App.config | 2 +- .../Exceptionless.SampleWindows/App.config | 2 +- .../Exceptionless.SampleWindows/Program.cs | 2 +- .../Properties/AssemblyInfo.cs | 2 +- .../ExceptionlessConfiguration.cs | 6 ++ src/Exceptionless/ExceptionlessClient.cs | 9 +++ .../ExceptionlessConfigurationExtensions.cs | 19 +++++ src/Exceptionless/Queue/ProcessQueueScope.cs | 15 ++++ .../ExceptionlessMiddleware.cs | 6 ++ .../ExceptionlessExtensions.cs | 25 ++++++- .../ExceptionlessLifetimeService.cs | 3 + test/Exceptionless.Tests/app.config | 2 +- 36 files changed, 612 insertions(+), 14 deletions(-) create mode 100644 samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj create mode 100644 samples/Exceptionless.SampleHosting/Program.cs create mode 100644 samples/Exceptionless.SampleHosting/Properties/launchSettings.json create mode 100644 samples/Exceptionless.SampleHosting/SampleService.cs create mode 100644 samples/Exceptionless.SampleHosting/appsettings.json create mode 100644 samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj create mode 100644 samples/Exceptionless.SampleLambda/Function.cs create mode 100644 samples/Exceptionless.SampleLambda/Readme.md create mode 100644 samples/Exceptionless.SampleLambda/aws-lambda-tools-defaults.json create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/Controllers/ValuesController.cs create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/LambdaEntryPoint.cs create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/Program.cs create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/Properties/launchSettings.json create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/Startup.cs create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/appsettings.json create mode 100644 samples/Exceptionless.SampleLambdaAspNetCore/aws-lambda-tools-defaults.json create mode 100644 src/Exceptionless/Queue/ProcessQueueScope.cs diff --git a/Exceptionless.Net.sln b/Exceptionless.Net.sln index 2cef4525..7d98e04f 100644 --- a/Exceptionless.Net.sln +++ b/Exceptionless.Net.sln @@ -76,6 +76,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleWpf", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.Extensions.Hosting", "src\Platforms\Exceptionless.Extensions.Hosting\Exceptionless.Extensions.Hosting.csproj", "{A5589072-EC59-4A6A-B78D-5D2ABB36DB1B}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleHosting", "samples\Exceptionless.SampleHosting\Exceptionless.SampleHosting.csproj", "{693ED127-0124-4697-8369-700717615E85}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleLambda", "samples\Exceptionless.SampleLambda\Exceptionless.SampleLambda.csproj", "{4B26BF7F-85FB-4B41-BF93-661E83A4EDD7}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptionless.SampleLambdaAspNetCore", "samples\Exceptionless.SampleLambdaAspNetCore\Exceptionless.SampleLambdaAspNetCore.csproj", "{D9987952-B891-48B4-AA93-59AA39F33FC4}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -174,6 +180,18 @@ Global {A5589072-EC59-4A6A-B78D-5D2ABB36DB1B}.Debug|Any CPU.Build.0 = Debug|Any CPU {A5589072-EC59-4A6A-B78D-5D2ABB36DB1B}.Release|Any CPU.ActiveCfg = Release|Any CPU {A5589072-EC59-4A6A-B78D-5D2ABB36DB1B}.Release|Any CPU.Build.0 = Release|Any CPU + {693ED127-0124-4697-8369-700717615E85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {693ED127-0124-4697-8369-700717615E85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {693ED127-0124-4697-8369-700717615E85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {693ED127-0124-4697-8369-700717615E85}.Release|Any CPU.Build.0 = Release|Any CPU + {4B26BF7F-85FB-4B41-BF93-661E83A4EDD7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4B26BF7F-85FB-4B41-BF93-661E83A4EDD7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4B26BF7F-85FB-4B41-BF93-661E83A4EDD7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4B26BF7F-85FB-4B41-BF93-661E83A4EDD7}.Release|Any CPU.Build.0 = Release|Any CPU + {D9987952-B891-48B4-AA93-59AA39F33FC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D9987952-B891-48B4-AA93-59AA39F33FC4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D9987952-B891-48B4-AA93-59AA39F33FC4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D9987952-B891-48B4-AA93-59AA39F33FC4}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -201,6 +219,9 @@ Global {E1D077DE-C62E-4137-B609-3E9845C8A027} = {2CEE12C6-3840-4C01-A952-D3026B0A662A} {AD95A70D-18DB-414E-9F6C-1B13B4FBA56E} = {2CEE12C6-3840-4C01-A952-D3026B0A662A} {A5589072-EC59-4A6A-B78D-5D2ABB36DB1B} = {D363E15F-621D-40E4-8C96-DEE41A7070FF} + {693ED127-0124-4697-8369-700717615E85} = {2CEE12C6-3840-4C01-A952-D3026B0A662A} + {4B26BF7F-85FB-4B41-BF93-661E83A4EDD7} = {2CEE12C6-3840-4C01-A952-D3026B0A662A} + {D9987952-B891-48B4-AA93-59AA39F33FC4} = {2CEE12C6-3840-4C01-A952-D3026B0A662A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EBB2CC85-FF87-431B-865F-2F110B2A10E6} diff --git a/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj b/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj index 071ba8c4..d0389c2f 100644 --- a/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj +++ b/samples/Exceptionless.SampleAspNetCore/Exceptionless.SampleAspNetCore.csproj @@ -5,9 +5,7 @@ - - \ No newline at end of file diff --git a/samples/Exceptionless.SampleAspNetCore/appsettings.json b/samples/Exceptionless.SampleAspNetCore/appsettings.json index 2ad13f2c..5f5bc011 100644 --- a/samples/Exceptionless.SampleAspNetCore/appsettings.json +++ b/samples/Exceptionless.SampleAspNetCore/appsettings.json @@ -1,7 +1,7 @@ { "Exceptionless": { "ApiKey": "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", - "ServerUrl": "http://localhost:50000", + "ServerUrl": "http://localhost:5000", "DefaultData": { "JSON_OBJECT": "{ \"Name\": \"Blake\" }", "Boolean": true, diff --git a/samples/Exceptionless.SampleConsole/Program.cs b/samples/Exceptionless.SampleConsole/Program.cs index 73220de1..b73d9704 100644 --- a/samples/Exceptionless.SampleConsole/Program.cs +++ b/samples/Exceptionless.SampleConsole/Program.cs @@ -22,7 +22,7 @@ using LogLevel = Exceptionless.Logging.LogLevel; // example of setting an attribute value in config. -[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:50000")] +[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:5000")] [assembly: ExceptionlessSetting("EnableWelcomeMessage", "True")] namespace Exceptionless.SampleConsole { diff --git a/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj b/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj new file mode 100644 index 00000000..46668907 --- /dev/null +++ b/samples/Exceptionless.SampleHosting/Exceptionless.SampleHosting.csproj @@ -0,0 +1,11 @@ + + + + netcoreapp3.1 + + + + + + + \ No newline at end of file diff --git a/samples/Exceptionless.SampleHosting/Program.cs b/samples/Exceptionless.SampleHosting/Program.cs new file mode 100644 index 00000000..157eaca2 --- /dev/null +++ b/samples/Exceptionless.SampleHosting/Program.cs @@ -0,0 +1,74 @@ +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Exceptionless.SampleHosting { + public class Program { + public static void Main(string[] args) { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureLogging(builder => { + // By default sends warning and error log messages to Exceptionless. + // Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. + builder.AddExceptionless(); + }) + .UseExceptionless() // listens for host shutdown and + .ConfigureServices(services => { + // Reads settings from IConfiguration then adds additional configuration from this lambda. + // This also configures ExceptionlessClient.Default + services.AddExceptionless(c => c.DefaultData["Startup"] = "heyyy"); + // OR + // services.AddExceptionless(); + // OR + // services.AddExceptionless("API_KEY_HERE"); + + // adds a hosted service that will send sample events to Exceptionless. + services.AddHostedService(); + }) + .UseConsoleLifetime() + .ConfigureWebHostDefaults(builder => { + builder.Configure(app => { + app.UseRouting(); + app.UseEndpoints(endpoints => { + endpoints.MapGet("/ping", context => { + var client = context.RequestServices.GetRequiredService(); + var logger = context.RequestServices.GetRequiredService>(); + + // Submit a feature usage event directly using the client instance. + client.SubmitFeatureUsage("MapGet_Ping"); + + // This log message will get sent to Exceptionless since Exceptionless has be added to the logging system in Program.cs. + logger.LogWarning("Test warning message from ping"); + + try { + throw new Exception($"Handled Exception: {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the client instance from DI. + handledException.ToExceptionless(client).Submit(); + } + + try { + throw new Exception($"Handled Exception (Default Client): {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the default client instance (ExceptionlessClient.Default). + // This works and is convenient, but its generally not recommended to use static singleton instances because it makes testing and + // other things harder. + handledException.ToExceptionless().Submit(); + } + + // Unhandled exceptions will get reported since called UseExceptionless in the Startup.cs which registers a listener for unhandled exceptions. + throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); + }); + }); + }); + }); + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleHosting/Properties/launchSettings.json b/samples/Exceptionless.SampleHosting/Properties/launchSettings.json new file mode 100644 index 00000000..b04492e1 --- /dev/null +++ b/samples/Exceptionless.SampleHosting/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Exceptionless.SampleAspNetCore": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/ping", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleHosting/SampleService.cs b/samples/Exceptionless.SampleHosting/SampleService.cs new file mode 100644 index 00000000..642ebd67 --- /dev/null +++ b/samples/Exceptionless.SampleHosting/SampleService.cs @@ -0,0 +1,52 @@ +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + +namespace Exceptionless.SampleHosting { + internal class SampleService : IHostedService { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly ILogger _logger; + + public SampleService(ExceptionlessClient exceptionlessClient, ILogger logger) { + _exceptionlessClient = exceptionlessClient; + _logger = logger; + } + + public Task StartAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Starting sample service."); + + // Submit a feature usage event directly using the client instance that is injected from the DI container. + _exceptionlessClient.SubmitFeatureUsage("SampleService"); + + // This log message will get sent to Exceptionless since Exceptionless has be added to the logging system in Program.cs. + _logger.LogWarning("Test warning message"); + + return Task.Run(() => { + try { + throw new Exception($"Handled Exception: {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the client instance from DI. + handledException.ToExceptionless(_exceptionlessClient).Submit(); + } + + try { + throw new Exception($"Handled Exception (Default Client): {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the default client instance (ExceptionlessClient.Default). + // This works and is convenient, but its generally not recommended to use static singleton instances because it makes testing and + // other things harder. + handledException.ToExceptionless().Submit(); + } + }, cancellationToken); + } + + public Task StopAsync(CancellationToken cancellationToken) { + _logger.LogInformation("Stopping sample service."); + return Task.CompletedTask; + } + } +} diff --git a/samples/Exceptionless.SampleHosting/appsettings.json b/samples/Exceptionless.SampleHosting/appsettings.json new file mode 100644 index 00000000..5f5bc011 --- /dev/null +++ b/samples/Exceptionless.SampleHosting/appsettings.json @@ -0,0 +1,24 @@ +{ + "Exceptionless": { + "ApiKey": "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", + "ServerUrl": "http://localhost:5000", + "DefaultData": { + "JSON_OBJECT": "{ \"Name\": \"Blake\" }", + "Boolean": true, + "Number": 1, + "Array": "1,2,3" + }, + "DefaultTags": [ "xplat" ], + "Settings": { + "FeatureXYZEnabled": false + } + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj b/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj new file mode 100644 index 00000000..1df9b8ae --- /dev/null +++ b/samples/Exceptionless.SampleLambda/Exceptionless.SampleLambda.csproj @@ -0,0 +1,19 @@ + + + netcoreapp3.1 + true + latest + Lambda + + + true + + + + + + + + + + \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambda/Function.cs b/samples/Exceptionless.SampleLambda/Function.cs new file mode 100644 index 00000000..4a79b348 --- /dev/null +++ b/samples/Exceptionless.SampleLambda/Function.cs @@ -0,0 +1,35 @@ +using System; +using Amazon.Lambda.Core; +using Exceptionless; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace Exceptionless.SampleLambda { + public class Function + { + public string FunctionHandler(string input, ILambdaContext context) + { + var client = new ExceptionlessClient(c => { + c.ApiKey = "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw"; + c.ServerUrl = "http://localhost:5000"; + + // read configuration values from environment variables + c.ReadFromEnvironmentalVariables(); + }); + + // will automatically trigger a client.ProcessQueue call when this method completes even if there is an unhandled exception + using var _ = client.ProcessQueueDeferred(); + + client.SubmitFeatureUsage("Serverless Function"); + + try { + throw new Exception("Lambda error"); + } catch (Exception ex) { + ex.ToExceptionless(client).Submit(); + } + + return input.ToLower(); + } + } +} diff --git a/samples/Exceptionless.SampleLambda/Readme.md b/samples/Exceptionless.SampleLambda/Readme.md new file mode 100644 index 00000000..abac0229 --- /dev/null +++ b/samples/Exceptionless.SampleLambda/Readme.md @@ -0,0 +1,49 @@ +# AWS Lambda Empty Function Project + +This starter project consists of: +* Function.cs - class file containing a class with a single function handler method +* aws-lambda-tools-defaults.json - default argument settings for use with Visual Studio and command line deployment tools for AWS + +You may also have a test project depending on the options selected. + +The generated function handler is a simple method accepting a string argument that returns the uppercase equivalent of the input string. Replace the body of this method, and parameters, to suit your needs. + +## Here are some steps to follow from Visual Studio: + +To deploy your function to AWS Lambda, right click the project in Solution Explorer and select *Publish to AWS Lambda*. + +To view your deployed function open its Function View window by double-clicking the function name shown beneath the AWS Lambda node in the AWS Explorer tree. + +To perform testing against your deployed function use the Test Invoke tab in the opened Function View window. + +To configure event sources for your deployed function, for example to have your function invoked when an object is created in an Amazon S3 bucket, use the Event Sources tab in the opened Function View window. + +To update the runtime configuration of your deployed function use the Configuration tab in the opened Function View window. + +To view execution logs of invocations of your function use the Logs tab in the opened Function View window. + +## Here are some steps to follow to get started from the command line: + +Once you have edited your template and code you can deploy your application using the [Amazon.Lambda.Tools Global Tool](https://github.com/aws/aws-extensions-for-dotnet-cli#aws-lambda-amazonlambdatools) from the command line. + +Install Amazon.Lambda.Tools Global Tools if not already installed. +``` + dotnet tool install -g Amazon.Lambda.Tools +``` + +If already installed check if new version is available. +``` + dotnet tool update -g Amazon.Lambda.Tools +``` + +Execute unit tests +``` + cd "BlueprintBaseName/test/BlueprintBaseName.Tests" + dotnet test +``` + +Deploy function to AWS Lambda +``` + cd "BlueprintBaseName/src/BlueprintBaseName" + dotnet lambda deploy-function +``` diff --git a/samples/Exceptionless.SampleLambda/aws-lambda-tools-defaults.json b/samples/Exceptionless.SampleLambda/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..fed85fff --- /dev/null +++ b/samples/Exceptionless.SampleLambda/aws-lambda-tools-defaults.json @@ -0,0 +1,16 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "framework": "netcoreapp3.1", + "function-runtime": "dotnetcore3.1", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "Exceptionless.SampleLambda::Exceptionless.SampleLambda.Function::FunctionHandler" +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Controllers/ValuesController.cs b/samples/Exceptionless.SampleLambdaAspNetCore/Controllers/ValuesController.cs new file mode 100644 index 00000000..c9633fc4 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Controllers/ValuesController.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; + +namespace Exceptionless.SampleAspNetCore.Controllers { + [Route("api/[controller]")] + public class ValuesController : Controller { + private readonly ExceptionlessClient _exceptionlessClient; + private readonly ILogger _logger; + + public ValuesController(ExceptionlessClient exceptionlessClient, ILogger logger) { + // ExceptionlessClient instance from DI that was registered with the AddExceptionless call in Startup.ConfigureServices + _exceptionlessClient = exceptionlessClient; + _logger = logger; + } + + // GET api/values + [HttpGet] + public Dictionary Get() { + // Submit a feature usage event directly using the client instance that is injected from the DI container. + _exceptionlessClient.SubmitFeatureUsage("ValuesController_Get"); + + // This log message will get sent to Exceptionless since Exceptionless has be added to the logging system in Program.cs. + _logger.LogWarning("Test warning message"); + + try { + throw new Exception($"Handled Exception: {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the client instance from DI. + handledException.ToExceptionless(_exceptionlessClient).Submit(); + } + + try { + throw new Exception($"Handled Exception (Default Client): {Guid.NewGuid()}"); + } + catch (Exception handledException) { + // Use the ToExceptionless extension method to submit this handled exception to Exceptionless using the default client instance (ExceptionlessClient.Default). + // This works and is convenient, but its generally not recommended to use static singleton instances because it makes testing and + // other things harder. + handledException.ToExceptionless().Submit(); + } + + // Unhandled exceptions will get reported since called UseExceptionless in the Startup.cs which registers a listener for unhandled exceptions. + throw new Exception($"Unhandled Exception: {Guid.NewGuid()}"); + } + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj b/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj new file mode 100644 index 00000000..11881a9b --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Exceptionless.SampleLambdaAspNetCore.csproj @@ -0,0 +1,15 @@ + + + netcoreapp3.1 + true + Lambda + + + + + + + + + + \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/LambdaEntryPoint.cs b/samples/Exceptionless.SampleLambdaAspNetCore/LambdaEntryPoint.cs new file mode 100644 index 00000000..ce4be663 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/LambdaEntryPoint.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +// The entrypoint used when the function is deployed to AWS. +namespace Exceptionless.SampleLambdaAspNetCore { + public class LambdaEntryPoint : Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction { + protected override void Init(IHostBuilder builder) { + builder.ConfigureLogging(b => { + // By default sends warning and error log messages to Exceptionless. + // Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. + b.AddExceptionless(); + }); + } + + protected override void Init(IWebHostBuilder builder) { + builder.UseStartup(); + } + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Program.cs b/samples/Exceptionless.SampleLambdaAspNetCore/Program.cs new file mode 100644 index 00000000..4428149e --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Program.cs @@ -0,0 +1,22 @@ +using System; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Hosting; + +namespace Exceptionless.SampleLambdaAspNetCore { + public class Program { + public static void Main(string[] args) { + CreateHostBuilder(args).Build().Run(); + } + + public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureLogging(b => { + // By default sends warning and error log messages to Exceptionless. + // Log levels can be controlled remotely per log source from the Exceptionless app in near real-time. + b.AddExceptionless(); + }) + .ConfigureWebHostDefaults(webBuilder => { + webBuilder.UseStartup(); + }); + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Properties/launchSettings.json b/samples/Exceptionless.SampleLambdaAspNetCore/Properties/launchSettings.json new file mode 100644 index 00000000..c9ebb6d1 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Exceptionless.SampleLambdaAspNetCore": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "http://localhost:5000/api/values", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/Startup.cs b/samples/Exceptionless.SampleLambdaAspNetCore/Startup.cs new file mode 100644 index 00000000..65939706 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/Startup.cs @@ -0,0 +1,43 @@ +using System; +using Exceptionless; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Exceptionless.SampleLambdaAspNetCore { + public class Startup { + public Startup(IConfiguration configuration) { + Configuration = configuration; + } + + public IConfiguration Configuration { get; } + + public void ConfigureServices(IServiceCollection services) { + // Reads settings from IConfiguration then adds additional configuration from this lambda. + // This also configures ExceptionlessClient.Default + services.AddExceptionless(c => c.DefaultData["Startup"] = "heyyy"); + // OR + // services.AddExceptionless(); + // OR + // services.AddExceptionless("API_KEY_HERE"); + + // This enables Exceptionless to gather more detailed information about unhandled exceptions and other events + services.AddHttpContextAccessor(); + + // This is normal ASP.NET code + services.AddControllers(); + } + + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { + // Adds Exceptionless middleware to listen for unhandled exceptions + app.UseExceptionless(); + + // This is normal ASP.NET code + app.UseRouting(); + app.UseEndpoints(endpoints => { + endpoints.MapControllers(); + }); + } + } +} \ No newline at end of file diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/appsettings.json b/samples/Exceptionless.SampleLambdaAspNetCore/appsettings.json new file mode 100644 index 00000000..bcb92628 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/appsettings.json @@ -0,0 +1,28 @@ +{ + "Exceptionless": { + "ApiKey": "LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", + "ServerUrl": "http://localhost:5000", + "ProcessQueueOnCompletedRequest": true, + "DefaultData": { + "JSON_OBJECT": "{ \"Name\": \"Blake\" }", + "Boolean": true, + "Number": 1, + "Array": "1,2,3" + }, + "DefaultTags": [ "xplat" ], + "Settings": { + "FeatureXYZEnabled": false + } + }, + "AWS": { + "Region": "" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/samples/Exceptionless.SampleLambdaAspNetCore/aws-lambda-tools-defaults.json b/samples/Exceptionless.SampleLambdaAspNetCore/aws-lambda-tools-defaults.json new file mode 100644 index 00000000..38116186 --- /dev/null +++ b/samples/Exceptionless.SampleLambdaAspNetCore/aws-lambda-tools-defaults.json @@ -0,0 +1,18 @@ +{ + "Information": [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile": "", + "region": "", + "configuration": "Release", + "framework": "netcoreapp3.1", + "function-runtime": "dotnetcore3.1", + "function-memory-size": 256, + "function-timeout": 30, + "function-handler": "Exceptionless.SampleLambdaAspNetCore::Exceptionless.SampleLambdaAspNetCore.LambdaEntryPoint::FunctionHandlerAsync", + "template": "serverless.template", + "template-parameters": "" +} diff --git a/samples/Exceptionless.SampleMvc/Web.config b/samples/Exceptionless.SampleMvc/Web.config index 01a56803..8d91eaa1 100644 --- a/samples/Exceptionless.SampleMvc/Web.config +++ b/samples/Exceptionless.SampleMvc/Web.config @@ -2,7 +2,7 @@ - + diff --git a/samples/Exceptionless.SampleWcf/Web.config b/samples/Exceptionless.SampleWcf/Web.config index e4d56a64..a5f2cbb3 100644 --- a/samples/Exceptionless.SampleWcf/Web.config +++ b/samples/Exceptionless.SampleWcf/Web.config @@ -4,7 +4,7 @@
- + diff --git a/samples/Exceptionless.SampleWeb/Web.config b/samples/Exceptionless.SampleWeb/Web.config index a756765b..f1de9fe7 100644 --- a/samples/Exceptionless.SampleWeb/Web.config +++ b/samples/Exceptionless.SampleWeb/Web.config @@ -3,8 +3,8 @@
- - diff --git a/samples/Exceptionless.SampleWebApi/App.config b/samples/Exceptionless.SampleWebApi/App.config index 07b32c56..b97dc33f 100644 --- a/samples/Exceptionless.SampleWebApi/App.config +++ b/samples/Exceptionless.SampleWebApi/App.config @@ -3,7 +3,7 @@
- + diff --git a/samples/Exceptionless.SampleWindows/App.config b/samples/Exceptionless.SampleWindows/App.config index 85f64e08..1c1b2a74 100644 --- a/samples/Exceptionless.SampleWindows/App.config +++ b/samples/Exceptionless.SampleWindows/App.config @@ -4,7 +4,7 @@
- + diff --git a/samples/Exceptionless.SampleWindows/Program.cs b/samples/Exceptionless.SampleWindows/Program.cs index d014565a..c0e51c22 100644 --- a/samples/Exceptionless.SampleWindows/Program.cs +++ b/samples/Exceptionless.SampleWindows/Program.cs @@ -3,7 +3,7 @@ using System.Windows.Forms; using Exceptionless.Configuration; -[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:50000")] +[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:5000")] namespace Exceptionless.SampleWindows { internal static class Program { diff --git a/samples/Exceptionless.SampleWpf/Properties/AssemblyInfo.cs b/samples/Exceptionless.SampleWpf/Properties/AssemblyInfo.cs index a60b65fd..f6f4c6cb 100644 --- a/samples/Exceptionless.SampleWpf/Properties/AssemblyInfo.cs +++ b/samples/Exceptionless.SampleWpf/Properties/AssemblyInfo.cs @@ -3,4 +3,4 @@ using Exceptionless.Configuration; [assembly: ThemeInfo(ResourceDictionaryLocation.None, ResourceDictionaryLocation.SourceAssembly)] -[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:50000")] \ No newline at end of file +[assembly: Exceptionless("LhhP1C9gijpSKCslHHCvwdSIz298twx271n1l6xw", ServerUrl = "http://localhost:5000")] \ No newline at end of file diff --git a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs index f8ee163e..5617a66f 100644 --- a/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs +++ b/src/Exceptionless/Configuration/ExceptionlessConfiguration.cs @@ -267,6 +267,12 @@ public bool IncludePrivateInformation { /// public bool SessionsEnabled { get; set; } + /// + /// Gets or sets a value indicating whether to automatically process the pending event queue after each request is completed. + /// NOTE: Only supported on AspNetCore currently. + /// + public bool ProcessQueueOnCompletedRequest { get; set; } + internal string CurrentSessionIdentifier { get; set; } /// diff --git a/src/Exceptionless/ExceptionlessClient.cs b/src/Exceptionless/ExceptionlessClient.cs index 1ebeb894..8786d8d5 100644 --- a/src/Exceptionless/ExceptionlessClient.cs +++ b/src/Exceptionless/ExceptionlessClient.cs @@ -153,6 +153,15 @@ public void ProcessQueue() { ProcessQueueAsync().ConfigureAwait(false).GetAwaiter().GetResult(); } + /// + /// Gets a disposable object that when disposed will trigger the client queue to be processed. + /// using var _ = client.ProcessQueueDeferred(); + /// + /// An that when disposed will trigger the client queue to be processed. + public IDisposable ProcessQueueDeferred() { + return new ProcessQueueScope(this); + } + /// /// Submits the event to be sent to the server. /// diff --git a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs index 558e52ab..3aac81da 100644 --- a/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs +++ b/src/Exceptionless/Extensions/ExceptionlessConfigurationExtensions.cs @@ -403,6 +403,25 @@ public static void ReadFromEnvironmentalVariables(this ExceptionlessConfiguratio if (Boolean.TryParse(GetEnvironmentalVariable("Exceptionless:Enabled") ?? GetEnvironmentalVariable("Exceptionless__Enabled"), out enabled) && !enabled) config.Enabled = false; + bool processQueueOnCompletedRequest; + string processQueueOnCompletedRequestValue = GetEnvironmentalVariable("Exceptionless:ProcessQueueOnCompletedRequest") ?? + GetEnvironmentalVariable("Exceptionless__ProcessQueueOnCompletedRequest"); + + // if we are running in a serverless environment default this config to true + if (String.IsNullOrEmpty(processQueueOnCompletedRequestValue)) { + + // check for AWS lambda environment + if (!String.IsNullOrEmpty(GetEnvironmentalVariable("AWS_LAMBDA_FUNCTION_NAME "))) + processQueueOnCompletedRequestValue = Boolean.TrueString; + + // check for azure functions environment + if (!String.IsNullOrEmpty(GetEnvironmentalVariable("FUNCTIONS_WORKER_RUNTIME"))) + processQueueOnCompletedRequestValue = Boolean.TrueString; + } + + if (Boolean.TryParse(processQueueOnCompletedRequestValue, out processQueueOnCompletedRequest) && processQueueOnCompletedRequest) + config.ProcessQueueOnCompletedRequest = true; + string serverUrl = GetEnvironmentalVariable("Exceptionless:ServerUrl") ?? GetEnvironmentalVariable("Exceptionless__ServerUrl"); if (!String.IsNullOrEmpty(serverUrl)) config.ServerUrl = serverUrl; diff --git a/src/Exceptionless/Queue/ProcessQueueScope.cs b/src/Exceptionless/Queue/ProcessQueueScope.cs new file mode 100644 index 00000000..ba226fa7 --- /dev/null +++ b/src/Exceptionless/Queue/ProcessQueueScope.cs @@ -0,0 +1,15 @@ +using System; + +namespace Exceptionless.Queue { + internal class ProcessQueueScope : IDisposable { + private readonly ExceptionlessClient _exceptionlessClient; + + public ProcessQueueScope(ExceptionlessClient exceptionlessClient) { + _exceptionlessClient = exceptionlessClient; + } + + public void Dispose() { + _exceptionlessClient.ProcessQueue(); + } + } +} diff --git a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs index 73dd6a8b..79f72170 100644 --- a/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs +++ b/src/Platforms/Exceptionless.AspNetCore/ExceptionlessMiddleware.cs @@ -14,6 +14,12 @@ public ExceptionlessMiddleware(RequestDelegate next, ExceptionlessClient client) } public async Task Invoke(HttpContext context) { + if (_client.Configuration.ProcessQueueOnCompletedRequest) { + context.Response.OnCompleted(async () => { + await _client.ProcessQueueAsync(); + }); + } + try { await _next(context); } catch (Exception ex) { diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs index 110d2ba2..7c98be28 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessExtensions.cs @@ -64,6 +64,26 @@ public static IServiceCollection AddExceptionless(this IServiceCollection servic }); } + /// + /// Adds an instance to the services collection as a singleton. + /// + /// The to add the instance to as a singleton. + /// The to use to configure the instance. + /// Allows altering the configuration. + /// + public static IServiceCollection AddExceptionless(this IServiceCollection services, IConfiguration configuration, Action configure = null) { + return services.AddSingleton(sp => { + var client = ExceptionlessClient.Default; + + if (configuration != null) + client.Configuration.ReadFromConfiguration(configuration); + + configure?.Invoke(client.Configuration); + + return client; + }); + } + /// /// Sets the configuration from .net configuration settings. /// @@ -122,7 +142,10 @@ public static void ReadFromConfiguration(this ExceptionlessConfiguration config, if (Boolean.TryParse(section["IncludePrivateInformation"], out bool includePrivateInformation) && !includePrivateInformation) config.IncludePrivateInformation = false; - + + if (Boolean.TryParse(section["ProcessQueueOnCompletedRequest"], out bool processQueueOnCompletedRequest) && processQueueOnCompletedRequest) + config.ProcessQueueOnCompletedRequest = true; + foreach (var tag in section.GetSection("DefaultTags").GetChildren()) config.DefaultTags.Add(tag.Value); diff --git a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs index 4f8e1b2e..8803d4d7 100644 --- a/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs +++ b/src/Platforms/Exceptionless.Extensions.Hosting/ExceptionlessLifetimeService.cs @@ -8,6 +8,9 @@ public class ExceptionlessLifetimeService : IHostedService { public ExceptionlessLifetimeService(ExceptionlessClient client, IHostApplicationLifetime appLifetime) { _exceptionlessClient = client; + + _exceptionlessClient.RegisterAppDomainUnhandledExceptionHandler(); + _exceptionlessClient.RegisterTaskSchedulerUnobservedTaskExceptionHandler(); appLifetime.ApplicationStopping.Register(() => _exceptionlessClient.ProcessQueue()); } diff --git a/test/Exceptionless.Tests/app.config b/test/Exceptionless.Tests/app.config index d12eefb6..9774b00d 100644 --- a/test/Exceptionless.Tests/app.config +++ b/test/Exceptionless.Tests/app.config @@ -4,7 +4,7 @@
- +