diff --git a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs index eceb93d32034..3571fa7f2d02 100644 --- a/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs +++ b/src/Identity/Core/src/IdentityServiceCollectionExtensions.cs @@ -89,6 +89,7 @@ public static class IdentityServiceCollectionExtensions // Hosting doesn't add IHttpContextAccessor by default services.AddHttpContextAccessor(); + services.AddMetrics(); // Identity services services.TryAddScoped, UserValidator>(); services.TryAddScoped, PasswordValidator>(); diff --git a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj index 5f13920b61f5..82a750e1393c 100644 --- a/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj +++ b/src/Identity/Core/src/Microsoft.AspNetCore.Identity.csproj @@ -23,6 +23,7 @@ + diff --git a/src/Identity/Core/src/SignInManager.cs b/src/Identity/Core/src/SignInManager.cs index 2391e36c0e72..4d139a408828 100644 --- a/src/Identity/Core/src/SignInManager.cs +++ b/src/Identity/Core/src/SignInManager.cs @@ -3,6 +3,7 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using System.Linq; using System.Security.Claims; using System.Text; @@ -63,7 +64,8 @@ public SignInManager(UserManager userManager, Logger = logger; _schemes = schemes; _confirmation = confirmation; - _metrics = userManager.ServiceProvider?.GetService(); + // SignInManagerMetrics created from constructor because of difficulties registering internal type. + _metrics = userManager.ServiceProvider?.GetService() is { } factory ? new SignInManagerMetrics(factory) : null; _passkeyHandler = userManager.ServiceProvider?.GetService>(); } diff --git a/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs b/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs index 28415c47318d..b7ed1a7652c7 100644 --- a/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs +++ b/src/Identity/Extensions.Core/src/IdentityServiceCollectionExtensions.cs @@ -35,6 +35,7 @@ public static IdentityBuilder AddIdentityCore(this IServiceCollection ser { // Services identity depends on services.AddOptions().AddLogging(); + services.AddMetrics(); // Services used by identity services.TryAddScoped, UserValidator>(); diff --git a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj index 538396395fc3..534517af55d2 100644 --- a/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj +++ b/src/Identity/Extensions.Core/src/Microsoft.Extensions.Identity.Core.csproj @@ -12,6 +12,7 @@ + diff --git a/src/Identity/Extensions.Core/src/UserManager.cs b/src/Identity/Extensions.Core/src/UserManager.cs index e184534b41ce..cfb6f717ccff 100644 --- a/src/Identity/Extensions.Core/src/UserManager.cs +++ b/src/Identity/Extensions.Core/src/UserManager.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.Metrics; using System.Linq; using System.Runtime.InteropServices; using System.Security.Claims; @@ -85,7 +86,8 @@ public UserManager(IUserStore store, ErrorDescriber = errors; Logger = logger; ServiceProvider = services; - _metrics = services?.GetService(); + // UserManagerMetrics created from constructor because of difficulties registering internal type. + _metrics = services?.GetService() is { } factory ? new UserManagerMetrics(factory) : null; if (userValidators != null) { diff --git a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs index fbce29d8f6e4..c9bb52658877 100644 --- a/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs +++ b/src/Identity/test/Identity.FunctionalTests/MapIdentityApiTests.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Diagnostics.Metrics; using System.Globalization; using System.Net; using System.Net.Http; @@ -23,6 +24,8 @@ using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Time.Testing; using Microsoft.Net.Http.Headers; @@ -1274,6 +1277,44 @@ public async Task MustSendValidRequestToSendEmailChangeConfirmation() AssertOk(await client.PostAsJsonAsync("/identity/login", new { Email = newEmail, Password = newPassword })); } + [Fact] + public async Task MetricsAreRecordedForUserManagerAndSignInManager() + { + // Arrange + await using var app = await CreateAppAsync(services => + { + AddIdentityApiEndpoints(services); + }); + var meterFactory = app.Services.GetRequiredService(); + + using var client = app.GetTestClient(); + + using var userCreateCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Identity", "aspnetcore.identity.user.create.duration"); + using var signInAuthenticateCollector = new MetricCollector(meterFactory, "Microsoft.AspNetCore.Identity", "aspnetcore.identity.sign_in.authenticate.duration"); + + // Act - Register a new user + AssertOkAndEmpty(await client.PostAsJsonAsync("/identity/register", new { Email, Password })); + + // Assert - UserManager metrics are recorded + var userCreateMeasurement = Assert.Single(userCreateCollector.GetMeasurementSnapshot()); + Assert.True(userCreateMeasurement.Value > 0); + Assert.Equal("Identity.DefaultUI.WebSite.ApplicationUser", (string?)userCreateMeasurement.Tags["aspnetcore.identity.user_type"]); + Assert.Equal("success", (string?)userCreateMeasurement.Tags["aspnetcore.identity.result"]); + + // Act - Login with the user + AssertOk(await client.PostAsJsonAsync("/identity/login", new { Email, Password })); + + // Assert - SignInManager metrics are recorded + var signInMeasurements = signInAuthenticateCollector.GetMeasurementSnapshot(); + Assert.NotEmpty(signInMeasurements); + var signInMeasurement = signInMeasurements.Last(); + Assert.True(signInMeasurement.Value > 0); + Assert.Equal("Identity.DefaultUI.WebSite.ApplicationUser", (string?)signInMeasurement.Tags["aspnetcore.identity.user_type"]); + Assert.Equal("Identity.Bearer", (string?)signInMeasurement.Tags["aspnetcore.authentication.scheme"]); + Assert.Equal("success", (string?)signInMeasurement.Tags["aspnetcore.identity.sign_in.result"]); + Assert.Equal("password", (string?)signInMeasurement.Tags["aspnetcore.identity.sign_in.type"]); + } + private async Task CreateAppAsync(Action? configureServices, bool autoStart = true) where TUser : class, new() where TContext : DbContext diff --git a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj index 88bbd988b1f3..e8ec634bb07d 100644 --- a/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj +++ b/src/Identity/test/Identity.FunctionalTests/Microsoft.AspNetCore.Identity.FunctionalTests.csproj @@ -1,4 +1,4 @@ - + true $(DefaultNetCoreTargetFramework) @@ -22,6 +22,7 @@ + diff --git a/src/Identity/test/Shared/MockHelpers.cs b/src/Identity/test/Shared/MockHelpers.cs index f6d374c6769e..309cbfc07196 100644 --- a/src/Identity/test/Shared/MockHelpers.cs +++ b/src/Identity/test/Shared/MockHelpers.cs @@ -22,7 +22,6 @@ public static Mock> MockUserManager( var services = new ServiceCollection(); if (meterFactory != null) { - services.AddSingleton(); services.AddSingleton(meterFactory); } if (passkeyHandler != null) @@ -61,8 +60,6 @@ public static UserManager TestUserManager(IUserStore store var services = new ServiceCollection(); if (meterFactory != null) { - services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(meterFactory); } var userManager = new UserManager(store, options.Object, new PasswordHasher(),