From 0cf9afa0b92ea3fef0d1e68331cea88297e77467 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 14:48:53 +0100 Subject: [PATCH 1/9] Add metrics for introspection requests --- crates/handlers/src/oauth2/introspection.rs | 56 ++++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/oauth2/introspection.rs b/crates/handlers/src/oauth2/introspection.rs index 608871e54..a97c62ab6 100644 --- a/crates/handlers/src/oauth2/introspection.rs +++ b/crates/handlers/src/oauth2/introspection.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use std::sync::LazyLock; + use axum::{Json, extract::State, http::HeaderValue, response::IntoResponse}; use hyper::{HeaderMap, StatusCode}; use mas_axum_utils::{ @@ -24,9 +26,21 @@ use oauth2_types::{ requests::{IntrospectionRequest, IntrospectionResponse}, scope::ScopeToken, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; -use crate::{ActivityTracker, impl_from_error_for_route}; +use crate::{ActivityTracker, METER, impl_from_error_for_route}; + +static INTROSPECTION_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.oauth2.introspection_request") + .with_description("Number of OAuth 2.0 introspection requests") + .with_unit("{request}") + .build() +}); + +const KIND: Key = Key::from_static_str("kind"); +const ACTIVE: Key = Key::from_static_str("active"); #[derive(Debug, Error)] pub enum RouteError { @@ -118,6 +132,7 @@ impl IntoResponse for RouteError { ), ) .into_response(), + Self::UnknownToken(_) | Self::UnexpectedTokenType | Self::InvalidToken(_) @@ -125,7 +140,12 @@ impl IntoResponse for RouteError { | Self::InvalidCompatSession | Self::InvalidOAuthSession | Self::InvalidTokenFormat(_) - | Self::CantEncodeDeviceID(_) => Json(INACTIVE).into_response(), + | Self::CantEncodeDeviceID(_) => { + INTROSPECTION_COUNTER.add(1, &[KeyValue::new(ACTIVE.clone(), false)]); + + Json(INACTIVE).into_response() + } + Self::NotAllowed => ( StatusCode::UNAUTHORIZED, Json(ClientError::from(ClientErrorCode::AccessDenied)), @@ -275,6 +295,14 @@ pub(crate) async fn post( .record_oauth2_session(&clock, &session, ip) .await; + INTROSPECTION_COUNTER.add( + 1, + &[ + KeyValue::new(KIND, "oauth2_access_token"), + KeyValue::new(ACTIVE, true), + ], + ); + IntrospectionResponse { active: true, scope: Some(session.scope), @@ -338,6 +366,14 @@ pub(crate) async fn post( .record_oauth2_session(&clock, &session, ip) .await; + INTROSPECTION_COUNTER.add( + 1, + &[ + KeyValue::new(KIND, "oauth2_refresh_token"), + KeyValue::new(ACTIVE, true), + ], + ); + IntrospectionResponse { active: true, scope: Some(session.scope), @@ -412,6 +448,14 @@ pub(crate) async fn post( .record_compat_session(&clock, &session, ip) .await; + INTROSPECTION_COUNTER.add( + 1, + &[ + KeyValue::new(KIND, "compat_access_token"), + KeyValue::new(ACTIVE, true), + ], + ); + IntrospectionResponse { active: true, scope: Some(scope), @@ -488,6 +532,14 @@ pub(crate) async fn post( .record_compat_session(&clock, &session, ip) .await; + INTROSPECTION_COUNTER.add( + 1, + &[ + KeyValue::new(KIND, "compat_refresh_token"), + KeyValue::new(ACTIVE, true), + ], + ); + IntrospectionResponse { active: true, scope: Some(scope), From 5345b3d68850de3bc52a3c75f1df4d12119cae3f Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 16:14:41 +0100 Subject: [PATCH 2/9] Record compat login metrics --- crates/handlers/src/compat/login.rs | 35 +++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/compat/login.rs b/crates/handlers/src/compat/login.rs index 7c1a6d97a..7711f53aa 100644 --- a/crates/handlers/src/compat/login.rs +++ b/crates/handlers/src/compat/login.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use axum::{ Json, @@ -27,6 +27,7 @@ use mas_storage::{ }, user::{UserPasswordRepository, UserRepository}, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use rand::{CryptoRng, RngCore}; use serde::{Deserialize, Serialize}; use serde_with::{DurationMilliSeconds, serde_as, skip_serializing_none}; @@ -35,10 +36,20 @@ use zeroize::Zeroizing; use super::MatrixError; use crate::{ - BoundActivityTracker, Limiter, RequesterFingerprint, impl_from_error_for_route, + BoundActivityTracker, Limiter, METER, RequesterFingerprint, impl_from_error_for_route, passwords::PasswordManager, rate_limit::PasswordCheckLimitedError, }; +static LOGIN_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.compat.login_request") + .with_description("How many compatibility login requests have happened") + .with_unit("{request}") + .build() +}); +const TYPE: Key = Key::from_static_str("type"); +const RESULT: Key = Key::from_static_str("result"); + #[derive(Debug, Serialize)] #[serde(tag = "type")] enum LoginType { @@ -123,6 +134,16 @@ pub enum Credentials { Unsupported, } +impl Credentials { + fn login_type(&self) -> &'static str { + match self { + Self::Password { .. } => "m.login.password", + Self::Token { .. } => "m.login.token", + Self::Unsupported => "unsupported", + } + } +} + #[derive(Debug, Serialize, Deserialize)] #[serde(tag = "type")] pub enum Identifier { @@ -192,6 +213,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let event_id = sentry::capture_error(&self); + LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let response = match self { Self::Internal(_) | Self::SessionNotFound | Self::ProvisionDeviceFailed(_) => { MatrixError { @@ -278,6 +300,7 @@ pub(crate) async fn post( WithRejection(Json(input), _): WithRejection, RouteError>, ) -> Result { let user_agent = user_agent.map(|ua| UserAgent::parse(ua.as_str().to_owned())); + let login_type = input.credentials.login_type(); let (mut session, user) = match (password_manager.is_enabled(), input.credentials) { ( true, @@ -360,6 +383,14 @@ pub(crate) async fn post( .record_compat_session(&clock, &session) .await; + LOGIN_COUNTER.add( + 1, + &[ + KeyValue::new(TYPE, login_type), + KeyValue::new(RESULT, "success"), + ], + ); + Ok(Json(ResponseBody { access_token: access_token.token, device_id: session.device, From c70103adb4da6289d0eeee81b8b6561d1a13e075 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 16:20:20 +0100 Subject: [PATCH 3/9] Record compat logout metrics --- crates/handlers/src/compat/logout.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/handlers/src/compat/logout.rs b/crates/handlers/src/compat/logout.rs index 20df8e682..557dbaef9 100644 --- a/crates/handlers/src/compat/logout.rs +++ b/crates/handlers/src/compat/logout.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use std::sync::LazyLock; + use axum::{Json, response::IntoResponse}; use axum_extra::typed_header::TypedHeader; use headers::{Authorization, authorization::Bearer}; @@ -15,10 +17,20 @@ use mas_storage::{ compat::{CompatAccessTokenRepository, CompatSessionRepository}, queue::{QueueJobRepositoryExt as _, SyncDevicesJob}, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; use super::MatrixError; -use crate::{BoundActivityTracker, impl_from_error_for_route}; +use crate::{BoundActivityTracker, METER, impl_from_error_for_route}; + +static LOGOUT_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.compat.logout_request") + .with_description("How many compatibility logout request have happened") + .with_unit("{request}") + .build() +}); +const RESULT: Key = Key::from_static_str("result"); #[derive(Error, Debug)] pub enum RouteError { @@ -40,6 +52,7 @@ impl_from_error_for_route!(mas_storage::RepositoryError); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let event_id = sentry::capture_error(&self); + LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let response = match self { Self::Internal(_) => MatrixError { errcode: "M_UNKNOWN", @@ -113,5 +126,7 @@ pub(crate) async fn post( repo.save().await?; + LOGOUT_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]); + Ok(Json(serde_json::json!({}))) } From e310ae22cde6f09d191ac100a6399ee1125f5359 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 16:53:29 +0100 Subject: [PATCH 4/9] Record metrics for OAuth 2.0 token requests --- crates/handlers/src/oauth2/token.rs | 27 +++++++++++++++++++++++++-- crates/oauth2-types/src/requests.rs | 14 ++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d76c81f1b..4ed02ef8d 100644 --- a/crates/handlers/src/oauth2/token.rs +++ b/crates/handlers/src/oauth2/token.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use axum::{Json, extract::State, response::IntoResponse}; use axum_extra::typed_header::TypedHeader; @@ -40,12 +40,23 @@ use oauth2_types::{ }, scope, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use thiserror::Error; use tracing::{debug, info}; use ulid::Ulid; use super::{generate_id_token, generate_token_pair}; -use crate::{BoundActivityTracker, impl_from_error_for_route}; +use crate::{BoundActivityTracker, METER, impl_from_error_for_route}; + +static TOKEN_REQUEST_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.oauth2.token_request") + .with_description("How many OAuth 2.0 token requests have gone through") + .with_unit("{request}") + .build() +}); +const GRANT_TYPE: Key = Key::from_static_str("grant_type"); +const RESULT: Key = Key::from_static_str("successful"); #[derive(Debug, Error)] pub(crate) enum RouteError { @@ -136,6 +147,8 @@ impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let event_id = sentry::capture_error(&self); + TOKEN_REQUEST_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); + let response = match self { Self::Internal(_) | Self::NoSuchBrowserSession @@ -254,6 +267,8 @@ pub(crate) async fn post( let form = client_authorization.form.ok_or(RouteError::BadRequest)?; + let grant_type = form.grant_type(); + let (reply, repo) = match form { AccessTokenRequest::AuthorizationCode(grant) => { authorization_code_grant( @@ -321,6 +336,14 @@ pub(crate) async fn post( repo.save().await?; + TOKEN_REQUEST_COUNTER.add( + 1, + &[ + KeyValue::new(GRANT_TYPE, grant_type), + KeyValue::new(RESULT, "success"), + ], + ); + let mut headers = HeaderMap::new(); headers.typed_insert(CacheControl::new().with_no_store()); headers.typed_insert(Pragma::no_cache()); diff --git a/crates/oauth2-types/src/requests.rs b/crates/oauth2-types/src/requests.rs index 631b33309..36ee36da6 100644 --- a/crates/oauth2-types/src/requests.rs +++ b/crates/oauth2-types/src/requests.rs @@ -638,6 +638,20 @@ pub enum AccessTokenRequest { Unsupported, } +impl AccessTokenRequest { + /// Returns the string representation of the grant type of the request. + #[must_use] + pub fn grant_type(&self) -> &'static str { + match self { + Self::AuthorizationCode(_) => "authorization_code", + Self::RefreshToken(_) => "refresh_token", + Self::ClientCredentials(_) => "client_credentials", + Self::DeviceCode(_) => "urn:ietf:params:oauth:grant-type:device_code", + Self::Unsupported => "unsupported", + } + } +} + /// A successful response from the [Token Endpoint]. /// /// [Token Endpoint]: https://www.rfc-editor.org/rfc/rfc6749#section-3.2 From cd2a5c127939ae902e10a8ac1aae9bb41a321d59 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 22:26:31 +0100 Subject: [PATCH 5/9] Record metrics for OAuth 2.0 client registrations --- crates/handlers/src/oauth2/registration.rs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/crates/handlers/src/oauth2/registration.rs b/crates/handlers/src/oauth2/registration.rs index 71812fac6..0b1f3515d 100644 --- a/crates/handlers/src/oauth2/registration.rs +++ b/crates/handlers/src/oauth2/registration.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use std::sync::LazyLock; + use axum::{Json, extract::State, response::IntoResponse}; use axum_extra::TypedHeader; use hyper::StatusCode; @@ -19,6 +21,7 @@ use oauth2_types::{ VerifiedClientMetadata, }, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use psl::Psl; use rand::distributions::{Alphanumeric, DistString}; use serde::Serialize; @@ -27,7 +30,16 @@ use thiserror::Error; use tracing::info; use url::Url; -use crate::{BoundActivityTracker, impl_from_error_for_route}; +use crate::{BoundActivityTracker, METER, impl_from_error_for_route}; + +static REGISTRATION_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.oauth2.registration_request") + .with_description("Number of OAuth2 registration requests") + .with_unit("{request}") + .build() +}); +const RESULT: Key = Key::from_static_str("result"); #[derive(Debug, Error)] pub(crate) enum RouteError { @@ -56,6 +68,9 @@ impl_from_error_for_route!(serde_json::Error); impl IntoResponse for RouteError { fn into_response(self) -> axum::response::Response { let event_id = sentry::capture_error(&self); + + REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "denied")]); + let response = match self { Self::Internal(_) => ( StatusCode::INTERNAL_SERVER_ERROR, @@ -303,6 +318,7 @@ pub(crate) async fn post( let client = if let Some(client) = existing_client { tracing::info!(%client.id, "Reusing existing client"); + REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "reused")]); client } else { let client = repo @@ -335,6 +351,7 @@ pub(crate) async fn post( ) .await?; tracing::info!(%client.id, "Registered new client"); + REGISTRATION_COUNTER.add(1, &[KeyValue::new(RESULT, "created")]); client }; From 41f897446ec4a75019f59ef3e4875a5f9c88783e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 22:35:32 +0100 Subject: [PATCH 6/9] Record password login attempts --- crates/handlers/src/views/login.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 292b3ff66..36b66c1ea 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use axum::{ extract::{Form, Query, State}, @@ -30,17 +30,27 @@ use mas_templates::{ AccountInactiveContext, FieldError, FormError, FormState, LoginContext, LoginFormField, PostAuthContext, PostAuthContextInner, TemplateContext, Templates, ToFormState, }; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use rand::Rng; use serde::{Deserialize, Serialize}; use zeroize::Zeroizing; use super::shared::OptionalPostAuthAction; use crate::{ - BoundActivityTracker, Limiter, PreferredLanguage, RequesterFingerprint, SiteConfig, + BoundActivityTracker, Limiter, METER, PreferredLanguage, RequesterFingerprint, SiteConfig, passwords::PasswordManager, session::{SessionOrFallback, load_session_or_fallback}, }; +static PASSWORD_LOGIN_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.user.password_login_attempt") + .with_description("Number of password login attempts") + .with_unit("{attempt}") + .build() +}); +const RESULT: Key = Key::from_static_str("result"); + #[derive(Debug, Deserialize, Serialize)] pub(crate) struct LoginForm { username: String, @@ -156,6 +166,7 @@ pub(crate) async fn post( } if !form_state.is_valid() { + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, cookie_jar, @@ -178,6 +189,7 @@ pub(crate) async fn post( // First, lookup the user let Some(user) = repo.user().find_by_username(username).await? else { let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, cookie_jar, @@ -196,6 +208,7 @@ pub(crate) async fn post( if let Err(e) = limiter.check_password(requester, &user) { tracing::warn!(error = &e as &dyn std::error::Error); let form_state = form_state.with_error_on_form(FormError::RateLimitExceeded); + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, cookie_jar, @@ -215,6 +228,7 @@ pub(crate) async fn post( // There is no password for this user, but we don't want to disclose that. Show // a generic 'invalid credentials' error instead let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, cookie_jar, @@ -257,6 +271,7 @@ pub(crate) async fn post( Ok(None) => user_password, Err(_) => { let form_state = form_state.with_error_on_form(FormError::InvalidCredentials); + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); return render( locale, cookie_jar, @@ -275,6 +290,7 @@ pub(crate) async fn post( // Now that we have checked the user password, we now want to show an error if // the user is locked or deactivated if user.deactivated_at.is_some() { + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let ctx = AccountInactiveContext::new(user) .with_csrf(csrf_token.form_value()) @@ -284,6 +300,7 @@ pub(crate) async fn post( } if user.locked_at.is_some() { + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "error")]); let (csrf_token, cookie_jar) = cookie_jar.csrf_token(&clock, &mut rng); let ctx = AccountInactiveContext::new(user) .with_csrf(csrf_token.form_value()) @@ -309,6 +326,8 @@ pub(crate) async fn post( repo.save().await?; + PASSWORD_LOGIN_COUNTER.add(1, &[KeyValue::new(RESULT, "success")]); + activity_tracker .record_browser_session(&clock, &user_session) .await; From f7919e9ce8e3ed52910aa8721e1786ced24ef932 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 22:40:03 +0100 Subject: [PATCH 7/9] Record password registrations --- .../handlers/src/views/register/steps/finish.rs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/views/register/steps/finish.rs b/crates/handlers/src/views/register/steps/finish.rs index 3edb6fbfd..7c73825cc 100644 --- a/crates/handlers/src/views/register/steps/finish.rs +++ b/crates/handlers/src/views/register/steps/finish.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use anyhow::Context as _; use axum::{ @@ -22,10 +22,21 @@ use mas_storage::{ user::UserEmailFilter, }; use mas_templates::{RegisterStepsEmailInUseContext, TemplateContext as _, Templates}; +use opentelemetry::metrics::Counter; use ulid::Ulid; use super::super::cookie::UserRegistrationSessions; -use crate::{BoundActivityTracker, PreferredLanguage, views::shared::OptionalPostAuthAction}; +use crate::{ + BoundActivityTracker, METER, PreferredLanguage, views::shared::OptionalPostAuthAction, +}; + +static PASSWORD_REGISTER_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.user.password_registration") + .with_description("Number of password registrations") + .with_unit("{registration}") + .build() +}); #[tracing::instrument( name = "handlers.views.register.steps.finish.get", @@ -203,6 +214,8 @@ pub(crate) async fn get( repo.browser_session() .authenticate_with_password(&mut rng, &clock, &user_session, &user_password) .await?; + + PASSWORD_REGISTER_COUNTER.add(1, &[]); } if let Some(terms_url) = registration.terms_url { From 86a1261b6de18dd972469b054bad138d0060f46e Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 22:54:43 +0100 Subject: [PATCH 8/9] Record metrics for upstream OAuth 2.0 callbacks --- .../handlers/src/upstream_oauth2/callback.rs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/crates/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index b0421c33d..766631d96 100644 --- a/crates/handlers/src/upstream_oauth2/callback.rs +++ b/crates/handlers/src/upstream_oauth2/callback.rs @@ -4,6 +4,8 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. +use std::sync::LazyLock; + use axum::{ Form, extract::{Path, State}, @@ -26,6 +28,7 @@ use mas_storage::{ }; use mas_templates::{FormPostContext, Templates}; use oauth2_types::{errors::ClientErrorCode, requests::AccessTokenRequest}; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use serde::{Deserialize, Serialize}; use serde_json::json; use thiserror::Error; @@ -37,7 +40,18 @@ use super::{ client_credentials_for_provider, template::{AttributeMappingContext, environment}, }; -use crate::{PreferredLanguage, impl_from_error_for_route, upstream_oauth2::cache::MetadataCache}; +use crate::{ + METER, PreferredLanguage, impl_from_error_for_route, upstream_oauth2::cache::MetadataCache, +}; + +static CALLBACK_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.upstream_oauth2.callback") + .with_description("Number of requests to the upstream OAuth2 callback endpoint") + .build() +}); +const PROVIDER: Key = Key::from_static_str("provider"); +const RESULT: Key = Key::from_static_str("result"); #[derive(Serialize, Deserialize)] pub struct Params { @@ -216,6 +230,14 @@ pub(crate) async fn handler( } if let Some(error) = params.error { + CALLBACK_COUNTER.add( + 1, + &[ + KeyValue::new(PROVIDER, provider_id.to_string()), + KeyValue::new(RESULT, "error"), + ], + ); + return Err(RouteError::ClientError { error, error_description: params.error_description.clone(), @@ -256,6 +278,14 @@ pub(crate) async fn handler( return Err(RouteError::MissingCode); }; + CALLBACK_COUNTER.add( + 1, + &[ + KeyValue::new(PROVIDER, provider_id.to_string()), + KeyValue::new(RESULT, "success"), + ], + ); + let mut lazy_metadata = LazyProviderInfos::new(&metadata_cache, &provider, &client); // Figure out the client credentials From f72ff850ce22f410b688fc33308767eb574aabfa Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Wed, 26 Mar 2025 23:08:38 +0100 Subject: [PATCH 9/9] Record metrics for upstream OAuth 2.0 logins and registrations --- crates/handlers/src/upstream_oauth2/link.rs | 31 +++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index dddf47fa8..cacba650a 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -4,7 +4,7 @@ // SPDX-License-Identifier: AGPL-3.0-only // Please see LICENSE in the repository root for full details. -use std::sync::Arc; +use std::sync::{Arc, LazyLock}; use axum::{ Form, @@ -35,6 +35,7 @@ use mas_templates::{ ToFormState, UpstreamExistingLinkContext, UpstreamRegister, UpstreamSuggestLink, }; use minijinja::Environment; +use opentelemetry::{Key, KeyValue, metrics::Counter}; use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; @@ -45,10 +46,26 @@ use super::{ template::{AttributeMappingContext, environment}, }; use crate::{ - BoundActivityTracker, PreferredLanguage, SiteConfig, impl_from_error_for_route, + BoundActivityTracker, METER, PreferredLanguage, SiteConfig, impl_from_error_for_route, views::shared::OptionalPostAuthAction, }; +static LOGIN_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.upstream_oauth2.login") + .with_description("Successful upstream OAuth 2.0 login to existing accounts") + .with_unit("{login}") + .build() +}); +static REGISTRATION_COUNTER: LazyLock> = LazyLock::new(|| { + METER + .u64_counter("mas.upstream_oauth2.registration") + .with_description("Successful upstream OAuth 2.0 registration") + .with_unit("{registration}") + .build() +}); +const PROVIDER: Key = Key::from_static_str("provider"); + const DEFAULT_LOCALPART_TEMPLATE: &str = "{{ user.preferred_username }}"; const DEFAULT_DISPLAYNAME_TEMPLATE: &str = "{{ user.name }}"; const DEFAULT_EMAIL_TEMPLATE: &str = "{{ user.email }}"; @@ -340,6 +357,14 @@ pub(crate) async fn get( repo.save().await?; + LOGIN_COUNTER.add( + 1, + &[KeyValue::new( + PROVIDER, + upstream_session.provider_id.to_string(), + )], + ); + post_auth_action.go_next(&url_builder).into_response() } @@ -805,6 +830,8 @@ pub(crate) async fn post( .into_response()); } + REGISTRATION_COUNTER.add(1, &[KeyValue::new(PROVIDER, provider.id.to_string())]); + // Now we can create the user let user = repo.user().add(&mut rng, &clock, username).await?;