diff --git a/index-scheduler/src/features.rs b/index-scheduler/src/features.rs index 5a663fe67..442a43320 100644 --- a/index-scheduler/src/features.rs +++ b/index-scheduler/src/features.rs @@ -10,19 +10,17 @@ const EXPERIMENTAL_FEATURES: &str = "experimental-features"; #[derive(Clone)] pub(crate) struct FeatureData { runtime: Database>, - instance: InstanceTogglableFeatures, } #[derive(Debug, Clone, Copy)] pub struct RoFeatures { runtime: RuntimeTogglableFeatures, - instance: InstanceTogglableFeatures, } impl RoFeatures { fn new(txn: RoTxn<'_>, data: &FeatureData) -> Result { let runtime = data.runtime_features(txn)?; - Ok(Self { runtime, instance: data.instance }) + Ok(Self { runtime }) } pub fn runtime_features(&self) -> RuntimeTogglableFeatures { @@ -43,7 +41,7 @@ impl RoFeatures { } pub fn check_metrics(&self) -> Result<()> { - if self.instance.metrics { + if self.runtime.metrics { Ok(()) } else { Err(FeatureNotEnabledError { @@ -73,9 +71,12 @@ impl FeatureData { pub fn new(env: &Env, instance_features: InstanceTogglableFeatures) -> Result { let mut wtxn = env.write_txn()?; let runtime_features = env.create_database(&mut wtxn, Some(EXPERIMENTAL_FEATURES))?; + let default_features = + RuntimeTogglableFeatures { metrics: instance_features.metrics, ..Default::default() }; + runtime_features.put(&mut wtxn, EXPERIMENTAL_FEATURES, &default_features)?; wtxn.commit()?; - Ok(Self { runtime: runtime_features, instance: instance_features }) + Ok(Self { runtime: runtime_features }) } pub fn put_runtime_features( diff --git a/meilisearch-types/src/features.rs b/meilisearch-types/src/features.rs index f62300485..4fe4affd4 100644 --- a/meilisearch-types/src/features.rs +++ b/meilisearch-types/src/features.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; pub struct RuntimeTogglableFeatures { pub score_details: bool, pub vector_store: bool, + pub metrics: bool, } #[derive(Default, Debug, Clone, Copy)] diff --git a/meilisearch/src/routes/features.rs b/meilisearch/src/routes/features.rs index a2822b4d4..8e35acc56 100644 --- a/meilisearch/src/routes/features.rs +++ b/meilisearch/src/routes/features.rs @@ -44,6 +44,8 @@ pub struct RuntimeTogglableFeatures { pub score_details: Option, #[deserr(default)] pub vector_store: Option, + #[deserr(default)] + pub metrics: Option, } async fn patch_features( @@ -62,19 +64,24 @@ async fn patch_features( let new_features = meilisearch_types::features::RuntimeTogglableFeatures { score_details: new_features.0.score_details.unwrap_or(old_features.score_details), vector_store: new_features.0.vector_store.unwrap_or(old_features.vector_store), + metrics: new_features.0.metrics.unwrap_or(old_features.metrics), }; // explicitly destructure for analytics rather than using the `Serialize` implementation, because // the it renames to camelCase, which we don't want for analytics. // **Do not** ignore fields with `..` or `_` here, because we want to add them in the future. - let meilisearch_types::features::RuntimeTogglableFeatures { score_details, vector_store } = - new_features; + let meilisearch_types::features::RuntimeTogglableFeatures { + score_details, + vector_store, + metrics, + } = new_features; analytics.publish( "Experimental features Updated".to_string(), json!({ "score_details": score_details, "vector_store": vector_store, + "metrics": metrics, }), Some(&req), ); diff --git a/meilisearch/tests/auth/authorization.rs b/meilisearch/tests/auth/authorization.rs index 883c23267..af028060d 100644 --- a/meilisearch/tests/auth/authorization.rs +++ b/meilisearch/tests/auth/authorization.rs @@ -2,10 +2,12 @@ use std::collections::{HashMap, HashSet}; use ::time::format_description::well_known::Rfc3339; use maplit::{hashmap, hashset}; +use meilisearch::Opt; use once_cell::sync::Lazy; +use tempfile::TempDir; use time::{Duration, OffsetDateTime}; -use crate::common::{Server, Value}; +use crate::common::{default_settings, Server, Value}; use crate::json; pub static AUTHORIZATIONS: Lazy>> = @@ -195,7 +197,9 @@ async fn access_authorized_master_key() { #[actix_rt::test] async fn access_authorized_restricted_index() { - let mut server = Server::new_auth().await; + let dir = TempDir::new().unwrap(); + let enable_metrics = Opt { experimental_enable_metrics: true, ..default_settings(dir.path()) }; + let mut server = Server::new_auth_with_options(enable_metrics, dir).await; for ((method, route), actions) in AUTHORIZATIONS.iter() { for action in actions { // create a new API key letting only the needed action. diff --git a/meilisearch/tests/common/server.rs b/meilisearch/tests/common/server.rs index 58f561eb8..27feb187f 100644 --- a/meilisearch/tests/common/server.rs +++ b/meilisearch/tests/common/server.rs @@ -202,6 +202,10 @@ impl Server { pub async fn set_features(&self, value: Value) -> (Value, StatusCode) { self.service.patch("/experimental-features", value).await } + + pub async fn get_metrics(&self) -> (Value, StatusCode) { + self.service.get("/metrics").await + } } pub fn default_settings(dir: impl AsRef) -> Opt { @@ -221,7 +225,7 @@ pub fn default_settings(dir: impl AsRef) -> Opt { skip_index_budget: true, ..Parser::parse_from(None as Option<&str>) }, - experimental_enable_metrics: true, + experimental_enable_metrics: false, ..Parser::parse_from(None as Option<&str>) } } diff --git a/meilisearch/tests/features/mod.rs b/meilisearch/tests/features/mod.rs index 348deb5e2..9de829d50 100644 --- a/meilisearch/tests/features/mod.rs +++ b/meilisearch/tests/features/mod.rs @@ -1,4 +1,7 @@ -use crate::common::Server; +use meilisearch::Opt; +use tempfile::TempDir; + +use crate::common::{default_settings, Server}; use crate::json; /// Feature name to test against. @@ -16,7 +19,8 @@ async fn experimental_features() { meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "scoreDetails": false, - "vectorStore": false + "vectorStore": false, + "metrics": false } "###); @@ -26,7 +30,8 @@ async fn experimental_features() { meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "scoreDetails": false, - "vectorStore": true + "vectorStore": true, + "metrics": false } "###); @@ -36,7 +41,8 @@ async fn experimental_features() { meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "scoreDetails": false, - "vectorStore": true + "vectorStore": true, + "metrics": false } "###); @@ -47,7 +53,8 @@ async fn experimental_features() { meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "scoreDetails": false, - "vectorStore": true + "vectorStore": true, + "metrics": false } "###); @@ -58,11 +65,63 @@ async fn experimental_features() { meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { "scoreDetails": false, - "vectorStore": true + "vectorStore": true, + "metrics": false } "###); } +#[actix_rt::test] +async fn experimental_feature_metrics() { + // instance flag for metrics enables metrics at startup + let dir = TempDir::new().unwrap(); + let enable_metrics = Opt { experimental_enable_metrics: true, ..default_settings(dir.path()) }; + let server = Server::new_with_options(enable_metrics).await.unwrap(); + + let (response, code) = server.get_features().await; + + meili_snap::snapshot!(code, @"200 OK"); + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" + { + "scoreDetails": false, + "vectorStore": false, + "metrics": true + } + "###); + + let (response, code) = server.get_metrics().await; + meili_snap::snapshot!(code, @"200 OK"); + + // metrics are not returned in json format + // so the test server will return null + meili_snap::snapshot!(response, @"null"); + + // disabling metrics results in invalid request + let (response, code) = server.set_features(json!({"metrics": false})).await; + meili_snap::snapshot!(code, @"200 OK"); + meili_snap::snapshot!(response["metrics"], @"false"); + + let (response, code) = server.get_metrics().await; + meili_snap::snapshot!(code, @"400 Bad Request"); + meili_snap::snapshot!(meili_snap::json_string!(response), @r###" + { + "message": "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/meilisearch/discussions/3518", + "code": "feature_not_enabled", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#feature_not_enabled" + } + "###); + + // enabling metrics via HTTP results in valid request + let (response, code) = server.set_features(json!({"metrics": true})).await; + meili_snap::snapshot!(code, @"200 OK"); + meili_snap::snapshot!(response["metrics"], @"true"); + + let (response, code) = server.get_metrics().await; + meili_snap::snapshot!(code, @"200 OK"); + meili_snap::snapshot!(response, @"null"); +} + #[actix_rt::test] async fn errors() { let server = Server::new().await; @@ -73,7 +132,7 @@ async fn errors() { meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(meili_snap::json_string!(response), @r###" { - "message": "Unknown field `NotAFeature`: expected one of `scoreDetails`, `vectorStore`", + "message": "Unknown field `NotAFeature`: expected one of `scoreDetails`, `vectorStore`, `metrics`", "code": "bad_request", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#bad_request"