From c589d9ea960d9d0303ee2aecf91830db958559b8 Mon Sep 17 00:00:00 2001 From: Steve Gordon Date: Thu, 4 Apr 2024 14:38:42 +0100 Subject: [PATCH] Add IServiceCollection extension methods to register ApmAgent --- .gitignore | 1 - 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 +- .../Properties/launchSettings.json | 13 + sample/WebApiExample/WebApiExample.csproj | 6 +- .../appsettings.Development.json | 8 + sample/WebApiExample/appsettings.json | 12 + sample/WorkerServiceSample/Program.cs | 11 + .../Properties/launchSettings.json | 12 + sample/WorkerServiceSample/Worker.cs | 21 + .../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 + .../Elastic.Apm.MongoDb/LICENSE | 416 +++++++++--------- .../ApplicationBuilderExtensions.cs | 12 +- .../ServiceCollectionExtensions.cs | 38 ++ .../ApmService.cs | 23 + .../Elastic.Apm.Extensions.Hosting.csproj | 4 +- .../HostBuilderExtensions.cs | 8 +- .../NetCoreLogger.cs | 58 ++- .../ServiceCollectionExtensions.cs | 121 +++++ .../ApmErrorLoggingProvider.cs | 9 +- .../ApplicationBuilderExtensions.cs | 3 + .../Elastic.Apm.NetCoreAll.csproj | 2 +- .../HostBuilderExtensions.cs | 3 + .../ServiceCollectionExtensions.cs | 80 ++++ .../Properties/launchSettings.json | 12 + .../Elastic.Apm.Tests.Utilities.csproj | 1 + .../MockPayloadSender.cs | 30 +- .../Properties/launchSettings.json | 12 + .../grpc/GrpcServiceSample/Program.cs | 4 +- .../Properties/launchSettings.json | 12 + .../grpc/GrpcServiceSample/Startup.cs | 2 +- .../Properties/launchSettings.json | 12 + .../ApplicationBuilderExtensionLoggingTest.cs | 4 +- .../Properties/launchSettings.json | 12 + ...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 | 96 ++++ .../HostingTestApp/HostingTestApp.csproj | 20 + .../applications/HostingTestApp/Program.cs | 263 +++++++++++ .../Properties/launchSettings.json | 13 + .../HostingTestApp/appsettings.json | 10 + .../Properties/launchSettings.json | 12 + .../SampleAspNetCoreApp/Startup.cs | 10 +- .../SampleConsoleNetCoreApp/HostedService.cs | 7 +- .../SampleConsoleNetCoreApp/Program.cs | 10 +- .../Properties/launchSettings.json | 12 + .../applications/WebApiSample/Startup.cs | 8 +- .../Properties/launchSettings.json | 12 + 61 files changed, 1420 insertions(+), 576 deletions(-) create mode 100644 sample/WebApiExample/Properties/launchSettings.json 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/Properties/launchSettings.json 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 create mode 100644 test/Elastic.Apm.Tests.MockApmServer/Properties/launchSettings.json create mode 100644 test/iis/Elastic.Apm.AspNetFullFramework.Tests/Properties/launchSettings.json create mode 100644 test/instrumentations/grpc/GrpcServiceSample/Properties/launchSettings.json create mode 100644 test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Properties/launchSettings.json create mode 100644 test/integrations/Elastic.Apm.AspNetCore.Tests/Properties/launchSettings.json 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/Properties/launchSettings.json create mode 100644 test/integrations/applications/HostingTestApp/appsettings.json create mode 100644 test/integrations/applications/SampleAspNetCoreApp/Properties/launchSettings.json create mode 100644 test/integrations/applications/WebApiSample/Properties/launchSettings.json create mode 100644 test/startuphook/Elastic.Apm.StartupHook.Sample/Properties/launchSettings.json diff --git a/.gitignore b/.gitignore index e7a9cbc81c..0b876c3b0d 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,6 @@ BenchmarkDotNet.Artifacts/ project.lock.json project.fragment.lock.json artifacts/ -**/Properties/launchSettings.json # StyleCop StyleCopReport.xml diff --git a/ElasticApmAgent.sln b/ElasticApmAgent.sln index aa8ce27e20..65142c8b82 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 d47924d1fe..4dced3465d 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 83d6a16f39..9008a84b12 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 7a361a0973..c9f258ba9d 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 with Microsoft.Extensions.Hosting [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 d89e8f8d67..09818553ce 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 3aafe321d0..6086949038 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 757c870559..fc7700b3e4 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/Properties/launchSettings.json b/sample/WebApiExample/Properties/launchSettings.json new file mode 100644 index 0000000000..4a409df045 --- /dev/null +++ b/sample/WebApiExample/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "WebApiExample": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "WeatherForecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:57899;http://localhost:57900" + } + } +} \ No newline at end of file diff --git a/sample/WebApiExample/WebApiExample.csproj b/sample/WebApiExample/WebApiExample.csproj index 9af40e9907..325a13544d 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 0000000000..b2dcdb6742 --- /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 0000000000..451014b32c --- /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 0000000000..44a68e11ee --- /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/Properties/launchSettings.json b/sample/WorkerServiceSample/Properties/launchSettings.json new file mode 100644 index 0000000000..144f8da06d --- /dev/null +++ b/sample/WorkerServiceSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "profiles": { + "WorkerServiceSample": { + "commandName": "Project", + "dotnetRunMessages": true, + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + } + } + } +} diff --git a/sample/WorkerServiceSample/Worker.cs b/sample/WorkerServiceSample/Worker.cs new file mode 100644 index 0000000000..35d8ed9db6 --- /dev/null +++ b/sample/WorkerServiceSample/Worker.cs @@ -0,0 +1,21 @@ +using System.Diagnostics; + +namespace WorkerServiceSample +{ + public class Worker(IHttpClientFactory httpClientFactory) : BackgroundService + { + private readonly IHttpClientFactory _httpClientFactory = httpClientFactory; + private static readonly ActivitySource ActivitySource = new("MyActivitySource"); + + 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 0000000000..7d9d055a66 --- /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 0000000000..b2dcdb6742 --- /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 0000000000..a43cea94f5 --- /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 de238058c2..417d36a27f 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 47589f7ebb..e88add7f0c 100644 --- a/src/Elastic.Apm/Elastic.Apm.csproj +++ b/src/Elastic.Apm/Elastic.Apm.csproj @@ -61,6 +61,7 @@ + diff --git a/src/instrumentations/Elastic.Apm.MongoDb/LICENSE b/src/instrumentations/Elastic.Apm.MongoDb/LICENSE index ad5f27ac60..950d224c10 100644 --- a/src/instrumentations/Elastic.Apm.MongoDb/LICENSE +++ b/src/instrumentations/Elastic.Apm.MongoDb/LICENSE @@ -1,208 +1,208 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright 2018 Elasticsearch BV - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - -========== -Elastic.Apm.MongoDb ----------- - -The code for Elastic.Apm.MongoDb is based on the elastic-apm-mongo project by Vadim Hatsura (@vhatsura), -licensed under the Apache 2.0 License. https://github.com/vhatsura/elastic-apm-mongo +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018 Elasticsearch BV + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +========== +Elastic.Apm.MongoDb +---------- + +The code for Elastic.Apm.MongoDb is based on the elastic-apm-mongo project by Vadim Hatsura (@vhatsura), +licensed under the Apache 2.0 License. https://github.com/vhatsura/elastic-apm-mongo diff --git a/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.AspNetCore/ApplicationBuilderExtensions.cs index 97be645cf7..0a260f0d81 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 0000000000..d8d010e436 --- /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 0000000000..6c9db17f2d --- /dev/null +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/ApmService.cs @@ -0,0 +1,23 @@ +// 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(IApmAgent agent) : IHostedService +{ +#pragma warning disable IDE0052 // Remove unread private members + private readonly IApmAgent _agent = agent; +#pragma warning restore IDE0052 // Remove unread private members + + 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 f816251c24..8725f6c151 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 abd132e21f..d33a62d73d 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 ea9dc55ed5..a57918ad30 100644 --- a/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs +++ b/src/integrations/Elastic.Apm.Extensions.Hosting/NetCoreLogger.cs @@ -1,43 +1,37 @@ +// 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 -{ - internal class NetCoreLogger : IApmLogger - { - private readonly ILogger _logger; +namespace Elastic.Apm.Extensions.Hosting; - public NetCoreLogger(ILoggerFactory loggerFactory) => - _logger = loggerFactory?.CreateLogger("Elastic.Apm") ?? throw new ArgumentNullException(nameof(loggerFactory)); +internal sealed class NetCoreLogger(ILoggerFactory loggerFactory) : IApmLogger +{ + private readonly ILogger _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 0000000000..89fda76cfc --- /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 Elastic.Apm; +using System; +using Elastic.Apm.Extensions.Hosting; +using Elastic.Apm.Config; +using Elastic.Apm.Extensions.Hosting.Config; +using Microsoft.Extensions.Hosting; +using Elastic.Apm.Logging; +using Elastic.Apm.DiagnosticSource; +using Elastic.Apm.NetCoreAll; +using Elastic.Apm.Report; +using Microsoft.Extensions.Logging; +using Elastic.Apm.Extensions.Logging; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; +using Microsoft.Extensions.DependencyInjection.Extensions; + +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 86d0ca2371..b9ccceacac 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 @@ -7,11 +6,9 @@ namespace Elastic.Apm.Extensions.Logging { - internal class ApmErrorLoggingProvider : ILoggerProvider + internal class ApmErrorLoggingProvider(IApmAgent apmAgent) : ILoggerProvider { - private readonly IApmAgent _apmAgent; - - public ApmErrorLoggingProvider(IApmAgent apmAgent) => _apmAgent = apmAgent; + private readonly IApmAgent _apmAgent = apmAgent; public ILogger CreateLogger(string categoryName) => new ApmErrorLogger(_apmAgent); diff --git a/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs b/src/integrations/Elastic.Apm.NetCoreAll/ApplicationBuilderExtensions.cs index 1d24ac0ba2..534fbfd31d 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 93f71bac82..1d1ba10d28 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 e58f93bb66..917eb2187f 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 0000000000..7e6f92db4c --- /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.DiagnosticSource; +using Elastic.Apm.AspNetCore.DiagnosticListener; +using Elastic.Apm.Azure.CosmosDb; +using Elastic.Apm.Azure.ServiceBus; +using Elastic.Apm.Azure.Storage; +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.MockApmServer/Properties/launchSettings.json b/test/Elastic.Apm.Tests.MockApmServer/Properties/launchSettings.json new file mode 100644 index 0000000000..9c89ee736a --- /dev/null +++ b/test/Elastic.Apm.Tests.MockApmServer/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elastic.Apm.Tests.MockApmServer": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50611;http://localhost:50612" + } + } +} \ No newline at end of file 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 1d00e874c7..30f0746d11 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 715ecf7375..36ccf1f028 100644 --- a/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs +++ b/test/Elastic.Apm.Tests.Utilities/MockPayloadSender.cs @@ -20,25 +20,25 @@ 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/iis/Elastic.Apm.AspNetFullFramework.Tests/Properties/launchSettings.json b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Properties/launchSettings.json new file mode 100644 index 0000000000..bb30c90d56 --- /dev/null +++ b/test/iis/Elastic.Apm.AspNetFullFramework.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elastic.Apm.AspNetFullFramework.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50613;http://localhost:50614" + } + } +} \ No newline at end of file diff --git a/test/instrumentations/grpc/GrpcServiceSample/Program.cs b/test/instrumentations/grpc/GrpcServiceSample/Program.cs index 39b4c977f4..1bb840405a 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/Properties/launchSettings.json b/test/instrumentations/grpc/GrpcServiceSample/Properties/launchSettings.json new file mode 100644 index 0000000000..b2b2cf6007 --- /dev/null +++ b/test/instrumentations/grpc/GrpcServiceSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "GrpcServiceSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50718;http://localhost:50719" + } + } +} \ No newline at end of file diff --git a/test/instrumentations/grpc/GrpcServiceSample/Startup.cs b/test/instrumentations/grpc/GrpcServiceSample/Startup.cs index 25d59ca32a..cb58a8f650 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.Static.Tests/Properties/launchSettings.json b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Properties/launchSettings.json new file mode 100644 index 0000000000..603e78d094 --- /dev/null +++ b/test/integrations/Elastic.Apm.AspNetCore.Static.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elastic.Apm.AspNetCore.Static.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50720;http://localhost:50721" + } + } +} \ No newline at end of file diff --git a/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs b/test/integrations/Elastic.Apm.AspNetCore.Tests/ApplicationBuilderExtensionLoggingTest.cs index 849f2e77f3..8c0480aea6 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/Properties/launchSettings.json b/test/integrations/Elastic.Apm.AspNetCore.Tests/Properties/launchSettings.json new file mode 100644 index 0000000000..3978781b7d --- /dev/null +++ b/test/integrations/Elastic.Apm.AspNetCore.Tests/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elastic.Apm.AspNetCore.Tests": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50606;http://localhost:50608" + } + } +} \ No newline at end of file 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 305f1f5b48..d7340831fd 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 5adcc5b4c1..0000000000 --- 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 0000000000..44ae0ed798 --- /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 cae863cabe..9838570d29 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 4b38f6fb41..2dcd2d9b8b 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 0000000000..e3957faba8 --- /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 0000000000..f15ac4f23a --- /dev/null +++ b/test/integrations/Elastic.Apm.Extensions.Tests.Shared/ExtensionsTestHelpers.cs @@ -0,0 +1,96 @@ +// 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(ITestOutputHelper testOutput) +{ + private readonly string _workingDirectory = Path.Combine(SolutionPaths.Root, "test", "integrations", "applications", "HostingTestApp"); + private readonly ITestOutputHelper _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 0000000000..bff7b18bc1 --- /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 0000000000..b671e58f27 --- /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.DiagnosticSource; +using Test; +using Elastic.Apm.Extensions.Hosting; +using Elastic.Apm.Api; +using SampleConsoleNetCoreApp; +using Elastic.Apm.Tests.Utilities; +using Elastic.Apm.Report; + +// 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/Properties/launchSettings.json b/test/integrations/applications/HostingTestApp/Properties/launchSettings.json new file mode 100644 index 0000000000..4aa5e56685 --- /dev/null +++ b/test/integrations/applications/HostingTestApp/Properties/launchSettings.json @@ -0,0 +1,13 @@ +{ + "profiles": { + "HostingTestApp": { + "commandName": "Project", + "commandLineArgs": "unset false true true", + "environmentVariables": { + "DOTNET_ENVIRONMENT": "Development" + }, + "dotnetRunMessages": true + } + }, + "$schema": "http://json.schemastore.org/launchsettings.json" +} \ No newline at end of file diff --git a/test/integrations/applications/HostingTestApp/appsettings.json b/test/integrations/applications/HostingTestApp/appsettings.json new file mode 100644 index 0000000000..8dcee0555d --- /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/Properties/launchSettings.json b/test/integrations/applications/SampleAspNetCoreApp/Properties/launchSettings.json new file mode 100644 index 0000000000..002df7429d --- /dev/null +++ b/test/integrations/applications/SampleAspNetCoreApp/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "SampleAspNetCoreApp": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50609;http://localhost:50610" + } + } +} \ No newline at end of file diff --git a/test/integrations/applications/SampleAspNetCoreApp/Startup.cs b/test/integrations/applications/SampleAspNetCoreApp/Startup.cs index 8cd0a830b4..47d28a5a78 100644 --- a/test/integrations/applications/SampleAspNetCoreApp/Startup.cs +++ b/test/integrations/applications/SampleAspNetCoreApp/Startup.cs @@ -33,6 +33,9 @@ public void ConfigureServices(IServiceCollection services) public static void ConfigureServicesExceptMvc(IServiceCollection services) { + if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SKIP_AGENT_REGISTRATION"))) + services.AddAllElasticApm(); + const string connection = @"Data Source=blogging.db"; services.AddDbContext (options => options.UseSqlite(connection)); @@ -59,12 +62,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) #else public void Configure(IApplicationBuilder app, IHostingEnvironment env) #endif - { - if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("SKIP_AGENT_REGISTRATION"))) - app.UseAllElasticApm(Configuration); - - ConfigureAllExceptAgent(app); - } + => ConfigureAllExceptAgent(app); public static void ConfigureAllExceptAgent(IApplicationBuilder app) { diff --git a/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs b/test/integrations/applications/SampleConsoleNetCoreApp/HostedService.cs index 1db3ccde0f..be007b2bd2 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 b51d0cae1f..4c982a12ec 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/Properties/launchSettings.json b/test/integrations/applications/WebApiSample/Properties/launchSettings.json new file mode 100644 index 0000000000..92eefece67 --- /dev/null +++ b/test/integrations/applications/WebApiSample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "WebApiSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50605;http://localhost:50607" + } + } +} \ No newline at end of file diff --git a/test/integrations/applications/WebApiSample/Startup.cs b/test/integrations/applications/WebApiSample/Startup.cs index 7003f51859..33a1a144d3 100644 --- a/test/integrations/applications/WebApiSample/Startup.cs +++ b/test/integrations/applications/WebApiSample/Startup.cs @@ -2,7 +2,6 @@ // 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.NetCoreAll; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; @@ -18,7 +17,7 @@ public class Startup // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) => - services.AddMvc(); + services.AddAllElasticApm().AddMvc(); // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -27,10 +26,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) #else public void Configure(IApplicationBuilder app, IHostingEnvironment env) #endif - { - app.UseAllElasticApm(_configuration); - ConfigureAllExceptAgent(app); - } + => ConfigureAllExceptAgent(app); public static void ConfigureAllExceptAgent(IApplicationBuilder app) { diff --git a/test/startuphook/Elastic.Apm.StartupHook.Sample/Properties/launchSettings.json b/test/startuphook/Elastic.Apm.StartupHook.Sample/Properties/launchSettings.json new file mode 100644 index 0000000000..0cf8952fe2 --- /dev/null +++ b/test/startuphook/Elastic.Apm.StartupHook.Sample/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "Elastic.Apm.StartupHook.Sample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:50722;http://localhost:50723" + } + } +} \ No newline at end of file