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

Otel Metrics in the UI #1469

Merged
merged 6 commits into from
Nov 28, 2023
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
5 changes: 4 additions & 1 deletion hosts/AspNetIdentity/Pages/Account/Login/Index.cshtml.cs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading