Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NEST-490: Double-logging in Application Insights #73

Merged
merged 26 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using Lombiq.Hosting.Azure.ApplicationInsights;
using Lombiq.Hosting.Azure.ApplicationInsights.Services;
using Lombiq.Hosting.Azure.ApplicationInsights.TelemetryInitializers;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DependencyCollector;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse;
using Microsoft.Extensions.Configuration;
using System.Linq;

namespace Microsoft.Extensions.DependencyInjection;

public static class ApplicationInsightsInitializerExtensions
{
/// <summary>
/// Initializes Application Insights for Orchard Core. Should be used in the application Program.cs file.
/// </summary>
public static IServiceCollection AddOrchardCoreApplicationInsightsTelemetry(
Piedone marked this conversation as resolved.
Show resolved Hide resolved
this IServiceCollection services,
IConfiguration configurationManager)
{
var connectionString = configurationManager.GetValue<string>("ApplicationInsights:ConnectionString");
Piedone marked this conversation as resolved.
Show resolved Hide resolved
var enableOfflineOperation = configurationManager
.GetValue<bool>("OrchardCore:Lombiq_Hosting_Azure_ApplicationInsights:EnableOfflineOperation");
if (string.IsNullOrEmpty(connectionString) && !enableOfflineOperation)
{
return services;
}

services.AddApplicationInsightsTelemetry(configurationManager);
services.AddApplicationInsightsTelemetryProcessor<TelemetryFilter>();

// Since the below AI configuration needs to happen during app startup in ConfigureServices() we can't use an
// injected IOptions<T> here but need to directly bind to ApplicationInsightsOptions.
var options = new ApplicationInsightsOptions();
var configSection = configurationManager.GetSection("OrchardCore:Lombiq_Hosting_Azure_ApplicationInsights");
configSection.Bind(options);
services.Configure<ApplicationInsightsOptions>(configSection);

services.ConfigureTelemetryModule<DependencyTrackingTelemetryModule>(
(module, _) => module.EnableSqlCommandTextInstrumentation = options.EnableSqlCommandTextInstrumentation);

services.ConfigureTelemetryModule<QuickPulseTelemetryModule>(
(module, _) => module.AuthenticationApiKey = options.QuickPulseTelemetryModuleAuthenticationApiKey);

services.AddSingleton<ITelemetryInitializer, UserContextPopulatingTelemetryInitializer>();
services.AddSingleton<ITelemetryInitializer, ShellNamePopulatingTelemetryInitializer>();
services.AddScoped<ITrackingScriptFactory, TrackingScriptFactory>();

if (options.EnableOfflineOperation)
{
foreach (var descriptor in services.Where(descriptor => descriptor.ServiceType == typeof(ITelemetryChannel)).ToArray())
{
services.Remove(descriptor);
}

services.AddSingleton<ITelemetryChannel, NullTelemetryChannel>();
services.Configure<ApplicationInsightsServiceOptions>(
options =>
{
options.EnableAppServicesHeartbeatTelemetryModule = false;
options.EnableHeartbeat = false;
options.EnableQuickPulseMetricStream = false;
});
}

return services;
}
}
74 changes: 17 additions & 57 deletions Lombiq.Hosting.Azure.ApplicationInsights/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,95 +1,55 @@
using Lombiq.Hosting.Azure.ApplicationInsights.Services;
using Lombiq.Hosting.Azure.ApplicationInsights.TelemetryInitializers;
using Microsoft.ApplicationInsights.AspNetCore.Extensions;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DependencyCollector;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.ApplicationInsights.Extensibility.PerfCounterCollector.QuickPulse;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.ApplicationInsights;
using Microsoft.Extensions.Options;
using OrchardCore.BackgroundTasks;
using OrchardCore.Environment.Shell.Configuration;
using OrchardCore.Modules;
using System;
using System.Linq;

namespace Lombiq.Hosting.Azure.ApplicationInsights;

public class Startup : StartupBase
{
private readonly IShellConfiguration _shellConfiguration;
private readonly ApplicationInsightsOptions _applicationInsightsOptions;
private readonly ApplicationInsightsServiceOptions _applicationInsightsServiceOptions;

public Startup(IShellConfiguration shellConfiguration) =>
_shellConfiguration = shellConfiguration;
public Startup(
IOptions<ApplicationInsightsOptions> applicationInsightsOptions,
IOptions<ApplicationInsightsServiceOptions> applicationInsightsServiceOptions)
{
_applicationInsightsOptions = applicationInsightsOptions.Value;
_applicationInsightsServiceOptions = applicationInsightsServiceOptions.Value;
}

public override void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(_shellConfiguration);
services.AddApplicationInsightsTelemetryProcessor<TelemetryFilter>();

// Since the below AI configuration needs to happen during app startup in ConfigureServices() we can't use an
// injected IOptions<T> here but need to directly bind to ApplicationInsightsOptions.
var options = new ApplicationInsightsOptions();
var configSection = _shellConfiguration.GetSection("Lombiq_Hosting_Azure_ApplicationInsights");
configSection.Bind(options);
services.Configure<ApplicationInsightsOptions>(configSection);

services.ConfigureTelemetryModule<DependencyTrackingTelemetryModule>(
(module, _) => module.EnableSqlCommandTextInstrumentation = options.EnableSqlCommandTextInstrumentation);

services.ConfigureTelemetryModule<QuickPulseTelemetryModule>(
(module, _) => module.AuthenticationApiKey = options.QuickPulseTelemetryModuleAuthenticationApiKey);
if (string.IsNullOrEmpty(_applicationInsightsServiceOptions.ConnectionString) &&
!_applicationInsightsOptions.EnableOfflineOperation)
{
return;
}

services.AddSingleton<ITelemetryInitializer, UserContextPopulatingTelemetryInitializer>();
services.AddSingleton<ITelemetryInitializer, ShellNamePopulatingTelemetryInitializer>();
services.Configure<MvcOptions>((options) => options.Filters.Add(typeof(TrackingScriptInjectingFilter)));
services.AddScoped<ITrackingScriptFactory, TrackingScriptFactory>();

if (options.EnableLoggingTestBackgroundTask)
if (_applicationInsightsOptions.EnableLoggingTestBackgroundTask)
{
services.AddSingleton<IBackgroundTask, LoggingTestBackgroundTask>();
}

if (options.EnableBackgroundTaskTelemetryCollection)
if (_applicationInsightsOptions.EnableBackgroundTaskTelemetryCollection)
{
services.AddScoped<IBackgroundTaskEventHandler, BackgroundTaskTelemetryEventHandler>();
}

if (options.EnableOfflineOperation)
{
foreach (var descriptor in services.Where(descriptor => descriptor.ServiceType == typeof(ITelemetryChannel)).ToArray())
{
services.Remove(descriptor);
}

services.AddSingleton<ITelemetryChannel, NullTelemetryChannel>();
services.Configure<ApplicationInsightsServiceOptions>(
options =>
{
options.EnableAppServicesHeartbeatTelemetryModule = false;
options.EnableHeartbeat = false;
options.EnableQuickPulseMetricStream = false;
});
}
}

public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider)
{
var options = serviceProvider.GetService<IOptions<ApplicationInsightsOptions>>().Value;
var options = serviceProvider.GetRequiredService<IOptions<ApplicationInsightsOptions>>().Value;

if (options.EnableLoggingTestMiddleware) app.UseMiddleware<LoggingTestMiddleware>();

var loggerFactory = serviceProvider.GetService<ILoggerFactory>();

// For some reason the AI logger provider needs to be re-registered here otherwise no logging will happen.
var aiProvider = serviceProvider.GetServices<ILoggerProvider>().Single(provider => provider is ApplicationInsightsLoggerProvider);
loggerFactory.AddProvider(aiProvider);
// There seems to be no way to apply a default filtering to this from code. Going via services.AddLogging() in
// ConfigureServices() doesn't work, neither there. The rules get saved but are never applied. The default
////{
Expand Down
14 changes: 12 additions & 2 deletions Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

## About

This [Orchard Core](https://orchardcore.net/) module enables easy integration of [Azure Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) telemetry into Orchard. Just install the module, configure the instrumentation key from a configuration source (like the _appsettings.json_ file) as normally for AI, and collected data will start appearing in the Azure Portal. Check out the module's demo [here](https://www.youtube.com/watch?v=NKKR4R3UPog), and the Orchard Harvest 2023 conference talk about automated QA in Orchard Core [here](https://youtu.be/CHdhwD2NHBU). Note that this module has an Orchard 1 version in the [dev-orchard-1 branch](https://github.com/Lombiq/Orchard-Azure-Application-Insights/tree/dev-orchard-1).
This [Orchard Core](https://orchardcore.net/) module enables easy integration of [Azure Application Insights](https://docs.microsoft.com/en-us/azure/azure-monitor/app/app-insights-overview) telemetry into Orchard. Just install the module, configure the instrumentation key from a configuration source (like the _appsettings.json_ file) as normally for AI, and use the `AddOrchardCoreApplicationInsightsTelemetry()` extension method in your Web project's _Program.cs_ file and collected data will start appearing in the Azure Portal. Check out the module's demo [here](https://www.youtube.com/watch?v=NKKR4R3UPog), and the Orchard Harvest 2023 conference talk about automated QA in Orchard Core [here](https://youtu.be/CHdhwD2NHBU). Note that this module has an Orchard 1 version in the [dev-orchard-1 branch](https://github.com/Lombiq/Orchard-Azure-Application-Insights/tree/dev-orchard-1).

What kind of data is collected from the telemetry and available for inspection in the Azure Portal?

Expand Down Expand Up @@ -51,7 +51,17 @@ Configure the built-in AI options as detailed in the [AI docs](https://docs.micr

```

In a multi-tenant setup you can configure different instrumentation keys to collect request tracking and client-side tracking data on different tenants, just follow [the Orchard Core configuration docs](https://docs.orchardcore.net/en/latest/docs/reference/core/Configuration/).
Add the AI services to the service collection in your Web project's _Program.cs_ file:

```csharp
var builder = WebApplication.CreateBuilder(args);

var configuration = builder.Configuration;

builder.Services.AddOrchardCoreApplicationInsightsTelemetry(configuration).AddOrchardCms();
```
wAsnk marked this conversation as resolved.
Show resolved Hide resolved

Note that due to how the Application Insights .NET SDK works, telemetry can only be collected for the whole app at once; collecting telemetry separately for each tenant is not supported.

When using the full CMS approach of Orchard Core (i.e. not decoupled or headless) then the client-side tracking script will be automatically injected as a head script. Otherwise, you can create it with `ITrackingScriptFactory`.

Expand Down