diff --git a/hosts/Configuration/Pages/Account/Login/Index.cshtml.cs b/hosts/Configuration/Pages/Account/Login/Index.cshtml.cs
index 9e07c5b65..249f75b05 100644
--- a/hosts/Configuration/Pages/Account/Login/Index.cshtml.cs
+++ b/hosts/Configuration/Pages/Account/Login/Index.cshtml.cs
@@ -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.
@@ -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);
         }
 
diff --git a/hosts/Configuration/Pages/Account/Logout/Index.cshtml.cs b/hosts/Configuration/Pages/Account/Logout/Index.cshtml.cs
index 9f9c4dc1e..aef1de3d8 100644
--- a/hosts/Configuration/Pages/Account/Logout/Index.cshtml.cs
+++ b/hosts/Configuration/Pages/Account/Logout/Index.cshtml.cs
@@ -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)
             {
diff --git a/hosts/Configuration/Pages/Ciba/Consent.cshtml.cs b/hosts/Configuration/Pages/Ciba/Consent.cshtml.cs
index 4ebf8cd53..f615fdf05 100644
--- a/hosts/Configuration/Pages/Ciba/Consent.cshtml.cs
+++ b/hosts/Configuration/Pages/Ciba/Consent.cshtml.cs
@@ -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.ConsentDeniedEvent(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
         }
         // user clicked 'yes' - validate the data
         else if (Input.Button == "yes")
@@ -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.ConsentGrantedEvent(request.Client.ClientId, result.ScopesValuesConsented, false);
+                var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(result.ScopesValuesConsented);
+                Telemetry.Metrics.ConsentDeniedEvent(request.Client.ClientId, denied);
             }
             else
             {
diff --git a/hosts/Configuration/Pages/Consent/Index.cshtml.cs b/hosts/Configuration/Pages/Consent/Index.cshtml.cs
index 2abdee20a..357691a39 100644
--- a/hosts/Configuration/Pages/Consent/Index.cshtml.cs
+++ b/hosts/Configuration/Pages/Consent/Index.cshtml.cs
@@ -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.ConsentDeniedEvent(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
         }
         // user clicked 'yes' - validate the data
         else if (Input.Button == "yes")
@@ -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.ConsentGrantedEvent(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
+                var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
+                Telemetry.Metrics.ConsentDeniedEvent(request.Client.ClientId, denied);
             }
             else
             {
diff --git a/hosts/Configuration/Pages/Device/Index.cshtml.cs b/hosts/Configuration/Pages/Device/Index.cshtml.cs
index f84bbb159..0d1721e44 100644
--- a/hosts/Configuration/Pages/Device/Index.cshtml.cs
+++ b/hosts/Configuration/Pages/Device/Index.cshtml.cs
@@ -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.ConsentDeniedEvent(request.Client.ClientId, request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName));
         }
         // user clicked 'yes' - validate the data
         else if (Input.Button == "yes")
@@ -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.ConsentGrantedEvent(request.Client.ClientId, grantedConsent.ScopesValuesConsented, grantedConsent.RememberConsent);
+                var denied = request.ValidatedResources.ParsedScopes.Select(s => s.ParsedName).Except(grantedConsent.ScopesValuesConsented);
+                Telemetry.Metrics.ConsentDeniedEvent(request.Client.ClientId, denied);
             }
             else
             {
diff --git a/hosts/Configuration/Pages/ExternalLogin/Callback.cshtml.cs b/hosts/Configuration/Pages/ExternalLogin/Callback.cshtml.cs
index 937ced8e9..281131bc2 100644
--- a/hosts/Configuration/Pages/ExternalLogin/Callback.cshtml.cs
+++ b/hosts/Configuration/Pages/ExternalLogin/Callback.cshtml.cs
@@ -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)
         {
diff --git a/hosts/Configuration/Pages/Grants/Index.cshtml.cs b/hosts/Configuration/Pages/Grants/Index.cshtml.cs
index dceca1034..a38545744 100644
--- a/hosts/Configuration/Pages/Grants/Index.cshtml.cs
+++ b/hosts/Configuration/Pages/Grants/Index.cshtml.cs
@@ -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");
     }
diff --git a/hosts/Configuration/Pages/Telemetry.cs b/hosts/Configuration/Pages/Telemetry.cs
new file mode 100644
index 000000000..15a993c6d
--- /dev/null
+++ b/hosts/Configuration/Pages/Telemetry.cs
@@ -0,0 +1,170 @@
+using Duende.IdentityServer.Events;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+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 ConsentGrantedEvent(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 ConsentDeniedEvent(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));
+    }
+}