Skip to content

Commit

Permalink
Merge pull request #1469 from DuendeSoftware/anders/otel-metrics-UI
Browse files Browse the repository at this point in the history
Otel Metrics in the UI
  • Loading branch information
AndersAbel committed Nov 28, 2023
2 parents ef64ef1 + dee60a9 commit 237ffca
Show file tree
Hide file tree
Showing 33 changed files with 763 additions and 21 deletions.
5 changes: 4 additions & 1 deletion hosts/AspNetIdentity/Pages/Account/Login/Index.cshtml.cs
Expand Up @@ -102,6 +102,7 @@ public async Task<IActionResult> OnPost()
{
var user = await _userManager.FindByNameAsync(Input.Username!);
await _events.RaiseAsync(new UserLoginSuccessEvent(user!.UserName, user.Id, user.UserName, clientId: context?.Client.ClientId));
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);

if (context != null)
{
Expand Down Expand Up @@ -135,7 +136,9 @@ public async Task<IActionResult> OnPost()
}
}

await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId:context?.Client.ClientId));
const string error = "invalid credentials";
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId:context?.Client.ClientId));
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
}

Expand Down
7 changes: 4 additions & 3 deletions hosts/AspNetIdentity/Pages/Account/Logout/Index.cshtml.cs
Expand Up @@ -75,12 +75,13 @@ public async Task<IActionResult> OnPost()
// delete local authentication cookie
await _signInManager.SignOutAsync();

// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));

// see if we need to trigger federated logout
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;

// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
Telemetry.Metrics.UserLogout(idp);

// if it's a local login we can ignore this workflow
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
{
Expand Down
4 changes: 4 additions & 0 deletions hosts/AspNetIdentity/Pages/Consent/Index.cshtml.cs
Expand Up @@ -66,6 +66,7 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
Expand All @@ -88,6 +89,9 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
Expand Down
4 changes: 4 additions & 0 deletions hosts/AspNetIdentity/Pages/Device/Index.cshtml.cs
Expand Up @@ -78,6 +78,7 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
Expand All @@ -100,6 +101,9 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
Expand Down
Expand Up @@ -97,6 +97,7 @@ public async Task<IActionResult> OnGet()
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.Id, user.UserName, true, context?.Client.ClientId));
Telemetry.Metrics.UserLogin(context?.Client.ClientId, provider!);

if (context != null)
{
Expand Down
1 change: 1 addition & 0 deletions hosts/AspNetIdentity/Pages/Grants/Index.cshtml.cs
Expand Up @@ -75,6 +75,7 @@ public async Task<IActionResult> OnPost()
{
await _interaction.RevokeUserConsentAsync(ClientId);
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), ClientId));
Telemetry.Metrics.GrantsRevoked(ClientId);

return RedirectToPage("/Grants/Index");
}
Expand Down
168 changes: 168 additions & 0 deletions hosts/AspNetIdentity/Pages/Telemetry.cs
@@ -0,0 +1,168 @@
using System.Diagnostics.Metrics;

namespace IdentityServerHost.Pages;

#pragma warning disable CA1034 // Nested types should not be visible
#pragma warning disable CA1724 // Type names should not match namespaces

/// <summary>
/// Telemetry helpers for the UI
/// </summary>
public static class Telemetry
{
private static readonly string ServiceVersion = typeof(Telemetry).Assembly.GetName().Version!.ToString();

/// <summary>
/// Service name for telemetry.
/// </summary>
public static readonly string ServiceName = typeof(Telemetry).Assembly.GetName().Name!;

/// <summary>
/// Metrics configuration
/// </summary>
public static class Metrics
{
/// <summary>
/// Name of Counters
/// </summary>
public static class Counters
{
/// <summary>
/// consent_granted
/// </summary>
public const string ConsentGranted = "consent_granted";

/// <summary>
/// consent_denied
/// </summary>
public const string ConsentDenied = "consent_denied";

/// <summary>
/// grants_revoked
/// </summary>
public const string GrantsRevoked = "grants_revoked";

/// <summary>
/// user_login
/// </summary>
public const string UserLogin = "user_login";

/// <summary>
/// user_login_failure
/// </summary>
public const string UserLoginFailure = "user_login_failure";

/// <summary>
/// user_logout
/// </summary>
public const string UserLogout = "user_logout";
}

/// <summary>
/// Name of tags
/// </summary>
public static class Tags
{
/// <summary>
/// client
/// </summary>
public const string Client = "client";

/// <summary>
/// error
/// </summary>
public const string Error = "error";

/// <summary>
/// idp
/// </summary>
public const string Idp = "idp";

/// <summary>
/// remember
/// </summary>
public const string Remember = "remember";

/// <summary>
/// scope
/// </summary>
public const string Scope = "scope";
}

/// <summary>
/// Meter for the IdentityServer host project
/// </summary>
private static readonly Meter Meter = new Meter(ServiceName, ServiceVersion);

private static Counter<long> ConsentGrantedCounter = Meter.CreateCounter<long>(Counters.ConsentGranted);

/// <summary>
/// Helper method to increase <see cref="Counters.ConsentGranted"/> counter. The scopes
/// are expanded and called one by one to not cause a combinatory explosion of scopes.
/// </summary>
/// <param name="clientId">Client id</param>
/// <param name="scopes">Scope names. Each element is added on it's own to the counter</param>
public static void ConsentGranted(string clientId, IEnumerable<string> scopes, bool remember)
{
ArgumentNullException.ThrowIfNull(scopes);
foreach(var scope in scopes)
{
ConsentGrantedCounter.Add(1, new(Tags.Client, clientId), new(Tags.Scope, scope), new(Tags.Remember, remember));
}
}

private static Counter<long> ConsentDeniedCounter = Meter.CreateCounter<long>(Counters.ConsentDenied);

/// <summary>
/// Helper method to increase <see cref="Counters.ConsentDenied"/> counter. The scopes
/// are expanded and called one by one to not cause a combinatory explosion of scopes.
/// </summary>
/// <param name="clientId">Client id</param>
/// <param name="scopes">Scope names. Each element is added on it's own to the counter</param>
public static void ConsentDenied(string clientId, IEnumerable<string> scopes)
{
ArgumentNullException.ThrowIfNull(scopes);
foreach (var scope in scopes)
{
ConsentDeniedCounter.Add(1, new(Tags.Client, clientId), new(Tags.Scope, scope));
}
}

private static Counter<long> GrantsRevokedCounter = Meter.CreateCounter<long>(Counters.GrantsRevoked);

/// <summary>
/// Helper method to increase the <see cref="Counters.GrantsRevoked"/> counter.
/// </summary>
/// <param name="clientId">Client id to revoke for, or null for all.</param>
public static void GrantsRevoked(string? clientId)
=> GrantsRevokedCounter.Add(1, tag: new(Tags.Client, clientId));

private static Counter<long> UserLoginCounter = Meter.CreateCounter<long>(Counters.UserLogin);

/// <summary>
/// Helper method to increase <see cref="Counters.UserLogin"/> counter.
/// </summary>
/// <param name="clientId">Client Id, if available</param>
public static void UserLogin(string? clientId, string idp)
=> UserLoginCounter.Add(1, new(Tags.Client, clientId), new(Tags.Idp, idp));

private static Counter<long> UserLoginFailureCounter = Meter.CreateCounter<long>(Counters.UserLoginFailure);

/// <summary>
/// Helper method to increase <see cref="Counters.UserLoginFailure" counter.
/// </summary>
/// <param name="clientId">Client Id, if available</param>
/// <param name="error">Error message</param>
public static void UserLoginFailure(string? clientId, string idp, string error)
=> UserLoginFailureCounter.Add(1, new(Tags.Client, clientId), new(Tags.Idp, idp), new(Tags.Error, error));

private static Counter<long> UserLogoutCounter = Meter.CreateCounter<long>(Counters.UserLogout);

/// <summary>
/// Helper method to increase the <see cref="Counters.UserLogout"/> counter.
/// </summary>
/// <param name="idp">Idp/authentication scheme for external authentication, or "local" for built in.</param>
public static void UserLogout(string? idp)
=> UserLogoutCounter.Add(1, tag: new(Tags.Idp, idp));
}
}
5 changes: 4 additions & 1 deletion hosts/Configuration/Pages/Account/Login/Index.cshtml.cs
Expand Up @@ -97,6 +97,7 @@ public async Task<IActionResult> OnPost()
{
var user = _users.FindByUsername(Input.Username);
await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username, clientId: context?.Client.ClientId));
Telemetry.Metrics.UserLogin(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider);

// only set explicit expiration here if user chooses "remember me".
// otherwise we rely upon expiration configured in cookie middleware.
Expand Down Expand Up @@ -144,7 +145,9 @@ public async Task<IActionResult> OnPost()
}
}

await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, "invalid credentials", clientId:context?.Client.ClientId));
const string error = "invalid credentials";
await _events.RaiseAsync(new UserLoginFailureEvent(Input.Username, error, clientId:context?.Client.ClientId));
Telemetry.Metrics.UserLoginFailure(context?.Client.ClientId, IdentityServerConstants.LocalIdentityProvider, error);
ModelState.AddModelError(string.Empty, LoginOptions.InvalidCredentialsErrorMessage);
}

Expand Down
7 changes: 4 additions & 3 deletions hosts/Configuration/Pages/Account/Logout/Index.cshtml.cs
Expand Up @@ -71,12 +71,13 @@ public async Task<IActionResult> OnPost()
// delete local authentication cookie
await HttpContext.SignOutAsync();

// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));

// see if we need to trigger federated logout
var idp = User.FindFirst(JwtClaimTypes.IdentityProvider)?.Value;

// raise the logout event
await _events.RaiseAsync(new UserLogoutSuccessEvent(User.GetSubjectId(), User.GetDisplayName()));
Telemetry.Metrics.UserLogout(idp);

// if it's a local login we can ignore this workflow
if (idp != null && idp != Duende.IdentityServer.IdentityServerConstants.LocalIdentityProvider)
{
Expand Down
4 changes: 4 additions & 0 deletions hosts/Configuration/Pages/Ciba/Consent.cshtml.cs
Expand Up @@ -69,6 +69,7 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
Expand All @@ -90,6 +91,9 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, result.ScopesValuesConsented, false));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, result.ScopesValuesConsented, false);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
Expand Down
4 changes: 4 additions & 0 deletions hosts/Configuration/Pages/Consent/Index.cshtml.cs
Expand Up @@ -66,6 +66,7 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
Expand All @@ -88,6 +89,9 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
Expand Down
4 changes: 4 additions & 0 deletions hosts/Configuration/Pages/Device/Index.cshtml.cs
Expand Up @@ -78,6 +78,7 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentDeniedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues));
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
}
// user clicked 'yes' - validate the data
else if (Input.Button == "yes")
Expand All @@ -100,6 +101,9 @@ public async Task<IActionResult> OnPost()

// emit event
await _events.RaiseAsync(new ConsentGrantedEvent(User.GetSubjectId(), request.Client.ClientId, request.ValidatedResources.RawScopeValues, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent));
Telemetry.Metrics.ConsentGranted(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
Telemetry.Metrics.ConsentDenied(request.Client.ClientId, denied);
}
else
{
Expand Down
1 change: 1 addition & 0 deletions hosts/Configuration/Pages/ExternalLogin/Callback.cshtml.cs
Expand Up @@ -106,6 +106,7 @@ public async Task<IActionResult> OnGet()
// check if external login is in the context of an OIDC request
var context = await _interaction.GetAuthorizationContextAsync(returnUrl);
await _events.RaiseAsync(new UserLoginSuccessEvent(provider, providerUserId, user.SubjectId, user.Username, true, context?.Client.ClientId));
Telemetry.Metrics.UserLogin(context?.Client.ClientId, provider!);

if (context != null)
{
Expand Down
1 change: 1 addition & 0 deletions hosts/Configuration/Pages/Grants/Index.cshtml.cs
Expand Up @@ -75,6 +75,7 @@ public async Task<IActionResult> OnPost()
{
await _interaction.RevokeUserConsentAsync(ClientId);
await _events.RaiseAsync(new GrantsRevokedEvent(User.GetSubjectId(), ClientId));
Telemetry.Metrics.GrantsRevoked(ClientId);

return RedirectToPage("/Grants/Index");
}
Expand Down

0 comments on commit 237ffca

Please sign in to comment.