mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-11-29 16:24:26 +01:00
Make the experimental route /metrics activable via HTTP
This commit is contained in:
parent
0913373a5e
commit
689ec7c7ad
@ -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(
|
||||||
|
@ -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)]
|
||||||
|
@ -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),
|
||||||
);
|
);
|
||||||
|
@ -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.
|
||||||
|
@ -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>)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user