Skip to content

Commit

Permalink
Merge pull request #73 from Lombiq/issue/NEST-490
Browse files Browse the repository at this point in the history
NEST-490: Double-logging in Application Insights
  • Loading branch information
Piedone authored Nov 12, 2023
2 parents bd27c54 + 338df26 commit 14bcc71
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 59 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
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 Microsoft.Extensions.Options;
using System.Linq;
using ApplicationInsightsFeatureIds = Lombiq.Hosting.Azure.ApplicationInsights.Constants.FeatureIds;

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 OrchardCoreBuilder AddOrchardCoreApplicationInsightsTelemetry(
this OrchardCoreBuilder builder,
IConfiguration configurationManager)
{
var services = builder.ApplicationServices;
services.AddApplicationInsightsTelemetry(configurationManager);

// Create a temporary ServiceProvider to configure ApplicationInsightsServiceOptions.
using var serviceProvider = services.BuildServiceProvider();
var applicationInsightsServiceOptions = serviceProvider
.GetService<IOptions<ApplicationInsightsServiceOptions>>()?.Value;

var applicationInsightsOptions = new ApplicationInsightsOptions();
var applicationInsightsConfigSection = configurationManager
.GetSection("OrchardCore:Lombiq_Hosting_Azure_ApplicationInsights");
applicationInsightsConfigSection.Bind(applicationInsightsOptions);

if (string.IsNullOrEmpty(applicationInsightsServiceOptions?.ConnectionString) &&
#pragma warning disable CS0618 // Type or member is obsolete
string.IsNullOrEmpty(applicationInsightsServiceOptions?.InstrumentationKey) &&
#pragma warning restore CS0618 // Type or member is obsolete
!applicationInsightsOptions.EnableOfflineOperation)
{
// Removing ITelemetryModules from the service collection is necessary because otherwise the modules will be
// used, even if there is no ConnectionString or InstrumentationKey added.
var descriptorsToDelete = services.Where(descriptor => descriptor.ServiceType == typeof(ITelemetryModule)).ToArray();

foreach (var descriptor in descriptorsToDelete)
{
services.Remove(descriptor);
}

return builder;
}

services.AddApplicationInsightsTelemetryProcessor<TelemetryFilter>();

services.Configure<ApplicationInsightsOptions>(applicationInsightsConfigSection);

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

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

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

if (applicationInsightsOptions.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;
});
}

builder.AddTenantFeatures(ApplicationInsightsFeatureIds.Default);

return builder;
}
}
77 changes: 20 additions & 57 deletions Lombiq.Hosting.Azure.ApplicationInsights/Startup.cs
Original file line number Diff line number Diff line change
@@ -1,95 +1,58 @@
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) &&
#pragma warning disable CS0618 // Type or member is obsolete
string.IsNullOrEmpty(_applicationInsightsServiceOptions.InstrumentationKey) &&
#pragma warning restore CS0618 // Type or member is obsolete
!_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
18 changes: 16 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,21 @@ 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
.AddOrchardCms(orchardCoreBuilder =>
{
orchardCoreBuilder.AddOrchardCoreApplicationInsightsTelemetry(configuration);
});
```

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

0 comments on commit 14bcc71

Please sign in to comment.