Skip to content

Commit

Permalink
Add IServiceCollection extension methods to register ApmAgent (#2326)
Browse files Browse the repository at this point in the history
* Add IServiceCollection extension methods to register ApmAgent

* Reverting updates to sample apps

* Update doc

* Remove unused NET5_0 compiler directives
  • Loading branch information
stevejgordon authored Apr 8, 2024
1 parent 049c290 commit 1651da8
Show file tree
Hide file tree
Showing 51 changed files with 1,084 additions and 395 deletions.
25 changes: 23 additions & 2 deletions ElasticApmAgent.sln
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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))));
}
Expand All @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions docs/setup-asp-net-core.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
[float]
==== Quick start

[NOTE]
[IMPORTANT]
--
We suggest using the approach described in the <<setup-dotnet-net-core, .NET Core setup instructions>>,
to register the agent on `IHostBuilder`, as opposed to using `IApplicationBuilder` as described below.
We strongly suggest using the approach described in the <<setup-dotnet-net-core, .NET applications using Microsoft.Extensions.Hosting instructions>>,
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.
--
Expand Down
160 changes: 109 additions & 51 deletions docs/setup-dotnet-net-core.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,156 @@
:dot: .

[[setup-dotnet-net-core]]
=== .NET Core
=== .NET Core and .NET 5+

[float]
==== Quick start

On .NET Core, the agent can be registered on the `IHostBuilder`. This applies to both ASP.NET Core and to other .NET Core applications that depend on `IHostBuilder`, like https://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services[background tasks]. In this case, you need to reference the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package.
In .NET (Core) applications using `Microsoft.Extensions.Hosting`, the agent can be registered on the `IServiceCollection`. This applies to ASP.NET Core and to other .NET applications that depend on the hosting APIs, such as those created using the https://learn.microsoft.com/en-us/dotnet/core/extensions/workers[worker services] template.

The simplest way to enable the agent and its instrumentations requires a reference to the {nuget}/Elastic.Apm.NetCoreAll[`Elastic.Apm.NetCoreAll`] package.

[source,xml]
----
<PackageReference Include="Elastic.Apm.NetCoreAll" Version="<LATEST>" /> <1>
----
<1> Replace the `<LATEST>` 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<Worker>();
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 <<public-api>>

*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<Startup>(); })
.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 <<public-api>>
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<Startup>(); })
.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<Worker>();
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

Expand All @@ -109,9 +167,9 @@ set DOTNET_STARTUP_HOOKS=<path-to-agent>\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.
--
1 change: 0 additions & 1 deletion docs/setup.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ On **.NET Core 3.0+ or .NET 5+**, the agent supports auto instrumentation withou
any recompilation of your projects. See <<zero-code-change-setup, Zero code change setup on .NET Core>>
for more details.


[float]
== Get started

Expand Down
10 changes: 3 additions & 7 deletions sample/WebApiExample/Controllers/WeatherForecastController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<WeatherForecastController> _logger;

public WeatherForecastController(ILogger<WeatherForecastController> logger) => _logger = logger;
];

[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get() =>
Expand Down
10 changes: 1 addition & 9 deletions sample/WebApiExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,28 +1,20 @@
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();
app.UseSwaggerUI();
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
6 changes: 3 additions & 3 deletions sample/WebApiExample/WebApiExample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="8.0.2" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\integrations\Elastic.Apm.AspNetCore\Elastic.Apm.AspNetCore.csproj" />
<ProjectReference Include="..\..\src\integrations\Elastic.Apm.NetCoreAll\Elastic.Apm.NetCoreAll.csproj" />
</ItemGroup>

</Project>
8 changes: 8 additions & 0 deletions sample/WebApiExample/appsettings.Development.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
Loading

0 comments on commit 1651da8

Please sign in to comment.