Make the experimental route /metrics activable via HTTP

This commit is contained in:
bwbonanno 2023-10-13 22:12:54 +00:00
parent 0913373a5e
commit 689ec7c7ad
6 changed files with 93 additions and 17 deletions

View File

@ -10,19 +10,17 @@ const EXPERIMENTAL_FEATURES: &str = "experimental-features";
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct FeatureData { pub(crate) struct FeatureData {
runtime: Database<Str, SerdeJson<RuntimeTogglableFeatures>>, runtime: Database<Str, SerdeJson<RuntimeTogglableFeatures>>,
instance: InstanceTogglableFeatures,
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct RoFeatures { pub struct RoFeatures {
runtime: RuntimeTogglableFeatures, runtime: RuntimeTogglableFeatures,
instance: InstanceTogglableFeatures,
} }
impl RoFeatures { impl RoFeatures {
fn new(txn: RoTxn<'_>, data: &FeatureData) -> Result<Self> { fn new(txn: RoTxn<'_>, data: &FeatureData) -> Result<Self> {
let runtime = data.runtime_features(txn)?; let runtime = data.runtime_features(txn)?;
Ok(Self { runtime, instance: data.instance }) Ok(Self { runtime })
} }
pub fn runtime_features(&self) -> RuntimeTogglableFeatures { pub fn runtime_features(&self) -> RuntimeTogglableFeatures {
@ -43,7 +41,7 @@ impl RoFeatures {
} }
pub fn check_metrics(&self) -> Result<()> { pub fn check_metrics(&self) -> Result<()> {
if self.instance.metrics { if self.runtime.metrics {
Ok(()) Ok(())
} else { } else {
Err(FeatureNotEnabledError { Err(FeatureNotEnabledError {
@ -73,9 +71,12 @@ impl FeatureData {
pub fn new(env: &Env, instance_features: InstanceTogglableFeatures) -> Result<Self> { pub fn new(env: &Env, instance_features: InstanceTogglableFeatures) -> Result<Self> {
let mut wtxn = env.write_txn()?; let mut wtxn = env.write_txn()?;
let runtime_features = env.create_database(&mut wtxn, Some(EXPERIMENTAL_FEATURES))?; 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()?; wtxn.commit()?;
Ok(Self { runtime: runtime_features, instance: instance_features }) Ok(Self { runtime: runtime_features })
} }
pub fn put_runtime_features( pub fn put_runtime_features(

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
pub struct RuntimeTogglableFeatures { pub struct RuntimeTogglableFeatures {
pub score_details: bool, pub score_details: bool,
pub vector_store: bool, pub vector_store: bool,
pub metrics: bool,
} }
#[derive(Default, Debug, Clone, Copy)] #[derive(Default, Debug, Clone, Copy)]

View File

@ -44,6 +44,8 @@ pub struct RuntimeTogglableFeatures {
pub score_details: Option<bool>, pub score_details: Option<bool>,
#[deserr(default)] #[deserr(default)]
pub vector_store: Option<bool>, pub vector_store: Option<bool>,
#[deserr(default)]
pub metrics: Option<bool>,
} }
async fn patch_features( async fn patch_features(
@ -62,19 +64,24 @@ async fn patch_features(
let new_features = meilisearch_types::features::RuntimeTogglableFeatures { let new_features = meilisearch_types::features::RuntimeTogglableFeatures {
score_details: new_features.0.score_details.unwrap_or(old_features.score_details), 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), 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 // explicitly destructure for analytics rather than using the `Serialize` implementation, because
// the it renames to camelCase, which we don't want for analytics. // 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. // **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 } = let meilisearch_types::features::RuntimeTogglableFeatures {
new_features; score_details,
vector_store,
metrics,
} = new_features;
analytics.publish( analytics.publish(
"Experimental features Updated".to_string(), "Experimental features Updated".to_string(),
json!({ json!({
"score_details": score_details, "score_details": score_details,
"vector_store": vector_store, "vector_store": vector_store,
"metrics": metrics,
}), }),
Some(&req), Some(&req),
); );

View File

@ -2,10 +2,12 @@ use std::collections::{HashMap, HashSet};
use ::time::format_description::well_known::Rfc3339; use ::time::format_description::well_known::Rfc3339;
use maplit::{hashmap, hashset}; use maplit::{hashmap, hashset};
use meilisearch::Opt;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use tempfile::TempDir;
use time::{Duration, OffsetDateTime}; use time::{Duration, OffsetDateTime};
use crate::common::{Server, Value}; use crate::common::{default_settings, Server, Value};
use crate::json; use crate::json;
pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'static str>>> = pub static AUTHORIZATIONS: Lazy<HashMap<(&'static str, &'static str), HashSet<&'static str>>> =
@ -195,7 +197,9 @@ async fn access_authorized_master_key() {
#[actix_rt::test] #[actix_rt::test]
async fn access_authorized_restricted_index() { 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 ((method, route), actions) in AUTHORIZATIONS.iter() {
for action in actions { for action in actions {
// create a new API key letting only the needed action. // create a new API key letting only the needed action.

View File

@ -202,6 +202,10 @@ impl Server {
pub async fn set_features(&self, value: Value) -> (Value, StatusCode) { pub async fn set_features(&self, value: Value) -> (Value, StatusCode) {
self.service.patch("/experimental-features", value).await 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<Path>) -> Opt { pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
@ -221,7 +225,7 @@ pub fn default_settings(dir: impl AsRef<Path>) -> Opt {
skip_index_budget: true, skip_index_budget: true,
..Parser::parse_from(None as Option<&str>) ..Parser::parse_from(None as Option<&str>)
}, },
experimental_enable_metrics: true, experimental_enable_metrics: false,
..Parser::parse_from(None as Option<&str>) ..Parser::parse_from(None as Option<&str>)
} }
} }

View File

@ -1,4 +1,7 @@
use crate::common::Server; use meilisearch::Opt;
use tempfile::TempDir;
use crate::common::{default_settings, Server};
use crate::json; use crate::json;
/// Feature name to test against. /// Feature name to test against.
@ -16,7 +19,8 @@ async fn experimental_features() {
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"scoreDetails": false, "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###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"scoreDetails": false, "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###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"scoreDetails": false, "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###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"scoreDetails": false, "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###" meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
{ {
"scoreDetails": false, "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] #[actix_rt::test]
async fn errors() { async fn errors() {
let server = Server::new().await; let server = Server::new().await;
@ -73,7 +132,7 @@ async fn errors() {
meili_snap::snapshot!(code, @"400 Bad Request"); meili_snap::snapshot!(code, @"400 Bad Request");
meili_snap::snapshot!(meili_snap::json_string!(response), @r###" 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", "code": "bad_request",
"type": "invalid_request", "type": "invalid_request",
"link": "https://docs.meilisearch.com/errors#bad_request" "link": "https://docs.meilisearch.com/errors#bad_request"