From 1651da8f82e10a9d7ddd7f186af02dad1cefafd5 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Mon, 8 Apr 2024 11:11:18 +0100 Subject: [PATCH] Add `IServiceCollection` extension methods to register ApmAgent (#2326) * Add IServiceCollection extension methods to register ApmAgent * Reverting updates to sample apps * Update doc * Remove unused NET5_0 compiler directives --- ElasticApmAgent.sln | 25 +- .../AspNetCorePerf/AspNetCoreSampleRunner.cs | 6 +- docs/setup-asp-net-core.asciidoc | 6 +- docs/setup-dotnet-net-core.asciidoc | 160 +++++++---- docs/setup.asciidoc | 1 - .../Controllers/WeatherForecastController.cs | 10 +- sample/WebApiExample/Program.cs | 10 +- sample/WebApiExample/WebApiExample.csproj | 6 +- .../appsettings.Development.json | 8 + sample/WebApiExample/appsettings.json | 12 + sample/WorkerServiceSample/Program.cs | 11 + sample/WorkerServiceSample/Worker.cs | 23 ++ .../WorkerServiceSample.csproj | 17 ++ .../appsettings.Development.json | 8 + sample/WorkerServiceSample/appsettings.json | 12 + src/Elastic.Apm/ApmAgentExtensions.cs | 2 +- src/Elastic.Apm/Elastic.Apm.csproj | 1 + .../ApplicationBuilderExtensions.cs | 12 +- .../ServiceCollectionExtensions.cs | 38 +++ .../ApmService.cs | 25 ++ .../Elastic.Apm.Extensions.Hosting.csproj | 4 +- .../HostBuilderExtensions.cs | 8 +- .../NetCoreLogger.cs | 58 ++-- .../ServiceCollectionExtensions.cs | 121 ++++++++ .../ApmErrorLoggingProvider.cs | 3 +- .../ApplicationBuilderExtensions.cs | 3 + .../Elastic.Apm.NetCoreAll.csproj | 2 +- .../HostBuilderExtensions.cs | 3 + .../ServiceCollectionExtensions.cs | 80 ++++++ .../Elastic.Apm.Tests.Utilities.csproj | 1 + .../MockPayloadSender.cs | 28 +- .../grpc/GrpcServiceSample/Program.cs | 4 +- .../grpc/GrpcServiceSample/Startup.cs | 2 +- .../ApplicationBuilderExtensionLoggingTest.cs | 4 +- .../AspNetCoreBasicTests.cs | 4 +- .../DistributedTracingAspNetCoreTests.cs | 6 +- ...lastic.Apm.Extensions.Hosting.Tests.csproj | 11 +- .../HostBuilderExtensionTests.cs | 114 -------- .../HostingTests.cs | 73 +++++ .../CaptureApmErrorsTests.cs | 81 ++---- ...lastic.Apm.Extensions.Logging.Tests.csproj | 11 +- ...Elastic.Apm.Extensions.Tests.Shared.csproj | 13 + .../ExtensionsTestHelpers.cs | 98 +++++++ .../HostingTestApp/HostingTestApp.csproj | 20 ++ .../applications/HostingTestApp/Program.cs | 263 ++++++++++++++++++ .../HostingTestApp/appsettings.json | 10 + .../SampleAspNetCoreApp/Startup.cs | 30 +- .../SampleAspNetCoreApp/Utils/CpuBurner.cs | 3 +- .../SampleConsoleNetCoreApp/HostedService.cs | 7 +- .../SampleConsoleNetCoreApp/Program.cs | 10 +- .../applications/WebApiSample/Startup.cs | 11 +- 51 files changed, 1084 insertions(+), 395 deletions(-) create mode 100644 sample/WebApiExample/appsettings.Development.json create mode 100644 sample/WebApiExample/appsettings.json create mode 100644 sample/WorkerServiceSample/Program.cs create mode 100644 sample/WorkerServiceSample/Worker.cs create mode 100644 sample/WorkerServiceSample/WorkerServiceSample.csproj create mode 100644 sample/WorkerServiceSample/appsettings.Development.json create mode 100644 sample/WorkerServiceSample/appsettings.json create mode 100644 src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs create mode 100644 src/integrations/Elastic.Apm.Extensions.Hosting/ApmService.cs create mode 100644 src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs create mode 100644 src/integrations/Elastic.Apm.NetCoreAll/ServiceCollectionExtensions.cs delete mode 100644 test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostBuilderExtensionTests.cs create mode 100644 test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostingTests.cs create mode 100644 test/integrations/Elastic.Apm.Extensions.Tests.Shared/Elastic.Apm.Extensions.Tests.Shared.csproj create mode 100644 test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs create mode 100644 test/integrations/applications/HostingTestApp/HostingTestApp.csproj create mode 100644 test/integrations/applications/HostingTestApp/Program.cs create mode 100644 test/integrations/applications/HostingTestApp/appsettings.json diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index aa8ce27e2..65142c8b8 100644 --- a/ElasticApmAgent.sln +++ b/ElasticApmAgent.sln @@ -229,9 +229,15 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "applications", "application EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.AzureFunctionApp.Core", "test\azure\applications\Elastic.Apm.AzureFunctionApp.Core\Elastic.Apm.AzureFunctionApp.Core.csproj", "{50F14EA5-DF72-425B-81A6-C7D532D2DD07}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Profiling", "benchmarks\Elastic.Apm.Profiling\Elastic.Apm.Profiling.csproj", "{CB6B3BA6-9D16-4CDC-95C2-7680CF50747D}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Elastic.Apm.Profiling", "benchmarks\Elastic.Apm.Profiling\Elastic.Apm.Profiling.csproj", "{CB6B3BA6-9D16-4CDC-95C2-7680CF50747D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WebApiExample", "sample\WebApiExample\WebApiExample.csproj", "{00A025F1-0A31-4676-AA06-1773FC9744ED}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApiExample", "sample\WebApiExample\WebApiExample.csproj", "{00A025F1-0A31-4676-AA06-1773FC9744ED}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkerServiceSample", "sample\WorkerServiceSample\WorkerServiceSample.csproj", "{C73BF86B-5359-4811-B698-8BE9A66C5EFA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HostingTestApp", "test\integrations\applications\HostingTestApp\HostingTestApp.csproj", "{61F5A733-DC79-44FC-B9CB-4EF34FC9D96B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.Apm.Extensions.Tests.Shared", "test\integrations\Elastic.Apm.Extensions.Tests.Shared\Elastic.Apm.Extensions.Tests.Shared.csproj", "{7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -581,6 +587,18 @@ Global {00A025F1-0A31-4676-AA06-1773FC9744ED}.Debug|Any CPU.Build.0 = Debug|Any CPU {00A025F1-0A31-4676-AA06-1773FC9744ED}.Release|Any CPU.ActiveCfg = Release|Any CPU {00A025F1-0A31-4676-AA06-1773FC9744ED}.Release|Any CPU.Build.0 = Release|Any CPU + {C73BF86B-5359-4811-B698-8BE9A66C5EFA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C73BF86B-5359-4811-B698-8BE9A66C5EFA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C73BF86B-5359-4811-B698-8BE9A66C5EFA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C73BF86B-5359-4811-B698-8BE9A66C5EFA}.Release|Any CPU.Build.0 = Release|Any CPU + {61F5A733-DC79-44FC-B9CB-4EF34FC9D96B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61F5A733-DC79-44FC-B9CB-4EF34FC9D96B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61F5A733-DC79-44FC-B9CB-4EF34FC9D96B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61F5A733-DC79-44FC-B9CB-4EF34FC9D96B}.Release|Any CPU.Build.0 = Release|Any CPU + {7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -684,6 +702,9 @@ Global {50F14EA5-DF72-425B-81A6-C7D532D2DD07} = {09CE5AC1-01F6-48C8-B266-2F891C408051} {CB6B3BA6-9D16-4CDC-95C2-7680CF50747D} = {2825A761-5372-4620-99AB-253AD953E8CD} {00A025F1-0A31-4676-AA06-1773FC9744ED} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {C73BF86B-5359-4811-B698-8BE9A66C5EFA} = {3C791D9C-6F19-4F46-B367-2EC0F818762D} + {61F5A733-DC79-44FC-B9CB-4EF34FC9D96B} = {59F3FB6E-4B48-4E87-AF3B-78DFED427EF1} + {7482D2D9-BBF7-4C8B-B26C-BEA9BCF345B0} = {67A4BFE3-F96E-4BDE-9383-DD0ACE6D3BA1} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {69E02FD9-C9DE-412C-AB6B-5B8BECC6BFA5} diff --git a/benchmarks/Elastic.Apm.Benchmarks/AspNetCorePerf/AspNetCoreSampleRunner.cs b/benchmarks/Elastic.Apm.Benchmarks/AspNetCorePerf/AspNetCoreSampleRunner.cs index d47924d1f..4dced3465 100644 --- a/benchmarks/Elastic.Apm.Benchmarks/AspNetCorePerf/AspNetCoreSampleRunner.cs +++ b/benchmarks/Elastic.Apm.Benchmarks/AspNetCorePerf/AspNetCoreSampleRunner.cs @@ -5,7 +5,6 @@ using System; using System.Reflection; using System.Threading; -using Elastic.Apm.AspNetCore; using Elastic.Apm.DiagnosticSource; using Elastic.Apm.EntityFrameworkCore; using Microsoft.AspNetCore.Hosting; @@ -25,6 +24,7 @@ public void StartSampleAppWithAgent(bool withAgent, string url) => Startup.ConfigureServicesExceptMvc(services); services + .AddElasticApm(new HttpDiagnosticsSubscriber(), new EfCoreDiagnosticsSubscriber()) .AddMvc() .AddApplicationPart(Assembly.Load(new AssemblyName(nameof(SampleAspNetCoreApp)))); } @@ -34,10 +34,6 @@ public void StartSampleAppWithAgent(bool withAgent, string url) => if (withAgent) { Environment.SetEnvironmentVariable("ELASTIC_APM_FLUSH_INTERVAL", "0"); - app.UseElasticApm(subscribers: new IDiagnosticsSubscriber[] - { - new HttpDiagnosticsSubscriber(), new EfCoreDiagnosticsSubscriber() - }); } Startup.ConfigureAllExceptAgent(app); diff --git a/docs/setup-asp-net-core.asciidoc b/docs/setup-asp-net-core.asciidoc index 83d6a16f3..9008a84b1 100644 --- a/docs/setup-asp-net-core.asciidoc +++ b/docs/setup-asp-net-core.asciidoc @@ -7,10 +7,10 @@ [float] ==== Quick start -[NOTE] +[IMPORTANT] -- -We suggest using the approach described in the <>, -to register the agent on `IHostBuilder`, as opposed to using `IApplicationBuilder` as described below. +We strongly suggest using the approach described in the <>, +to register the agent on the `IServiceCollection`, as opposed to using `IApplicationBuilder` as described below. We keep the `IApplicationBuilder` introduced here only for backwards compatibility. -- diff --git a/docs/setup-dotnet-net-core.asciidoc b/docs/setup-dotnet-net-core.asciidoc index 7a361a097..1fd52590f 100644 --- a/docs/setup-dotnet-net-core.asciidoc +++ b/docs/setup-dotnet-net-core.asciidoc @@ -2,98 +2,156 @@ :dot: . [[setup-dotnet-net-core]] -=== .NET Core +=== .NET Core and .NET 5+ [float] ==== Quick start -On .NET Core, the agent can be registered on the `IHostBuilder`. This applies to both ASP.NET Core and to other .NET Core applications that depend on `IHostBuilder`, like https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services[background tasks]. In this case, you need to reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package. +In .NET (Core) applications using `Microsoft.Extensions.Hosting`, the agent can be registered on the `IServiceCollection`. This applies to ASP.NET Core and to other .NET applications that depend on the hosting APIs, such as those created using the https://learn.microsoft.com/en-us/dotnet/core/extensions/workers[worker services] template. +The simplest way to enable the agent and its instrumentations requires a reference to the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package. +[source,xml] +---- + <1> +---- +<1> Replace the `` placeholder with the latest version of the agent available on NuGet. + +[NOTE] +-- +The following code sample assumes the instrumentation of a .NET 8 worker service, using https://learn.microsoft.com/en-us/dotnet/csharp/tutorials/top-level-statements[top-level statements]. +-- + +*Program.cs* +[source,csharp] +---- +using WorkerServiceSample; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddHttpClient(); +builder.Services.AddAllElasticApm(); <1> +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); +---- +<1> Register Elastic APM before registering other IHostedServices to ensure its dependencies are initialized first. + +When registering services with `AddAllElasticApm()`, an APM agent with all instrumentations is enabled. On ASP.NET Core, it'll automatically capture incoming requests, database calls through supported technologies, outgoing HTTP requests, etc. + +For other application templates, such as worker services, you must manually instrument your `BackgroundService` to identify one or more units of work that should be captured. + +[float] +==== Manual instrumentation using `ITracer` + +`AddAllElasticApm` adds an `ITracer` to the Dependency Injection system, which can be used in your code to manually instrument your application, using the <> + +*Worker.cs* [source,csharp] ---- -using Elastic.Apm.NetCoreAll; +using Elastic.Apm.Api; -namespace MyApplication +namespace WorkerServiceSample { - public class Program + public class Worker : BackgroundService { - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseAllElasticApm(); + private readonly IHttpClientFactory _httpClientFactory; + private readonly ITracer _tracer; - public static void Main(string[] args) => CreateHostBuilder(args).Build().Run(); + public Worker(IHttpClientFactory httpClientFactory, ITracer tracer) + { + _httpClientFactory = httpClientFactory; + _tracer = tracer; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + await _tracer.CaptureTransaction("UnitOfWork", ApiConstants.TypeApp, async () => <1> + { + var client = _httpClientFactory.CreateClient(); + await client.GetAsync("https://www.elastic.co", stoppingToken); + await Task.Delay(5000, stoppingToken); + }); + } + } } } ---- +<1> The `CaptureTransaction` method creates a transaction named 'UnitOfWork' and type 'App'. The lambda passed to it represents the unit of work that should be captured within the context of the transaction. -With the `UseAllElasticApm()`, the agent with all its components is turned on. On ASP.NET Core, it'll automatically capture incoming requests, database calls through supported technologies, outgoing HTTP requests, and so on. +When this application runs, a new transaction will be captured and sent for each while loop iteration. A span named 'HTTP GET' within the transaction will be created for the HTTP request to `https://www.elastic.co`. The HTTP span is captured because the NetCoreAll package enables this instrumentation automatically. [float] -==== Manual instrumentation +==== Manual instrumentation using OpenTelemetry + +As an alternative to using the Elastic APM API by injecting an `ITracer`, you can use the OpenTelemetry API to manually instrument your application. The Elastic APM agent automatically bridges instrumentations created using the OpenTelemetry API, so you can use it to create spans and transactions. In .NET, the https://learn.microsoft.com/en-us/dotnet/core/diagnostics/distributed-tracing-instrumentation-walkthroughs[`Activity` API] can be used to instrument applications. -The `UseAllElasticApm` will add an `ITracer` to the Dependency Injection system, which can be used in your code to manually instrument your application, using the <> +In the case of this sample worker service, we can update the code to prefer the OpenTelemetry API. +*Worker.cs* [source,csharp] ---- -using Elastic.Apm.Api; +using System.Diagnostics; -namespace WebApplication.Controllers +namespace WorkerServiceSample { - public class HomeController : Controller - { - private readonly ITracer _tracer; + public class Worker : BackgroundService + { + private readonly IHttpClientFactory _httpClientFactory; + private static readonly ActivitySource ActivitySource = new("MyActivitySource"); <1> - //ITracer injected through Dependency Injection - public HomeController(ITracer tracer) => _tracer = tracer; + public Worker(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } - public IActionResult Index() - { - //use ITracer - var span = _tracer.CurrentTransaction?.StartSpan("MySampleSpan", "Sample"); - try - { - //your code here - } - catch (Exception e) - { - span?.CaptureException(e); - throw; - } - finally - { - span?.End(); - } - return View(); - } + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + using var activity = ActivitySource.StartActivity("UnitOfWork"); <2> + var client = _httpClientFactory.CreateClient(); + await client.GetAsync("https://www.elastic.co", stoppingToken); + await Task.Delay(5000, stoppingToken); + } } + } } ---- - -Similarly to this ASP.NET Core controller, you can use the same approach with `IHostedService` implementations. +<1> Defines an `ActivitySource` for this application from which activities can be created. +<2> Starts an `Activity` with the name `UnitOfWork`. As this is `IDisposable`, it will automatically end when each iteration of the `while` block ends. [float] ==== Instrumentation modules -The `Elastic.Apm.NetCoreAll` package references every agent component that can be automatically configured. This is usually not a problem, but if you want to keep dependencies minimal, you can instead reference the `Elastic.Apm.Extensions.Hosting` package and use the `UseElasticApm` method, instead of `UseAllElasticApm`. With this setup you can control what the agent will listen for. +The `Elastic.Apm.NetCoreAll` package references every agent component that can be automatically configured. This is usually not a problem, but if you want to keep dependencies minimal, you can instead reference the `Elastic.Apm.Extensions.Hosting` package and register services with `AddElasticApm` method, instead of `AddAllElasticApm`. With this setup you can explicitly control what the agent will listen for. -The following example only turns on outgoing HTTP monitoring (so, for instance, database or Elasticsearch calls won't be automatically captured): +The following example only turns on outgoing HTTP monitoring (so, for instance, database and Elasticsearch calls won't be automatically captured): [source,csharp] ---- -public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseElasticApm(new HttpDiagnosticsSubscriber()); ----- +using Elastic.Apm.DiagnosticSource; +using WorkerServiceSample; + +var builder = Host.CreateApplicationBuilder(args); +builder.Services.AddHttpClient(); +builder.Services.AddElasticApm(new HttpDiagnosticsSubscriber()); <1> +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); +---- +<1> The `HttpDiagnosticsSubscriber` is a diagnostic listener that captures spans for outgoing HTTP requests. [float] [[zero-code-change-setup]] ==== Zero code change setup on .NET Core and .NET 5+ (added[1.7]) -If you can't or don't want to reference NuGet packages in your application, you can use the startup hook feature to inject the agent during startup, if your application runs on .NET Core 3.0 or .NET 5 or newer. +If you can't or don't want to reference NuGet packages in your application, you can use the startup hook feature to inject the agent during startup, if your application runs on .NET Core 3.0, .NET Core 3.1 or .NET 5 or newer. To configure startup hooks @@ -109,9 +167,9 @@ set DOTNET_STARTUP_HOOKS=\ElasticApmAgentStartupHook.dll <1> . Start your .NET Core application in a context where the `DOTNET_STARTUP_HOOKS` environment variable is visible. -With this setup the agent will be injected into the application during startup and it will start every auto instrumentation feature. On ASP.NET Core (including gRPC), incoming requests will be automatically captured. +With this setup, the agent will be injected into the application during startup, enabling every instrumentation feature. Incoming requests will be automatically captured on ASP.NET Core (including gRPC). [NOTE] -- -Agent configuration can be controlled through environment variables with the startup hook feature. +Agent configuration can be controlled through environment variables when using the startup hook feature. -- \ No newline at end of file diff --git a/docs/setup.asciidoc b/docs/setup.asciidoc index d89e8f8d6..09818553c 100644 --- a/docs/setup.asciidoc +++ b/docs/setup.asciidoc @@ -20,7 +20,6 @@ On **.NET Core 3.0+ or .NET 5+**, the agent supports auto instrumentation withou any recompilation of your projects. See <> for more details. - [float] == Get started diff --git a/sample/WebApiExample/Controllers/WeatherForecastController.cs b/sample/WebApiExample/Controllers/WeatherForecastController.cs index 3aafe321d..608694903 100644 --- a/sample/WebApiExample/Controllers/WeatherForecastController.cs +++ b/sample/WebApiExample/Controllers/WeatherForecastController.cs @@ -6,14 +6,10 @@ namespace WebApiExample.Controllers; [Route("[controller]")] public class WeatherForecastController : ControllerBase { - private static readonly string[] Summaries = new[] - { + private static readonly string[] Summaries = + [ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" - }; - - private readonly ILogger _logger; - - public WeatherForecastController(ILogger logger) => _logger = logger; + ]; [HttpGet(Name = "GetWeatherForecast")] public IEnumerable Get() => diff --git a/sample/WebApiExample/Program.cs b/sample/WebApiExample/Program.cs index 757c87055..fc7700b3e 100644 --- a/sample/WebApiExample/Program.cs +++ b/sample/WebApiExample/Program.cs @@ -1,18 +1,12 @@ -using Elastic.Apm.AspNetCore; - var builder = WebApplication.CreateBuilder(args); -// Add services to the container. - +builder.Services.AddAllElasticApm(); builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); -app.UseElasticApm(app.Configuration); -// Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); @@ -20,9 +14,7 @@ } app.UseHttpsRedirection(); - app.UseAuthorization(); - app.MapControllers(); app.Run(); diff --git a/sample/WebApiExample/WebApiExample.csproj b/sample/WebApiExample/WebApiExample.csproj index 9af40e990..325a13544 100644 --- a/sample/WebApiExample/WebApiExample.csproj +++ b/sample/WebApiExample/WebApiExample.csproj @@ -7,12 +7,12 @@ - - + + - + diff --git a/sample/WebApiExample/appsettings.Development.json b/sample/WebApiExample/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/sample/WebApiExample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/sample/WebApiExample/appsettings.json b/sample/WebApiExample/appsettings.json new file mode 100644 index 000000000..451014b32 --- /dev/null +++ b/sample/WebApiExample/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Elastic.Apm": "Information" + } + }, + "ElasticApm": { + "ServiceName": "WebApiSample" + } +} diff --git a/sample/WorkerServiceSample/Program.cs b/sample/WorkerServiceSample/Program.cs new file mode 100644 index 000000000..44a68e11e --- /dev/null +++ b/sample/WorkerServiceSample/Program.cs @@ -0,0 +1,11 @@ +using Elastic.Apm.DiagnosticSource; +using WorkerServiceSample; + +var builder = Host.CreateApplicationBuilder(args); + +builder.Services.AddHttpClient(); +builder.Services.AddElasticApm(new HttpDiagnosticsSubscriber()); // register Elastic APM before registering other IHostedServices +builder.Services.AddHostedService(); + +var host = builder.Build(); +host.Run(); diff --git a/sample/WorkerServiceSample/Worker.cs b/sample/WorkerServiceSample/Worker.cs new file mode 100644 index 000000000..c5c025358 --- /dev/null +++ b/sample/WorkerServiceSample/Worker.cs @@ -0,0 +1,23 @@ +using System.Diagnostics; + +namespace WorkerServiceSample +{ + public class Worker : BackgroundService + { + private readonly IHttpClientFactory _httpClientFactory; + private static readonly ActivitySource ActivitySource = new("MyActivitySource"); + + public Worker(IHttpClientFactory httpClientFactory) => _httpClientFactory = httpClientFactory; + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + while (!stoppingToken.IsCancellationRequested) + { + using var activity = ActivitySource.StartActivity("UnitOfWork"); + var client = _httpClientFactory.CreateClient(); + await client.GetAsync("https://www.elastic.co", stoppingToken); + await Task.Delay(5000, stoppingToken); + } + } + } +} diff --git a/sample/WorkerServiceSample/WorkerServiceSample.csproj b/sample/WorkerServiceSample/WorkerServiceSample.csproj new file mode 100644 index 000000000..7d9d055a6 --- /dev/null +++ b/sample/WorkerServiceSample/WorkerServiceSample.csproj @@ -0,0 +1,17 @@ + + + + net8.0 + enable + enable + dotnet-WorkerServiceSample-4a2affa2-a620-462a-87a0-e6e0f3f309fe + + + + + + + + + + diff --git a/sample/WorkerServiceSample/appsettings.Development.json b/sample/WorkerServiceSample/appsettings.Development.json new file mode 100644 index 000000000..b2dcdb674 --- /dev/null +++ b/sample/WorkerServiceSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.Hosting.Lifetime": "Information" + } + } +} diff --git a/sample/WorkerServiceSample/appsettings.json b/sample/WorkerServiceSample/appsettings.json new file mode 100644 index 000000000..a43cea94f --- /dev/null +++ b/sample/WorkerServiceSample/appsettings.json @@ -0,0 +1,12 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Elastic.Apm": "Information" + } + }, + "ElasticApm": { + "ServiceName": "WorkerServiceSampleApp" + } +} diff --git a/src/Elastic.Apm/ApmAgentExtensions.cs b/src/Elastic.Apm/ApmAgentExtensions.cs index de238058c..417d36a27 100644 --- a/src/Elastic.Apm/ApmAgentExtensions.cs +++ b/src/Elastic.Apm/ApmAgentExtensions.cs @@ -33,7 +33,7 @@ public static IDisposable Subscribe(this IApmAgent agent, params IDiagnosticsSub { var disposable = new CompositeDisposable(); - subscribers ??= Array.Empty(); + subscribers ??= []; var subscribersList = string.Join(", ", subscribers.Select(s => s.GetType().Name)); agent.Logger.Trace()?.Log("Agent.Subscribe(), Agent Enabled: {AgentEnabled} Subscriber count: {NumberOfSubscribers}, ({Subscribers})", diff --git a/src/Elastic.Apm/Elastic.Apm.csproj b/src/Elastic.Apm/Elastic.Apm.csproj index 47589f7eb..e88add7f0 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -61,6 +61,7 @@ + diff --git a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs index 97be645cf..0a260f0d8 100644 --- a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs @@ -13,7 +13,6 @@ using Elastic.Apm.Logging; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Logging; using IConfiguration = Microsoft.Extensions.Configuration.IConfiguration; namespace Elastic.Apm.AspNetCore @@ -39,19 +38,21 @@ public static class ApplicationBuilderExtensions /// /// You can optionally pass the IConfiguration of your application to the Elastic APM Agent. By /// doing this the agent will read agent related configurations through this IConfiguration instance. - /// If no is passed to the agent then it will read configs from environment variables. + /// If no is passed to the agent then it will read configs from environment variables. /// /// /// Specify which diagnostic source subscribers you want to connect. /// The will always be injected if not specified. /// + [Obsolete("This extension is maintained for backward compatibility." + + " We recommend registering the agent via the IServiceCollection using the AddElasticApm extension method instead. This method may be removed in a future release.")] public static IApplicationBuilder UseElasticApm( this IApplicationBuilder builder, IConfiguration configuration = null, params IDiagnosticsSubscriber[] subscribers ) { - var logger = builder.ApplicationServices.GetApmLogger(); + var logger = NetCoreLogger.GetApmLogger(builder.ApplicationServices); var configReader = configuration == null ? new EnvironmentConfiguration(logger) @@ -99,10 +100,5 @@ private static string GetEnvironmentName(this IServiceProvider serviceProvider) (serviceProvider.GetService(typeof(IHostingEnvironment)) as IHostingEnvironment)?.EnvironmentName; #pragma warning restore CS0246 #endif - - internal static IApmLogger GetApmLogger(this IServiceProvider serviceProvider) => - serviceProvider.GetService(typeof(ILoggerFactory)) is ILoggerFactory loggerFactory - ? new NetCoreLogger(loggerFactory) - : ConsoleLogger.Instance; } } diff --git a/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..dbc4734c8 --- /dev/null +++ b/src/integrations/Elastic.Apm.AspNetCore/ServiceCollectionExtensions.cs @@ -0,0 +1,38 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Linq; +using Elastic.Apm.AspNetCore.DiagnosticListener; +using Elastic.Apm.DiagnosticSource; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers Elastic APM .NET Agent into the dependency injection container and enables the . + /// You can customize the agent by passing additional components to this method. + /// + /// An where services are to be registered. + /// Specify zero or more additional diagnostic source subscribers to enable. + public static IServiceCollection AddElasticApmForAspNetCore(this IServiceCollection services, params IDiagnosticsSubscriber[] subscribers) + { + if (subscribers is null || subscribers.Length == 0) + { + services.AddElasticApm(new AspNetCoreDiagnosticSubscriber()); + } + else if (subscribers.Any(s => s is AspNetCoreDiagnosticSubscriber)) + { + services.AddElasticApm(subscribers); + } + else + { + var subs = subscribers.ToList(); + subs.Add(new AspNetCoreDiagnosticSubscriber()); + services.AddElasticApm([..subs]); + } + + return services; + } +} diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/ApmService.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/ApmService.cs new file mode 100644 index 000000000..b4f57b025 --- /dev/null +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ApmService.cs @@ -0,0 +1,25 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Hosting; + +namespace Elastic.Apm.NetCoreAll; + +/// +/// When registered into the DI container, this ensures that an instance of is +/// created by invoking the implementation factory. +/// +internal sealed class ApmService : IHostedService +{ +#pragma warning disable IDE0052 // Remove unread private members + private readonly IApmAgent _agent; +#pragma warning restore IDE0052 // Remove unread private members + + public ApmService(IApmAgent agent) => _agent = agent; + + public Task StartAsync(CancellationToken cancellationToken) => Task.CompletedTask; + public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; +} diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj index f816251c2..8725f6c15 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/Elastic.Apm.Extensions.Hosting.csproj @@ -1,4 +1,4 @@ - + Elastic.Apm.Extensions.Hosting @@ -19,6 +19,7 @@ + @@ -39,5 +40,4 @@ - diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs index abd132e21..d33a62d73 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/HostBuilderExtensions.cs @@ -1,3 +1,7 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System; using System.Linq; using Elastic.Apm.Api; @@ -41,6 +45,8 @@ internal static string GetHostingEnvironmentName(HostBuilderContext ctx, IApmLog /// /// Builder. /// Specify which diagnostic source subscribers you want to connect. + [Obsolete("This extension is maintained for backward compatibility." + + " We recommend registering the agent via the IServiceCollection using the AddElasticApm extension method instead. This method may be removed in a future release.")] public static IHostBuilder UseElasticApm(this IHostBuilder builder, params IDiagnosticsSubscriber[] subscribers) { builder.ConfigureServices((ctx, services) => @@ -96,7 +102,7 @@ public static IHostBuilder UseElasticApm(this IHostBuilder builder, params IDiag // Only add ElasticApmErrorLoggingProvider after the agent is created, because it depends on the agent services.AddSingleton(sp => - new ApmErrorLoggingProvider(sp.GetService())); + new ApmErrorLoggingProvider(apmAgent)); if (subscribers != null && subscribers.Any() && Agent.IsConfigured) apmAgent.Subscribe(subscribers); diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs index ea9dc55ed..f806f3499 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs @@ -1,43 +1,39 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System; using Elastic.Apm.Logging; using Microsoft.Extensions.Logging; using LogLevel = Elastic.Apm.Logging.LogLevel; -namespace Elastic.Apm.Extensions.Hosting +namespace Elastic.Apm.Extensions.Hosting; + +internal sealed class NetCoreLogger : IApmLogger { - internal class NetCoreLogger : IApmLogger - { - private readonly ILogger _logger; + private readonly ILogger _logger; - public NetCoreLogger(ILoggerFactory loggerFactory) => - _logger = loggerFactory?.CreateLogger("Elastic.Apm") ?? throw new ArgumentNullException(nameof(loggerFactory)); + public NetCoreLogger(ILoggerFactory loggerFactory) => _logger = loggerFactory?.CreateLogger("Elastic.Apm") ?? throw new ArgumentNullException(nameof(loggerFactory)); - public bool IsEnabled(LogLevel level) => _logger.IsEnabled(Convert(level)); + public bool IsEnabled(LogLevel level) => _logger.IsEnabled(Convert(level)); - public void Log(LogLevel level, TState state, Exception e, Func formatter) => - _logger.Log(Convert(level), new EventId(), state, e, formatter); + public void Log(LogLevel level, TState state, Exception e, Func formatter) => + _logger.Log(Convert(level), new EventId(), state, e, formatter); - private static Microsoft.Extensions.Logging.LogLevel Convert(LogLevel logLevel) + private static Microsoft.Extensions.Logging.LogLevel Convert(LogLevel logLevel) => + logLevel switch { - switch (logLevel) - { - case LogLevel.Trace: - return Microsoft.Extensions.Logging.LogLevel.Trace; - case LogLevel.Debug: - return Microsoft.Extensions.Logging.LogLevel.Debug; - case LogLevel.Information: - return Microsoft.Extensions.Logging.LogLevel.Information; - case LogLevel.Warning: - return Microsoft.Extensions.Logging.LogLevel.Warning; - case LogLevel.Error: - return Microsoft.Extensions.Logging.LogLevel.Error; - case LogLevel.Critical: - return Microsoft.Extensions.Logging.LogLevel.Critical; - // ReSharper disable once RedundantCaseLabel - case LogLevel.None: - default: - return Microsoft.Extensions.Logging.LogLevel.None; - } - } - } + LogLevel.Trace => Microsoft.Extensions.Logging.LogLevel.Trace, + LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug, + LogLevel.Information => Microsoft.Extensions.Logging.LogLevel.Information, + LogLevel.Warning => Microsoft.Extensions.Logging.LogLevel.Warning, + LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error, + LogLevel.Critical => Microsoft.Extensions.Logging.LogLevel.Critical, + _ => Microsoft.Extensions.Logging.LogLevel.None, + }; + + internal static IApmLogger GetApmLogger(IServiceProvider serviceProvider) => + serviceProvider.GetService(typeof(ILoggerFactory)) is ILoggerFactory loggerFactory + ? new NetCoreLogger(loggerFactory) + : ConsoleLogger.Instance; } diff --git a/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..1923e42e2 --- /dev/null +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ServiceCollectionExtensions.cs @@ -0,0 +1,121 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System; +using Elastic.Apm; +using Elastic.Apm.Config; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Extensions.Hosting; +using Elastic.Apm.Extensions.Hosting.Config; +using Elastic.Apm.Extensions.Logging; +using Elastic.Apm.Logging; +using Elastic.Apm.NetCoreAll; +using Elastic.Apm.Report; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Register Elastic APM .NET Agent into the dependency injection container. + /// You can customize the agent by passing additional components to this method. + /// + /// Use this method if you want to control what tracing capability of the agent you would like to use + /// or in case you want to minimize the number of dependencies added to your application. + /// + /// + /// If you want to simply enable every tracing component without configuration please use the + /// AddAllElasticApm extension method from the Elastic.Apm.NetCoreAll package. + /// + /// + /// An where services are to be registered. + /// Specify zero or more diagnostic source subscribers to enable. + public static IServiceCollection AddElasticApm(this IServiceCollection services, params IDiagnosticsSubscriber[] subscribers) + { + services.AddSingleton(sp => + { + var agentConfigured = Agent.IsConfigured; + + // If the agent singleton has already been configured, we use that instance, + // regardless of when/where it was created. This ensures that we don't attempt to + // create multiple agent instances in the same process, which would result in + // errors in the logs. When used correctly, this should never happen and we + // should always initialise a new agent here. + if (agentConfigured) + return Agent.Instance; + + var logger = NetCoreLogger.GetApmLogger(sp); + var environmentName = GetEnvironmentName(sp); + + if (environmentName is null) + { + logger?.Warning()?.Log("Failed to retrieve hosting environment name"); + environmentName = "Undetermined"; + } + + var configuration = sp.GetService(); + + IConfigurationReader configurationReader = configuration is null + ? new EnvironmentConfiguration(logger) + : new ApmConfiguration(configuration, logger, environmentName); + + // This may be null, which is fine + var payloadSender = sp.GetService(); + + var components = agentConfigured + ? Agent.Components + : new AgentComponents(logger, configurationReader, payloadSender); + + HostBuilderExtensions.UpdateServiceInformation(components.Service); + + Agent.Setup(components); + + // Under expected usage, this will always be a new lazily created instance based + // on the configuration and components above. Worst case, it will be the existing + // instance if another thread has created one since we checked at the start of this + // method. + var agent = Agent.Instance; + + // If the configuration is disabled, we don't want to subscribe any listeners. + // We simply log a message and return the agent as-is. + if (!agent.Configuration.Enabled) + { + logger?.Info()?.Log("The 'Enabled' agent config is set to false - the agent won't collect and send any data."); + } + else + { + // Subscribe handles cases where subscribers is null or empty, so we avoid + // repeating that check here. + agent.Subscribe(subscribers); + } + + var loggerFactory = sp.GetService(); + loggerFactory.AddProvider(new ApmErrorLoggingProvider(agent)); + + return agent; + }); + + // The ITracer is registered as a singleton to allow for easy access to the tracer + // via dependency injection. This is useful for manual instrumentation. + services.AddSingleton(sp => sp.GetRequiredService().Tracer); + + // This service is registered to trigger the creation of the IApmAgent. + services.AddHostedService(); + + return services; + } + + private static string GetEnvironmentName(IServiceProvider serviceProvider) => +#if NET6_0_OR_GREATER + (serviceProvider.GetService(typeof(IHostEnvironment)) as IHostEnvironment)?.EnvironmentName; // This is preferred since 3.0 +#else +#pragma warning disable CS0246 + (serviceProvider.GetService(typeof(IHostingEnvironment)) as IHostingEnvironment)?.EnvironmentName; +#pragma warning restore CS0246 +#endif +} diff --git a/src/integrations/Elastic.Apm.Extensions.Logging/ApmErrorLoggingProvider.cs b/src/integrations/Elastic.Apm.Extensions.Logging/ApmErrorLoggingProvider.cs index 86d0ca237..fb43a2770 100644 --- a/src/integrations/Elastic.Apm.Extensions.Logging/ApmErrorLoggingProvider.cs +++ b/src/integrations/Elastic.Apm.Extensions.Logging/ApmErrorLoggingProvider.cs @@ -1,5 +1,4 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. +// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information diff --git a/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs index 1d24ac0ba..534fbfd31 100644 --- a/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs @@ -2,6 +2,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; using Elastic.Apm.Azure.CosmosDb; using Elastic.Apm.Azure.ServiceBus; using Elastic.Apm.Azure.Storage; @@ -42,6 +43,8 @@ public static class ApplicationBuilderExtensions /// The agent reads agent-related configuration from the instance, and uses it to configure the agent. /// If no is provided, the agent reads agent-related configuration from environment variables. /// + [Obsolete("This extension is maintained for backward compatibility." + + " We recommend registering the agent via the IServiceCollection using the AddAllElasticApm extension method instead. This method may be removed in a future release.")] public static IApplicationBuilder UseAllElasticApm( this IApplicationBuilder builder, IConfiguration configuration = null diff --git a/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj b/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj index 93f71bac8..1d1ba10d2 100644 --- a/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj +++ b/src/integrations/Elastic.Apm.NetCoreAll/Elastic.Apm.NetCoreAll.csproj @@ -1,4 +1,4 @@ - + netstandard2.0;net6.0 Elastic.Apm.NetCoreAll diff --git a/src/integrations/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs b/src/integrations/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs index e58f93bb6..917eb2187 100644 --- a/src/integrations/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs +++ b/src/integrations/Elastic.Apm.NetCoreAll/HostBuilderExtensions.cs @@ -3,6 +3,7 @@ // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information +using System; using Elastic.Apm.AspNetCore.DiagnosticListener; using Elastic.Apm.Azure.CosmosDb; using Elastic.Apm.Azure.ServiceBus; @@ -37,6 +38,8 @@ public static class HostBuilderExtensions /// and . /// /// Builder. + [Obsolete("This extension is maintained for backward compatibility." + + " We recommend registering the agent via the IServiceCollection using the AddAllElasticApm extension method instead.")] public static IHostBuilder UseAllElasticApm(this IHostBuilder builder) => builder.UseElasticApm( new HttpDiagnosticsSubscriber(), new AspNetCoreDiagnosticSubscriber(), diff --git a/src/integrations/Elastic.Apm.NetCoreAll/ServiceCollectionExtensions.cs b/src/integrations/Elastic.Apm.NetCoreAll/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..018e591ae --- /dev/null +++ b/src/integrations/Elastic.Apm.NetCoreAll/ServiceCollectionExtensions.cs @@ -0,0 +1,80 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm.AspNetCore.DiagnosticListener; +using Elastic.Apm.Azure.CosmosDb; +using Elastic.Apm.Azure.ServiceBus; +using Elastic.Apm.Azure.Storage; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Elasticsearch; +using Elastic.Apm.EntityFrameworkCore; +using Elastic.Apm.GrpcClient; +using Elastic.Apm.Instrumentations.SqlClient; +using Elastic.Apm.MongoDb; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceCollectionExtensions +{ + /// + /// Registers Elastic APM .NET Agent into the dependency injection container and enables: + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// An where services are to be registered. + public static IServiceCollection AddAllElasticApm(this IServiceCollection services) => + services.AddElasticApm( + new HttpDiagnosticsSubscriber(), + new AspNetCoreDiagnosticSubscriber(), + new EfCoreDiagnosticsSubscriber(), + new SqlClientDiagnosticSubscriber(), + new ElasticsearchDiagnosticsSubscriber(), + new GrpcClientDiagnosticSubscriber(), + new AzureMessagingServiceBusDiagnosticsSubscriber(), + new MicrosoftAzureServiceBusDiagnosticsSubscriber(), + new AzureBlobStorageDiagnosticsSubscriber(), + new AzureQueueStorageDiagnosticsSubscriber(), + new AzureFileShareStorageDiagnosticsSubscriber(), + new AzureCosmosDbDiagnosticsSubscriber(), + new MongoDbDiagnosticsSubscriber()); +} diff --git a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj index 1d00e874c..30f0746d1 100644 --- a/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj +++ b/test/Elastic.Apm.Tests.Utilities/Elastic.Apm.Tests.Utilities.csproj @@ -30,6 +30,7 @@ + diff --git a/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs b/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs index 715ecf737..c72b7c21d 100644 --- a/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs +++ b/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs @@ -22,23 +22,23 @@ namespace Elastic.Apm.Tests.Utilities internal class MockPayloadSender : IPayloadSender { private static readonly JObject JsonSpanTypesData = - JObject.Parse(File.ReadAllText("./TestResources/json-specs/span_types.json")); - - private readonly List _errors = new List(); - private readonly List> _errorFilters = new List>(); - private readonly object _spanLock = new object(); - private readonly object _transactionLock = new object(); - private readonly object _metricsLock = new object(); - private readonly object _errorLock = new object(); - private readonly List _metrics = new List(); - private readonly List> _spanFilters = new List>(); - private readonly List _spans = new List(); - private readonly List> _transactionFilters = new List>(); - private readonly List _transactions = new List(); + JObject.Parse(File.ReadAllText(Path.Combine(SolutionPaths.Root, "test/Elastic.Apm.Tests.Utilities/TestResources/json-specs/span_types.json"))); + + private readonly List _errors = []; + private readonly List> _errorFilters = []; + private readonly object _spanLock = new(); + private readonly object _transactionLock = new(); + private readonly object _metricsLock = new(); + private readonly object _errorLock = new(); + private readonly List _metrics = []; + private readonly List> _spanFilters = []; + private readonly List _spans = []; + private readonly List> _transactionFilters = []; + private readonly List _transactions = []; public MockPayloadSender(IApmLogger logger = null) { - _waitHandles = new[] { new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false) }; + _waitHandles = [new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false), new AutoResetEvent(false)]; _transactionWaitHandle = _waitHandles[0]; _spanWaitHandle = _waitHandles[1]; diff --git a/test/instrumentations/grpc/GrpcServiceSample/Program.cs b/test/instrumentations/grpc/GrpcServiceSample/Program.cs index 39b4c977f..1bb840405 100644 --- a/test/instrumentations/grpc/GrpcServiceSample/Program.cs +++ b/test/instrumentations/grpc/GrpcServiceSample/Program.cs @@ -1,4 +1,3 @@ -using Elastic.Apm.NetCoreAll; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; @@ -12,7 +11,6 @@ public class Program // For instructions on how to configure Kestrel and gRPC clients on macOS, visit https://go.microsoft.com/fwlink/?linkid=2099682 public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }) - .UseAllElasticApm(); + .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } } diff --git a/test/instrumentations/grpc/GrpcServiceSample/Startup.cs b/test/instrumentations/grpc/GrpcServiceSample/Startup.cs index 25d59ca32..cb58a8f65 100644 --- a/test/instrumentations/grpc/GrpcServiceSample/Startup.cs +++ b/test/instrumentations/grpc/GrpcServiceSample/Startup.cs @@ -11,7 +11,7 @@ public class Startup // This method gets called by the runtime. Use this method to add services to the container. // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 public void ConfigureServices(IServiceCollection services) - => services.AddGrpc(); + => services.AddAllElasticApm().AddGrpc(); // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs index 849f2e77f..8c0480aea 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs @@ -20,7 +20,7 @@ public void UseElasticApmShouldUseAspNetLoggerWhenLoggingIsConfigured() var services = new ServiceCollection() .AddLogging(); - var logger = services.BuildServiceProvider().GetApmLogger(); + var logger = NetCoreLogger.GetApmLogger(services.BuildServiceProvider()); Assert.IsType(logger); } @@ -30,7 +30,7 @@ public void UseElasticApmShouldUseConsoleLoggerInstanceWhenLoggingIsNotConfigure { var services = new ServiceCollection(); - var logger = services.BuildServiceProvider().GetApmLogger(); + var logger = NetCoreLogger.GetApmLogger(services.BuildServiceProvider()); Assert.IsType(logger); Assert.Same(ConsoleLogger.Instance, logger); diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs index 31ba36e73..23edf20e0 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/AspNetCoreBasicTests.cs @@ -238,9 +238,7 @@ public async Task HomeSimplePagePostTransactionTest() var aspNetCoreVersion = Assembly.Load("Microsoft.AspNetCore").GetName().Version.ToString(2); agent.Service.Framework.Version.Should().StartWith(aspNetCoreVersion); -#if NET5_0 - agent.Service.Runtime.Name.Should().Be(Runtime.DotNetName + " 5"); -#elif NET6_0 +#if NET6_0 agent.Service.Runtime.Name.Should().Be(Runtime.DotNetName + " 6"); #elif NET7_0 agent.Service.Runtime.Name.Should().Be(Runtime.DotNetName + " 7"); diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs index ab3400335..da9493a4f 100644 --- a/test/integrations/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/DistributedTracingAspNetCoreTests.cs @@ -469,7 +469,6 @@ public async Task TraceContinuationStrategyRestartExternalAndNoTraceState() /// private async Task ExecuteAndCheckDistributedCall(bool startActivityBeforeHttpCall = true) { -#if NET5_0_OR_GREATER // .NET 5 has built-in W3C TraceContext support and Activity uses the W3C id format by default (pre .NET 5 it was opt-in) // This means if there is no active activity, the outgoing HTTP request on HttpClient will add the traceparent header with // a flag recorded=false. The agent would pick this up on the incoming call and start an unsampled transaction. @@ -480,7 +479,7 @@ private async Task ExecuteAndCheckDistributedCall(bool startActivityBeforeHttpCa activity = new Activity("foo").Start(); activity.ActivityTraceFlags |= ActivityTraceFlags.Recorded; } -#endif + var client = new HttpClient(); var res = await client.GetAsync("http://localhost:5901/Home/DistributedTracingMiniSample"); res.IsSuccessStatusCode.Should().BeTrue(); @@ -493,10 +492,9 @@ private async Task ExecuteAndCheckDistributedCall(bool startActivityBeforeHttpCa //make sure the 2 transactions have the same traceid: _payloadSender2.FirstTransaction.TraceId.Should().Be(_payloadSender1.FirstTransaction.TraceId); -#if NET5_0 + if (startActivityBeforeHttpCall) activity.Dispose(); -#endif } } } diff --git a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj index 305f1f5b4..d7340831f 100644 --- a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj +++ b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/Elastic.Apm.Extensions.Hosting.Tests.csproj @@ -6,17 +6,12 @@ - - - - - - - + - + + diff --git a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostBuilderExtensionTests.cs b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostBuilderExtensionTests.cs deleted file mode 100644 index 5adcc5b4c..000000000 --- a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostBuilderExtensionTests.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Threading.Tasks; -using Elastic.Apm.DiagnosticSource; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using SampleConsoleNetCoreApp; -using Xunit; - -namespace Elastic.Apm.Extensions.Hosting.Tests -{ - public class HostBuilderExtensionTests - { - /// - /// Makes sure in case of 2 IHostBuilder insatnces when both call UseElasticApm no exception is thrown - /// - /// - [Fact] - public async Task TwoHostBuildersNoException() - { - using var hostBuilder1 = CreateHostBuilder().Build(); - using var hostBuilder2 = CreateHostBuilder().Build(); - var builder1Task = hostBuilder1.StartAsync(); - var builder2Task = hostBuilder2.StartAsync(); - - await Task.WhenAll(builder1Task, builder2Task); - await Task.WhenAll(hostBuilder1.StopAsync(), hostBuilder2.StopAsync()); - } - - /// - /// Makes sure that is true after the agent is enabled through - /// . - /// - [Fact] - public void IsAgentInitializedAfterUseElasticApm() - { - using var _ = CreateHostBuilder().Build(); - Agent.IsConfigured.Should().BeTrue(); - } - - /// - /// Makes sure that agent enables the passed into - /// . - /// - [Fact] - public void DiagnosticSubscriberWithUseElasticApm() - { - var fakeSubscriber = new FakeSubscriber(); - fakeSubscriber.IsSubscribed.Should().BeFalse(); - - using var _ = Host.CreateDefaultBuilder() - .ConfigureServices((_, services) => { services.AddHostedService(); }) - .UseElasticApm(fakeSubscriber) - .Build(); - - fakeSubscriber.IsSubscribed.Should().BeTrue(); - } - - [Fact] - public void GetHostingEnvironmentName_WorksViaReflection() - { - var environmentName = default(string); - CreateHostBuilder().ConfigureServices((ctx, _) => - { - environmentName = HostBuilderExtensions.GetHostingEnvironmentName(ctx, null); - }).Build(); - - environmentName.Should().Be("Production"); - } - - /// - /// Sets `enabled=false` and makes sure that does not turn on diagnostic - /// listeners. - /// - [Fact(Skip = "Fails on CI but not locally")] - public void DiagnosticSubscriberWithUseElasticApmAgentDisabled() - { - var fakeSubscriber = new FakeSubscriber(); - fakeSubscriber.IsSubscribed.Should().BeFalse(); - - Environment.SetEnvironmentVariable("ELASTIC_APM_ENABLED", "false"); - - try - { - using var _ = Host.CreateDefaultBuilder() - .ConfigureServices((_, services) => { services.AddHostedService(); }) - .UseElasticApm(fakeSubscriber) - .Build(); - - fakeSubscriber.IsSubscribed.Should().BeFalse(); - } - finally - { - Environment.SetEnvironmentVariable("ELASTIC_APM_ENABLED", null); - } - } - - private static IHostBuilder CreateHostBuilder() => - Host.CreateDefaultBuilder() - .ConfigureServices((_, services) => { services.AddHostedService(); }) - .UseElasticApm(); - - public class FakeSubscriber : IDiagnosticsSubscriber - { - public bool IsSubscribed { get; set; } - - public IDisposable Subscribe(IApmAgent components) - { - IsSubscribed = true; - return null; - } - } - } -} diff --git a/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostingTests.cs b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostingTests.cs new file mode 100644 index 000000000..f1b1f3953 --- /dev/null +++ b/test/integrations/Elastic.Apm.Extensions.Hosting.Tests/HostingTests.cs @@ -0,0 +1,73 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Threading.Tasks; +using Elastic.Apm.Extensions.Tests.Shared; +using FluentAssertions; +using Microsoft.Extensions.Hosting; +using Xunit; +using Xunit.Abstractions; + +namespace Elastic.Apm.Extensions.Hosting.Tests +{ + public class HostingTests + { + private readonly ExtensionsTestHelper _extensionsTestHelper; + + public HostingTests(ITestOutputHelper output) + { + _extensionsTestHelper = new(output); + _extensionsTestHelper.TestSetup(); + } + + [Fact] + public async Task AddElasticApm_WhenEnabledIsNotConfigured() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, false, false, false); + + [Fact] + public async Task AddElasticApm_WhenDisabledInConfiguration() => + await _extensionsTestHelper.ExecuteTestProcessAsync(false, false, false, false, false); + + [Fact] + public async Task AddElasticApm_WhenDisabledInEnvironmentVariables() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, false, true, false); + + [Fact] + public async Task AddElasticApm_WhenRegisteredMultipleTimes() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, true, false, true, false); + + [Fact] + public async Task UseElasticApm_WhenEnabledIsNotConfigured() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, true, false, false); + + [Fact] + public async Task UseElasticApm_WhenDisabledInConfiguration() => + await _extensionsTestHelper.ExecuteTestProcessAsync(false, false, true, false, false); + + [Fact] + public async Task UseElasticApm_WhenDisabledInEnvironmentVariables() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, true, true, false); + + [Fact] + public async Task UseElasticApm_WhenRegisteredMultipleTimes() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, true, true, true, false); + + [Fact] + public void GetHostingEnvironmentName_WorksViaReflection() + { + var environmentName = default(string); +#pragma warning disable CS0618 // Type or member is obsolete + Host.CreateDefaultBuilder() + .UseElasticApm() + .ConfigureServices((ctx, _) => + { + environmentName = HostBuilderExtensions.GetHostingEnvironmentName(ctx, null); + }) + .Build(); +#pragma warning restore CS0618 // Type or member is obsolete + + environmentName.Should().Be("Production"); + } + } +} diff --git a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/CaptureApmErrorsTests.cs b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/CaptureApmErrorsTests.cs index cae863cab..9838570d2 100644 --- a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/CaptureApmErrorsTests.cs +++ b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/CaptureApmErrorsTests.cs @@ -1,74 +1,29 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. +// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information -using System; -using System.Linq; using System.Threading.Tasks; -using Elastic.Apm.Extensions.Hosting; -using Elastic.Apm.Report; -using Elastic.Apm.Tests.Utilities; -using FluentAssertions; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using SampleConsoleNetCoreApp; +using Elastic.Apm.Extensions.Tests.Shared; using Xunit; +using Xunit.Abstractions; -namespace Elastic.Apm.Extensions.Logging.Tests -{ - public class CaptureApmErrorsTests - { - [Fact] - public async Task CaptureErrorLogsAsApmError() - { - var payloadSender = new MockPayloadSender(); - using var hostBuilder = CreateHostBuilder(payloadSender).Build(); - - await hostBuilder.StartAsync(); - - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); - - payloadSender.FirstError.Log.Message.Should().Be("This is a sample error log message, with a sample value: 42"); - payloadSender.FirstError.Log.ParamMessage.Should().Be("This is a sample error log message, with a sample value: {intParam}"); +namespace Elastic.Apm.Extensions.Logging.Tests; - // Test a log with exception - var logger = (ILogger)hostBuilder.Services.GetService(typeof(ILogger)); - - try - { - throw new Exception(); - } - catch (Exception e) - { - logger.LogError(e, "error log with exception"); - } +public class CaptureApmErrorsTests +{ + private readonly ExtensionsTestHelper _extensionsTestHelper; - payloadSender.WaitForErrors(); - payloadSender.Errors.Should().NotBeEmpty(); - payloadSender.Errors.Where(n => n.Log.Message == "error log with exception" && - n.Log.StackTrace != null && n.Log.StackTrace.Count > 0) - .Should() - .NotBeNullOrEmpty(); + public CaptureApmErrorsTests(ITestOutputHelper output) + { + _extensionsTestHelper = new(output); + _extensionsTestHelper.TestSetup(); + } - await hostBuilder.StopAsync(); - } + [Fact] + public async Task UseElasticApm_CaptureErrorLogsAsApmError() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, true, false, true); - private static IHostBuilder CreateHostBuilder(MockPayloadSender payloadSender = null) => - Host.CreateDefaultBuilder() - .ConfigureServices(n => n.AddSingleton(_ => payloadSender)) - .ConfigureServices((_, services) => { services.AddHostedService(); }) - .ConfigureLogging((_, logging) => - { - logging.ClearProviders(); -#if NET5_0_OR_GREATER - logging.AddSimpleConsole(o => o.IncludeScopes = true); -#else - logging.AddConsole(options => options.IncludeScopes = true); -#endif - }) - .UseElasticApm(); - } + [Fact] + public async Task AddElasticApm_CaptureErrorLogsAsApmError() => + await _extensionsTestHelper.ExecuteTestProcessAsync(null, false, false, false, true); } diff --git a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj index 4b38f6fb4..2dcd2d9b8 100644 --- a/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj +++ b/test/integrations/Elastic.Apm.Extensions.Logging.Tests/Elastic.Apm.Extensions.Logging.Tests.csproj @@ -4,20 +4,15 @@ Exe net8.0 - - - - - + - - + - + diff --git a/test/integrations/Elastic.Apm.Extensions.Tests.Shared/Elastic.Apm.Extensions.Tests.Shared.csproj b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/Elastic.Apm.Extensions.Tests.Shared.csproj new file mode 100644 index 000000000..e3957faba --- /dev/null +++ b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/Elastic.Apm.Extensions.Tests.Shared.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs new file mode 100644 index 000000000..e8c8459c8 --- /dev/null +++ b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs @@ -0,0 +1,98 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using System.Diagnostics; +using Elastic.Apm.Tests.Utilities; +using FluentAssertions; +using Xunit.Abstractions; + +namespace Elastic.Apm.Extensions.Tests.Shared; + +public class ExtensionsTestHelper +{ + private readonly string _workingDirectory = Path.Combine(SolutionPaths.Root, "test", "integrations", "applications", "HostingTestApp"); + private readonly ITestOutputHelper _testOutput; + + public ExtensionsTestHelper(ITestOutputHelper testOutput) => _testOutput = testOutput; + + public void TestSetup() + { + // Build the app once before running tests + + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + UseShellExecute = false, + RedirectStandardOutput = false, + RedirectStandardError = false, + CreateNoWindow = true, + WorkingDirectory = _workingDirectory, + }; + + startInfo.ArgumentList.Add("build"); + startInfo.ArgumentList.Add("-c"); + startInfo.ArgumentList.Add("Release"); + + using var proc = new Process { StartInfo = startInfo }; + + proc.Start(); + proc.WaitForExit(); + + if (proc.ExitCode != 0) + throw new Exception("Unable to build test app project required for tests!"); + } + + public async Task ExecuteTestProcessAsync(bool? enabled, bool registerTwice, bool legacyIHostBuilder, bool disabledViaEnvVar, bool loggingMode) + { + var startInfo = new ProcessStartInfo + { + FileName = "dotnet", + UseShellExecute = false, + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + WorkingDirectory = _workingDirectory, + }; + + if (disabledViaEnvVar) + startInfo.Environment["ELASTIC_APM_ENABLED"] = "false"; + + startInfo.ArgumentList.Add("run"); + startInfo.ArgumentList.Add("-c"); + startInfo.ArgumentList.Add("Release"); + startInfo.ArgumentList.Add("--no-build"); + startInfo.ArgumentList.Add("--no-restore"); + startInfo.ArgumentList.Add("--"); + startInfo.ArgumentList.Add(enabled.HasValue ? enabled.Value.ToString() : "unset"); + startInfo.ArgumentList.Add(registerTwice.ToString()); + startInfo.ArgumentList.Add(legacyIHostBuilder.ToString()); + startInfo.ArgumentList.Add(loggingMode.ToString()); + + using var proc = new Process { StartInfo = startInfo }; + + proc.OutputDataReceived += new DataReceivedEventHandler((_, e) => + { + if (e.Data is null) + return; + + _testOutput.WriteLine(e.Data); + }); + + proc.ErrorDataReceived += new DataReceivedEventHandler((_, e) => + { + if (e.Data is null) + return; + + _testOutput.WriteLine($"ERROR: {e.Data}"); + }); + + proc.Start(); + proc.BeginOutputReadLine(); + proc.BeginErrorReadLine(); + + await proc.WaitForExitAsync(); + + proc.ExitCode.Should().Be(0); + } +} diff --git a/test/integrations/applications/HostingTestApp/HostingTestApp.csproj b/test/integrations/applications/HostingTestApp/HostingTestApp.csproj new file mode 100644 index 000000000..bff7b18bc --- /dev/null +++ b/test/integrations/applications/HostingTestApp/HostingTestApp.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + + diff --git a/test/integrations/applications/HostingTestApp/Program.cs b/test/integrations/applications/HostingTestApp/Program.cs new file mode 100644 index 000000000..9ab73521c --- /dev/null +++ b/test/integrations/applications/HostingTestApp/Program.cs @@ -0,0 +1,263 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Apm; +using Elastic.Apm.Api; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.Extensions.Hosting; +using Elastic.Apm.Report; +using Elastic.Apm.Tests.Utilities; +using SampleConsoleNetCoreApp; +using Test; + +// This is an application used to test the Elastic.Apm.Extensions.Hosting and Elastic.Apm.Extensions.Logging +// packages. It is used by the Elastic.Apm.Extensions.Hosting.Tests project which starts this application +// with different configurations. This allows isolated testing of the Elastic.Apm.Extensions.Hosting package +// and avoids issues with the singleton ApmAgent affecting the outcome of other test cases. + +// Args: +// [0] - Control IConfiguration enabling of IApmAgent - Valid values: true, false, unset +// When unset, the configuration is not set and the agent defaults should apply. +// [1] - Control multiple registration of IApmAgent - Valid values: true, false +// When true, the agent is registered twice so that we can validate that this does not +// cause an exception or error logs. We expect the first registration to win. +// [2] - Control whether to use legacy IHostBuilder - Valid values: true, false +// When true, the legacy extension method on the IHostBuilder is used. When false, +// the new IServiceCollection registration is used. +// [3] - Control whether to test the logging by registering a mock payload sender - Valid values: true, false +// When true, the application registers an IHostedService and MockPayloadSender to test that error logs +// are captured. + +bool? enabled; + +if (args[0] == "unset") +{ + enabled = null; +} +else +{ + if (!bool.TryParse(args[0], out var e)) + throw new Exception("The first argument must be true, false or unset."); + + enabled = e; +} + +if (!bool.TryParse(args[1], out var registerTwice)) + throw new Exception("The second argument must be true or false."); + +if (!bool.TryParse(args[2], out var legacyIHostBuilder)) + throw new Exception("The third argument must be true or false."); + +if (!bool.TryParse(args[3], out var loggingTestMode)) + throw new Exception("The forth argument must be true or false."); + +if (enabled.HasValue) +{ + Console.WriteLine("Starting with enabled: " + enabled.Value); +} +else +{ + Console.WriteLine("Starting with enabled: unset"); +} + +Console.WriteLine($"Starting with registerTwice: {registerTwice}"); +Console.WriteLine($"Starting with legacy IHostBuilder: {legacyIHostBuilder}"); +Console.WriteLine($"Starting in logging test mode: {loggingTestMode}"); + +var fakeSubscriber = new FakeSubscriber(); + +if (fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should not be subscribed yet."); + +var enabledEnvironmentVariable = Environment.GetEnvironmentVariable("ELASTIC_APM_ENABLED"); + +var envEnabled = false; +if (enabledEnvironmentVariable is not null && bool.TryParse(args[0], out envEnabled)) +{ + Console.WriteLine("Starting with ELASTIC_APM_ENABLED: " + envEnabled); +} +else +{ + Console.WriteLine("ELASTIC_APM_ENABLED not configured"); +} + +// Build the IHost, either via the legacy IHostBuilder or the newer HostApplicationBuilder (IHostApplicationBuilder) + +var payloadSender = new MockPayloadSender(); + +IHost host; +if (legacyIHostBuilder) +{ +#pragma warning disable CS0618 // Type or member is obsolete + var builder = Host.CreateDefaultBuilder(); + + if (enabled.HasValue) + builder.ConfigureAppConfiguration((hostingContext, config) => + config.AddInMemoryCollection(new Dictionary { { "ElasticApm:Enabled", enabled.Value.ToString() } })); + + if (loggingTestMode) + { + // This must occur before UseElasticApm + builder.ConfigureLogging((_, logging) => + { + logging.ClearProviders(); + logging.AddSimpleConsole(o => o.IncludeScopes = true); + }); + + builder.ConfigureServices((_, services) => + { + services.AddHostedService(); + services.AddSingleton(payloadSender); + }); + } + + builder.UseElasticApm(fakeSubscriber); + + if (registerTwice) + builder.UseElasticApm(); +#pragma warning restore CS0618 // Type or member is obsolete + + host = builder.Build(); +} +else +{ + var builder = Host.CreateApplicationBuilder(args); + + if (enabled.HasValue) + builder.Configuration.AddInMemoryCollection(new Dictionary { { "ElasticApm:Enabled", enabled.Value.ToString() } }); + + if (loggingTestMode) + { + // This must occur before AddElasticApm + builder.Logging + .ClearProviders() + .AddSimpleConsole(o => o.IncludeScopes = true); + } + + builder.Services.AddElasticApm(fakeSubscriber); + + if (registerTwice) + builder.Services.AddElasticApm(); + + if (loggingTestMode) + { + builder.Services + .AddHostedService() // The IHostedService must be registered after AddElasticApm so its invoked after the agent is initialized + .AddSingleton(payloadSender); + } + + host = builder.Build(); +} + +// Start the host which should trigger the creation of the IApmAgent via DI +await host.StartAsync(); + +// We expect the agent to be configured by this point +if (!Agent.IsConfigured) + throw new Exception("Agent should be configured."); + +// We expect an ITracer to be available via DI +host.Services.GetRequiredService(); + +if (loggingTestMode) +{ + payloadSender.WaitForErrors(); + + if (payloadSender.Errors.Count != 3) + throw new Exception($"Expected 3 errors to be captured but receieved {payloadSender.Errors.Count}."); + + if (payloadSender.FirstError.Log.Message != "This is a sample error log message, with a sample value: 42") + throw new Exception($"Unexpected first message: {payloadSender.FirstError.Log.Message}."); + + if (payloadSender.FirstError.Log.ParamMessage != "This is a sample error log message, with a sample value: {intParam}") + throw new Exception($"Unexpected first param message: {payloadSender.FirstError.Log.ParamMessage}."); + + // Test a log with exception + var logger = (ILogger?)host.Services.GetService(typeof(ILogger)); + const string errorLogWithException = "error log with exception"; + + try + { + throw new Exception(); + } + catch (Exception e) + { + logger!.LogError(e, errorLogWithException); + } + + payloadSender.WaitForErrors(); + + if (payloadSender.Errors.SingleOrDefault(n => n.Log.Message == errorLogWithException && + n.Log.StackTrace != null && n.Log.StackTrace.Count > 0) is null) + throw new Exception($"Expected one error log with exception."); +} + +// Perform assertions based on the configuration +if (enabled.HasValue) +{ + // When the enabled configuration is set and 'true', we expect the agent configuration to reflect this. + if (enabled.Value && !Agent.Config.Enabled) + throw new Exception("Agent should be enabled."); + + // When the enabled configuration is set and 'true', we expect the subscriber to be subscribed. + if (enabled.Value && !fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should be subscribed."); + + // When the enabled configuration is set and 'false', we expect the agent configuration to reflect this. + if (!enabled.Value && Agent.Config.Enabled) + throw new Exception("Agent should not be enabled."); + + // When the enabled configuration is set and 'false', we do not expect the subscriber to be subscribed. + if (!enabled.Value && fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should not be subscribed."); +} +else if (enabledEnvironmentVariable is not null) +{ + // When the enabled env var is set and 'true', we expect the agent configuration to reflect this. + if (envEnabled && !Agent.Config.Enabled) + throw new Exception("Agent should be enabled."); + + // When the enabled env var is set and 'true', we expect the subscriber to be subscribed. + if (envEnabled && !fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should be subscribed."); + + // When the enabled env var is set and 'false', we expect the agent configuration to reflect this. + if (!envEnabled && Agent.Config.Enabled) + throw new Exception("Agent should not be enabled."); + + // When the enabled env var is set and 'false', we do not expect the subscriber to be subscribed. + if (!envEnabled && fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should not be subscribed."); +} +else +{ + // When the enabled configuration is not set and no env var is provided, we expect the agent configuration to default to enabled. + if (!Agent.Config.Enabled) + throw new Exception("Agent should be enabled."); + + // When the enabled configuration is not set and no env var is provided, we expect the subscriber to be subscribed. + if (!fakeSubscriber.IsSubscribed) + throw new Exception("Subscriber should be subscribed."); +} + +// Stop and dispose the host +await host.StopAsync(); +host.Dispose(); + +Console.WriteLine("FINISHED"); + +namespace Test +{ + public class FakeSubscriber : IDiagnosticsSubscriber + { + public bool IsSubscribed { get; set; } + + public IDisposable? Subscribe(IApmAgent components) + { + IsSubscribed = true; + return null; + } + } +} + diff --git a/test/integrations/applications/HostingTestApp/appsettings.json b/test/integrations/applications/HostingTestApp/appsettings.json new file mode 100644 index 000000000..8dcee0555 --- /dev/null +++ b/test/integrations/applications/HostingTestApp/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Error" + } + }, + "ElasticApm": { + "ServiceName": "TestApp" + } +} diff --git a/test/integrations/applications/SampleAspNetCoreApp/Startup.cs b/test/integrations/applications/SampleAspNetCoreApp/Startup.cs index 8cd0a830b..d6e801c24 100644 --- a/test/integrations/applications/SampleAspNetCoreApp/Startup.cs +++ b/test/integrations/applications/SampleAspNetCoreApp/Startup.cs @@ -11,10 +11,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using SampleAspNetCoreApp.Data; -#if NET5_0 -using OpenTelemetry; -using OpenTelemetry.Trace; -#endif namespace SampleAspNetCoreApp { @@ -54,14 +50,12 @@ public static void ConfigureServicesExceptMvc(IServiceCollection services) } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. -#if NET6_0_OR_GREATER public void Configure(IApplicationBuilder app, IWebHostEnvironment env) -#else - public void Configure(IApplicationBuilder app, IHostingEnvironment env) -#endif { if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SKIP_AGENT_REGISTRATION"))) +#pragma warning disable CS0618 // Type or member is obsolete app.UseAllElasticApm(Configuration); +#pragma warning restore CS0618 // Type or member is obsolete ConfigureAllExceptAgent(app); } @@ -78,7 +72,6 @@ public static void ConfigureAllExceptAgent(IApplicationBuilder app) public static void ConfigureRoutingAndMvc(IApplicationBuilder app) { -#if NETCOREAPP3_0 || NETCOREAPP3_1 || NET5_0_OR_GREATER app.UseRouting(); app.UseAuthentication(); @@ -98,25 +91,6 @@ public static void ConfigureRoutingAndMvc(IApplicationBuilder app) endpoints.MapControllers(); endpoints.MapRazorPages(); }); -#else - app.UseAuthentication(); - - app.UseMvc(routes => - { - routes.MapAreaRoute( - "MyOtherArea", - "MyOtherArea", - "MyOtherArea/{controller=Home}/{action=Index}/{id?}"); - - routes.MapRoute( - "MyArea", - "{area:exists}/{controller=Home}/{action=Index}/{id?}"); - - routes.MapRoute( - "default", - "{controller=Home}/{action=Index}/{id?}"); - }); -#endif } } } diff --git a/test/integrations/applications/SampleAspNetCoreApp/Utils/CpuBurner.cs b/test/integrations/applications/SampleAspNetCoreApp/Utils/CpuBurner.cs index f4c716e3f..bec42042a 100644 --- a/test/integrations/applications/SampleAspNetCoreApp/Utils/CpuBurner.cs +++ b/test/integrations/applications/SampleAspNetCoreApp/Utils/CpuBurner.cs @@ -1,5 +1,4 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. +// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs b/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs index 1db3ccde0..be007b2bd 100644 --- a/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs +++ b/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs @@ -1,5 +1,4 @@ -// Licensed to Elasticsearch B.V under -// one or more agreements. +// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information @@ -39,9 +38,7 @@ await _apmAgent.Tracer.CaptureTransaction("Console .Net Core Example", "backgrou using var fooScope = _logger.BeginScope("foo"); // Make sure Agent.Tracer.CurrentTransaction is not null - var currentTransaction = Agent.Tracer.CurrentTransaction; - if (currentTransaction == null) - throw new Exception("Agent.Tracer.CurrentTransaction returns null"); + var currentTransaction = Agent.Tracer.CurrentTransaction ?? throw new Exception("Agent.Tracer.CurrentTransaction returns null"); var httpClient = new HttpClient(); return await httpClient.GetAsync("https://elastic.co", cancellationToken); diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs b/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs index b51d0cae1..4c982a12e 100644 --- a/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs +++ b/test/integrations/applications/SampleConsoleNetCoreApp/Program.cs @@ -1,6 +1,9 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + using System.Threading.Tasks; using Elastic.Apm.DiagnosticSource; -using Elastic.Apm.Extensions.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -18,12 +21,11 @@ private static async Task Main(string[] args) private static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) - .ConfigureServices((_, services) => { services.AddHostedService(); }) + .ConfigureServices((_, services) => { services.AddElasticApm(new HttpDiagnosticsSubscriber()).AddHostedService(); }) .ConfigureLogging((_, logging) => { logging.ClearProviders(); logging.AddConsole(options => options.IncludeScopes = true); - }) - .UseElasticApm(new HttpDiagnosticsSubscriber()); + }); } } diff --git a/test/integrations/applications/WebApiSample/Startup.cs b/test/integrations/applications/WebApiSample/Startup.cs index 7003f5185..d756a61b0 100644 --- a/test/integrations/applications/WebApiSample/Startup.cs +++ b/test/integrations/applications/WebApiSample/Startup.cs @@ -20,15 +20,12 @@ public class Startup public void ConfigureServices(IServiceCollection services) => services.AddMvc(); - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. -#if NET6_0_OR_GREATER public void Configure(IApplicationBuilder app, IWebHostEnvironment env) -#else - public void Configure(IApplicationBuilder app, IHostingEnvironment env) -#endif { +#pragma warning disable CS0618 // Type or member is obsolete app.UseAllElasticApm(_configuration); +#pragma warning restore CS0618 // Type or member is obsolete ConfigureAllExceptAgent(app); } @@ -38,13 +35,9 @@ public static void ConfigureAllExceptAgent(IApplicationBuilder app) app.UseHttpsRedirection(); -#if NET6_0_OR_GREATER app.UseRouting(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); -#else - app.UseMvc(); -#endif } } }