Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.5" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="xunit" Version="2.5.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.3" />
</ItemGroup>

<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\CPS.ComplexCases.API.HttpTelemetry\CPS.ComplexCases.API.HttpTelemetry.csproj" />
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;

namespace CPS.ComplexCases.API.HttpTelemetry.Tests;

public class HealthCheckTelemetryHostRegistrationTests
{
[Fact]
public void Register_WrapsTelemetryConfiguration_AndDropsStatusHostTelemetry()
{
var channel = new StubChannel();
var services = new ServiceCollection();
services.AddSingleton(_ => new TelemetryConfiguration { TelemetryChannel = channel });

HealthCheckTelemetryHostRegistration.Register(services);

var config = services.BuildServiceProvider().GetRequiredService<TelemetryConfiguration>();
var client = new TelemetryClient(config);

var statusRequest = new RequestTelemetry
{
Name = "Status",
Url = new Uri("https://example.com/api/status")
};
var invokeDependency = new DependencyTelemetry
{
Name = "Invoke",
Type = "InProc"
};
invokeDependency.Context.Operation.Name = "Functions.Status";

client.TrackRequest(statusRequest);
client.Track(invokeDependency);
client.TrackRequest(new RequestTelemetry
{
Name = "GetCase",
Url = new Uri("https://example.com/api/v1/cases/1")
});
client.Flush();

var remaining = Assert.Single(channel.Items);
var remainingRequest = Assert.IsType<RequestTelemetry>(remaining);
Assert.Equal("GetCase", remainingRequest.Name);
}

[Fact]
public void Register_AddsTelemetryModule_WhenConfigurationIsNotRegistered()
{
var services = new ServiceCollection();

HealthCheckTelemetryHostRegistration.Register(services);

Assert.Contains(services, descriptor =>
descriptor.ServiceType == typeof(ITelemetryModule)
&& descriptor.ImplementationType == typeof(HealthCheckTelemetryModule));
}

[Fact]
public void Module_Initialize_DropsStatusRequestsAfterBuild()
{
var channel = new StubChannel();
var config = new TelemetryConfiguration { TelemetryChannel = channel };

new HealthCheckTelemetryModule().Initialize(config);
config.TelemetryProcessorChainBuilder.Build();

var client = new TelemetryClient(config);
client.TrackRequest(new RequestTelemetry
{
Name = "Status",
Url = new Uri("https://example.com/api/status")
});
client.TrackRequest(new RequestTelemetry
{
Name = "GetCase",
Url = new Uri("https://example.com/api/v1/cases/1")
});
client.Flush();

var remaining = Assert.Single(channel.Items);
var remainingRequest = Assert.IsType<RequestTelemetry>(remaining);
Assert.Equal("GetCase", remainingRequest.Name);
}

private sealed class StubChannel : ITelemetryChannel
{
public List<ITelemetry> Items { get; } = [];
public bool? DeveloperMode { get; set; }
public string? EndpointAddress { get; set; }
public void Send(ITelemetry item) => Items.Add(item);
public void Flush() { }
public void Dispose() { }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

<PropertyGroup>

<VersionPrefix>1.0.41</VersionPrefix>
<VersionPrefix>1.0.42</VersionPrefix>

<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<DefineConstants>$(DefineConstants);HOST_TELEMETRY</DefineConstants>

<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<Authors>Brian Dunnington</Authors>
Expand All @@ -26,5 +27,11 @@
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\CPS.ComplexCases.Common\Telemetry\HealthCheckTelemetryFilter.cs" Link="HealthCheckTelemetryFilter.cs" />
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>CPS.ComplexCases.API.HttpTelemetry.Tests</_Parameter1>
</AssemblyAttribute>
</ItemGroup>

</Project>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.DependencyInjection;

namespace CPS.ComplexCases.API.HttpTelemetry;

internal static class HealthCheckTelemetryHostRegistration
{
public static void Register(IServiceCollection services)
{
var configDescriptor = services.FirstOrDefault(descriptor => descriptor.ServiceType == typeof(TelemetryConfiguration));
if (configDescriptor?.ImplementationFactory is not null)
{
var implFactory = configDescriptor.ImplementationFactory;
services.Remove(configDescriptor);
services.AddSingleton(provider =>
{
var config = (TelemetryConfiguration)implFactory(provider);
Attach(config);
return config;
});
return;
}

if (configDescriptor?.ImplementationInstance is TelemetryConfiguration instance)
{
Attach(instance);
return;
}

services.AddSingleton<ITelemetryModule, HealthCheckTelemetryModule>();
}

internal static void Attach(TelemetryConfiguration config)
{
config.TelemetryProcessorChainBuilder
.Use(next => new HealthCheckTelemetryFilter(next))
.Build();
}
}

internal sealed class HealthCheckTelemetryModule : ITelemetryModule
{
public void Initialize(TelemetryConfiguration configuration)
{
configuration.TelemetryProcessorChainBuilder.Use(next => new HealthCheckTelemetryFilter(next));
}
}
2 changes: 2 additions & 0 deletions backend/CPS.ComplexCases.API.HttpTelemetry/Startup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public void Configure(WebJobsBuilderContext context, IWebJobsBuilder builder)
builder.AddExtension<HttpTelemetryExtensionConfigProvider>();
builder.Services.AddSingleton<ITelemetryInitializer, HttpTelemetryInitializer>();
builder.Services.AddHttpContextAccessor();
// Runs in the Functions host process. The worker ITelemetryProcessor cannot see host Request/Invoke telemetry.
HealthCheckTelemetryHostRegistration.Register(builder.Services);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using Microsoft.Azure.Functions.Worker.Extensions.Abstractions;

[assembly: ExtensionInformation("CPS.ComplexCases.API.HttpTelemetry", "1.0.41")]
[assembly: ExtensionInformation("CPS.ComplexCases.API.HttpTelemetry", "1.0.42")]
namespace CPS.ComplexCases.API.Attributes
{
[AttributeUsage(AttributeTargets.Parameter)]
Expand Down
8 changes: 4 additions & 4 deletions backend/CPS.ComplexCases.API/CPS.ComplexCases.API.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
<PackageReference Include="Aspose.Imaging" Version="25.2.0" />
<PackageReference Include="Aspose.PDF.Drawing" Version="25.1.0" />
<PackageReference Include="Aspose.Slides.NET" Version="25.2.0" />
<PackageReference Include="Aspose.Words" Version="25.2.0" />
<PackageReference Include="Ardalis.SmartEnum" Version="8.2.0" />
<PackageReference Include="Aspose.Words" Version="25.2.0" />
<PackageReference Include="Ardalis.SmartEnum" Version="8.2.0" />
<PackageReference Include="Azure.Storage.Blobs" Version="12.27.0" />
<PackageReference Include="FluentValidation" Version="11.11.0" />
<PackageReference Include="FluentValidation.DependencyInjectionExtensions" Version="11.11.0" />
Expand All @@ -33,7 +33,7 @@
<PackageReference Include="Microsoft.Extensions.Azure" Version="1.10.0" />
<PackageReference Include="Microsoft.Extensions.Http.Resilience" Version="10.5.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="10.0.5" />
<PackageReference Include="CPS.ComplexCases.API.HttpTelemetry" Version="1.0.41" />
<PackageReference Include="CPS.ComplexCases.API.HttpTelemetry" Version="1.0.42" />
<PackageReference Include="ThrottlingTroll.AzureFunctions" Version="9.1.0" />
</ItemGroup>
<ItemGroup>
Expand Down Expand Up @@ -68,4 +68,4 @@
<None Include="SourceFiles/SecurityGroupMappings.PreProd.json" CopyToOutputDirectory="Always" />
<None Include="SourceFiles/SecurityGroupMappings.Production.json" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@ public HealthCheckTelemetryFilterTests()
_sut = new HealthCheckTelemetryFilter(_next.Object);
}

[Fact]
public void Process_FiltersRequest_WhenFunctionNameIsStatus()
[Theory]
[InlineData("Status")]
[InlineData("Functions.Status")]
[InlineData("functions.status")]
[InlineData(" Functions.Status ")]
public void Process_FiltersRequest_WhenFunctionNameIsStatus(string functionName)
{
var request = new RequestTelemetry { Name = "GetCase" };
request.Properties["AzureFunctions_FunctionName"] = "Status";
request.Properties["AzureFunctions_FunctionName"] = functionName;

AssertFiltered(request);
}
Expand Down Expand Up @@ -58,6 +62,7 @@ public void Process_FiltersRequest_WhenUrlIsPeriodicHealthEndpoint(string url)

[Theory]
[InlineData("Status")]
[InlineData("Functions.Status")]
[InlineData("GET /api/status")]
[InlineData("GET /api/health")]
public void Process_FiltersRequest_WhenNameIsPeriodicHealthEndpoint(string name)
Expand All @@ -83,6 +88,8 @@ public void Process_FiltersDependency_WhenDataIsPeriodicHealthEndpoint(string da
}

[Theory]
[InlineData("Status")]
[InlineData("Functions.Status")]
[InlineData("GET /api/status")]
[InlineData("GET /api/health")]
[InlineData("/api/status")]
Expand All @@ -109,6 +116,7 @@ public void Process_FiltersDependency_WhenTargetIsPeriodicHealthEndpoint()

[Theory]
[InlineData("Status")]
[InlineData("Functions.Status")]
[InlineData("GET /api/status")]
[InlineData("GET /api/health")]
public void Process_FiltersDependency_WhenOperationNameIsPeriodicHealthEndpoint(string operationName)
Expand Down Expand Up @@ -139,6 +147,15 @@ public void Process_DoesNotFilterRequest_WhenPathIsTransferStatus()
AssertPassedThrough(request);
}

[Fact]
public void Process_DoesNotFilterRequest_WhenFunctionNameIsPrefixedTransferStatus()
{
var request = new RequestTelemetry { Name = "GetTransferStatus" };
request.Properties["AzureFunctions_FunctionName"] = "Functions.GetTransferStatus";

AssertPassedThrough(request);
}

[Fact]
public void Process_DoesNotFilterDependency_WhenDataIsTransferStatus()
{
Expand All @@ -154,6 +171,16 @@ public void Process_DoesNotFilterDependency_WhenDataIsTransferStatus()
AssertPassedThrough(dependency);
}

[Fact]
public void Process_DoesNotFilterDependency_WhenNameIsInvokeWithoutHealthOperation()
{
AssertPassedThrough(new DependencyTelemetry
{
Name = "Invoke",
Type = "InProc"
});
}

[Fact]
public void Process_DoesNotFilterDependency_WhenUnrelatedToHealthEndpoints()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;

#if HOST_TELEMETRY
namespace CPS.ComplexCases.API.HttpTelemetry;
#else
namespace CPS.ComplexCases.Common.Telemetry;
#endif

public class HealthCheckTelemetryFilter(ITelemetryProcessor next) : ITelemetryProcessor
{
Expand All @@ -13,6 +17,7 @@ public class HealthCheckTelemetryFilter(ITelemetryProcessor next) : ITelemetryPr
];

private const string StatusFunctionName = "Status";
private const string FunctionsPrefix = "Functions.";

private readonly ITelemetryProcessor _next = next;

Expand Down Expand Up @@ -58,15 +63,28 @@ private static bool ShouldFilter(ITelemetry item)
if (item is DependencyTelemetry dependency)
{
return IsPeriodicHealthProbeValue(dependency.Data)
|| IsPeriodicHealthProbeValue(dependency.Name)
|| IsPeriodicHealthOperation(dependency.Name)
|| IsPeriodicHealthProbeValue(dependency.Target);
}

return false;
}

private static bool IsPeriodicHealthFunctionName(string? name) =>
string.Equals(name, StatusFunctionName, StringComparison.OrdinalIgnoreCase);
private static bool IsPeriodicHealthFunctionName(string? name)
{
if (string.IsNullOrWhiteSpace(name))
{
return false;
}

var trimmed = name.Trim();
if (trimmed.StartsWith(FunctionsPrefix, StringComparison.OrdinalIgnoreCase))
{
trimmed = trimmed[FunctionsPrefix.Length..];
}

return string.Equals(trimmed, StatusFunctionName, StringComparison.OrdinalIgnoreCase);
}

private static bool IsPeriodicHealthOperation(string? name) =>
IsPeriodicHealthFunctionName(name) || IsPeriodicHealthProbeValue(name);
Expand Down
Loading
Loading