diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 92eacbd5f..85c5a89f1 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -109,17 +109,18 @@ async fn try_main() -> anyhow::Result { // Load the base configuration files let figment = opts.figment(); - // Telemetry config could fail to load, but that's probably OK, since the whole - // config will be loaded afterwards, and crash if there is a problem. - // Falling back to default. - let telemetry_config = TelemetryConfig::extract(&figment).unwrap_or_default(); + let telemetry_config = + TelemetryConfig::extract(&figment).context("Failed to load telemetry config")?; // Setup Sentry let sentry = sentry::init(( telemetry_config.sentry.dsn.as_deref(), sentry::ClientOptions { transport: Some(Arc::new(SentryTransportFactory::new())), - traces_sample_rate: 1.0, + environment: telemetry_config.sentry.environment.clone().map(Into::into), + release: Some(VERSION.into()), + sample_rate: telemetry_config.sentry.sample_rate.unwrap_or(1.0), + traces_sample_rate: telemetry_config.sentry.traces_sample_rate.unwrap_or(0.0), auto_session_tracking: true, session_mode: sentry::SessionMode::Request, ..Default::default() diff --git a/crates/cli/src/telemetry.rs b/crates/cli/src/telemetry.rs index 4eca94bbe..7d4e4e30a 100644 --- a/crates/cli/src/telemetry.rs +++ b/crates/cli/src/telemetry.rs @@ -102,7 +102,10 @@ fn stdout_tracer_provider() -> SdkTracerProvider { .build() } -fn otlp_tracer_provider(endpoint: Option<&Url>) -> anyhow::Result { +fn otlp_tracer_provider( + endpoint: Option<&Url>, + sample_rate: f64, +) -> anyhow::Result { let mut exporter = opentelemetry_otlp::SpanExporter::builder() .with_http() .with_http_client(mas_http::reqwest_client()); @@ -119,17 +122,18 @@ fn otlp_tracer_provider(endpoint: Option<&Url>) -> anyhow::Result anyhow::Result<()> { + let sample_rate = config.sample_rate.unwrap_or(1.0); let tracer_provider = match config.exporter { TracingExporterKind::None => return Ok(()), TracingExporterKind::Stdout => stdout_tracer_provider(), - TracingExporterKind::Otlp => otlp_tracer_provider(config.endpoint.as_ref())?, + TracingExporterKind::Otlp => otlp_tracer_provider(config.endpoint.as_ref(), sample_rate)?, }; TRACER_PROVIDER .set(tracer_provider.clone()) diff --git a/crates/config/src/sections/telemetry.rs b/crates/config/src/sections/telemetry.rs index f4d73c29d..0c11e0285 100644 --- a/crates/config/src/sections/telemetry.rs +++ b/crates/config/src/sections/telemetry.rs @@ -5,12 +5,16 @@ // Please see LICENSE in the repository root for full details. use schemars::JsonSchema; -use serde::{Deserialize, Serialize}; +use serde::{Deserialize, Serialize, de::Error as _}; use serde_with::skip_serializing_none; use url::Url; use super::ConfigurationSection; +fn sample_rate_example() -> f64 { + 0.5 +} + /// Propagation format for incoming and outgoing requests #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] #[serde(rename_all = "lowercase")] @@ -61,6 +65,13 @@ pub struct TracingConfig { /// List of propagation formats to use for incoming and outgoing requests #[serde(default)] pub propagators: Vec, + + /// Sample rate for traces + /// + /// Defaults to `1.0` if not set. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + pub sample_rate: Option, } impl TracingConfig { @@ -116,6 +127,10 @@ fn sentry_dsn_example() -> &'static str { "https://public@host:port/1" } +fn sentry_environment_example() -> &'static str { + "production" +} + /// Configuration related to the Sentry integration #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct SentryConfig { @@ -123,6 +138,27 @@ pub struct SentryConfig { #[schemars(url, example = "sentry_dsn_example")] #[serde(skip_serializing_if = "Option::is_none")] pub dsn: Option, + + /// Environment to use when sending events to Sentry + /// + /// Defaults to `production` if not set. + #[schemars(example = "sentry_environment_example")] + #[serde(skip_serializing_if = "Option::is_none")] + pub environment: Option, + + /// Sample rate for event submissions + /// + /// Defaults to `1.0` if not set. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + pub sample_rate: Option, + + /// Sample rate for tracing transactions + /// + /// Defaults to `0.0` if not set. + #[serde(skip_serializing_if = "Option::is_none")] + #[schemars(example = "sample_rate_example", range(min = 0.0, max = 1.0))] + pub traces_sample_rate: Option, } impl SentryConfig { @@ -157,4 +193,35 @@ impl TelemetryConfig { impl ConfigurationSection for TelemetryConfig { const PATH: Option<&'static str> = Some("telemetry"); + + fn validate(&self, _figment: &figment::Figment) -> Result<(), figment::Error> { + if let Some(sample_rate) = self.sentry.sample_rate { + if !(0.0..=1.0).contains(&sample_rate) { + return Err(figment::error::Error::custom( + "Sentry sample rate must be between 0.0 and 1.0", + ) + .with_path("sentry.sample_rate")); + } + } + + if let Some(sample_rate) = self.sentry.traces_sample_rate { + if !(0.0..=1.0).contains(&sample_rate) { + return Err(figment::error::Error::custom( + "Sentry sample rate must be between 0.0 and 1.0", + ) + .with_path("sentry.traces_sample_rate")); + } + } + + if let Some(sample_rate) = self.tracing.sample_rate { + if !(0.0..=1.0).contains(&sample_rate) { + return Err(figment::error::Error::custom( + "Tracing sample rate must be between 0.0 and 1.0", + ) + .with_path("tracing.sample_rate")); + } + } + + Ok(()) + } } 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, 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!({}))) } 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), 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 }; diff --git a/crates/handlers/src/oauth2/token.rs b/crates/handlers/src/oauth2/token.rs index d5f65be6c..c1a842be7 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/handlers/src/upstream_oauth2/callback.rs b/crates/handlers/src/upstream_oauth2/callback.rs index 9556fd3af..be4b5a2d1 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 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?; diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 67256c92e..6d195df05 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; 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 { 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 diff --git a/crates/syn2mas/src/migration.rs b/crates/syn2mas/src/migration.rs index efefc25d7..e00292799 100644 --- a/crates/syn2mas/src/migration.rs +++ b/crates/syn2mas/src/migration.rs @@ -456,11 +456,23 @@ async fn migrate_threepids( } = threepid_res.into_synapse("reading threepid")?; let created_at: DateTime = added_at.into(); + // HACK(matrix.org): for some reason, m.org has threepids for the :vector.im + // server. We skip just skip them. + if synapse_user_id.0.ends_with(":vector.im") { + continue; + } + let username = synapse_user_id .extract_localpart(&state.server_name) .into_extract_localpart(synapse_user_id.clone())? .to_owned(); let Some(user_infos) = state.users.get(username.as_str()).copied() else { + // HACK(matrix.org): we seem to have casing inconsistencies + if state.users.contains_key(username.to_lowercase().as_str()) { + tracing::warn!(mxid = %synapse_user_id, "Threepid found in the database matching an MXID with the wrong casing"); + continue; + } + return Err(Error::MissingUserFromDependentTable { table: "user_threepids".to_owned(), user: synapse_user_id, @@ -556,6 +568,12 @@ async fn migrate_external_ids( .into_extract_localpart(synapse_user_id.clone())? .to_owned(); let Some(user_infos) = state.users.get(username.as_str()).copied() else { + // HACK(matrix.org): we seem to have casing inconsistencies + if state.users.contains_key(username.to_lowercase().as_str()) { + tracing::warn!(mxid = %synapse_user_id, "External ID found in the database matching an MXID with the wrong casing"); + continue; + } + return Err(Error::MissingUserFromDependentTable { table: "user_external_ids".to_owned(), user: synapse_user_id, @@ -656,6 +674,12 @@ async fn migrate_devices( .into_extract_localpart(synapse_user_id.clone())? .to_owned(); let Some(user_infos) = state.users.get(username.as_str()).copied() else { + // HACK(matrix.org): we seem to have casing inconsistencies + if state.users.contains_key(username.to_lowercase().as_str()) { + tracing::warn!(mxid = %synapse_user_id, "Device found in the database matching an MXID with the wrong casing"); + continue; + } + return Err(Error::MissingUserFromDependentTable { table: "devices".to_owned(), user: synapse_user_id, @@ -801,6 +825,12 @@ async fn migrate_unrefreshable_access_tokens( .into_extract_localpart(synapse_user_id.clone())? .to_owned(); let Some(user_infos) = state.users.get(username.as_str()).copied() else { + // HACK(matrix.org): we seem to have casing inconsistencies + if state.users.contains_key(username.to_lowercase().as_str()) { + tracing::warn!(mxid = %synapse_user_id, "Access token found in the database matching an MXID with the wrong casing"); + continue; + } + return Err(Error::MissingUserFromDependentTable { table: "access_tokens".to_owned(), user: synapse_user_id, @@ -955,6 +985,12 @@ async fn migrate_refreshable_token_pairs( .into_extract_localpart(synapse_user_id.clone())? .to_owned(); let Some(user_infos) = state.users.get(username.as_str()).copied() else { + // HACK(matrix.org): we seem to have casing inconsistencies + if state.users.contains_key(username.to_lowercase().as_str()) { + tracing::warn!(mxid = %synapse_user_id, "Refresh token found in the database matching an MXID with the wrong casing"); + continue; + } + return Err(Error::MissingUserFromDependentTable { table: "refresh_tokens".to_owned(), user: synapse_user_id, diff --git a/docs/config.schema.json b/docs/config.schema.json index 0d8325529..2a4a2c719 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1213,6 +1213,16 @@ "items": { "$ref": "#/definitions/Propagator" } + }, + "sample_rate": { + "description": "Sample rate for traces\n\nDefaults to `1.0` if not set.", + "examples": [ + 0.5 + ], + "type": "number", + "format": "double", + "maximum": 1.0, + "minimum": 0.0 } } }, @@ -1333,6 +1343,33 @@ ], "type": "string", "format": "uri" + }, + "environment": { + "description": "Environment to use when sending events to Sentry\n\nDefaults to `production` if not set.", + "examples": [ + "production" + ], + "type": "string" + }, + "sample_rate": { + "description": "Sample rate for event submissions\n\nDefaults to `1.0` if not set.", + "examples": [ + 0.5 + ], + "type": "number", + "format": "float", + "maximum": 1.0, + "minimum": 0.0 + }, + "traces_sample_rate": { + "description": "Sample rate for tracing transactions\n\nDefaults to `0.0` if not set.", + "examples": [ + 0.5 + ], + "type": "number", + "format": "float", + "maximum": 1.0, + "minimum": 0.0 } } },