mirror of
https://github.com/meilisearch/MeiliSearch
synced 2024-12-24 21:50:07 +01:00
handle and tests errors
This commit is contained in:
parent
bf5cea8b10
commit
80774148fd
@ -61,7 +61,6 @@ impl ErrorCode for MeilisearchHttpError {
|
|||||||
fn error_code(&self) -> Code {
|
fn error_code(&self) -> Code {
|
||||||
match self {
|
match self {
|
||||||
MeilisearchHttpError::MissingContentType(_) => Code::MissingContentType,
|
MeilisearchHttpError::MissingContentType(_) => Code::MissingContentType,
|
||||||
/// TODO: TAMO: create a new error code
|
|
||||||
MeilisearchHttpError::AlreadyUsedLogRoute => Code::BadRequest,
|
MeilisearchHttpError::AlreadyUsedLogRoute => Code::BadRequest,
|
||||||
MeilisearchHttpError::CsvDelimiterWithWrongContentType(_) => Code::InvalidContentType,
|
MeilisearchHttpError::CsvDelimiterWithWrongContentType(_) => Code::InvalidContentType,
|
||||||
MeilisearchHttpError::MissingPayload(_) => Code::MissingPayload,
|
MeilisearchHttpError::MissingPayload(_) => Code::MissingPayload,
|
||||||
|
@ -1,19 +1,21 @@
|
|||||||
use std::fmt;
|
use std::convert::Infallible;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
use std::ops::ControlFlow;
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use actix_web::web::{Bytes, Data};
|
use actix_web::web::{Bytes, Data};
|
||||||
use actix_web::{web, HttpRequest, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
use deserr::actix_web::AwebJson;
|
use deserr::actix_web::AwebJson;
|
||||||
use deserr::Deserr;
|
use deserr::{DeserializeError, Deserr, ErrorKind, MergeWithError, ValuePointerRef};
|
||||||
use futures_util::Stream;
|
use futures_util::Stream;
|
||||||
use meilisearch_auth::AuthController;
|
use meilisearch_auth::AuthController;
|
||||||
use meilisearch_types::deserr::DeserrJsonError;
|
use meilisearch_types::deserr::DeserrJsonError;
|
||||||
use meilisearch_types::error::deserr_codes::*;
|
use meilisearch_types::error::deserr_codes::*;
|
||||||
use meilisearch_types::error::{Code, ResponseError};
|
use meilisearch_types::error::{Code, ResponseError};
|
||||||
use tokio::sync::mpsc::{self};
|
use tokio::sync::mpsc::{self};
|
||||||
|
use tracing_subscriber::filter::Targets;
|
||||||
use tracing_subscriber::Layer;
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
use crate::error::MeilisearchHttpError;
|
use crate::error::MeilisearchHttpError;
|
||||||
@ -30,17 +32,6 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Deserr)]
|
|
||||||
#[deserr(rename_all = lowercase)]
|
|
||||||
pub enum LogLevel {
|
|
||||||
Error,
|
|
||||||
Warn,
|
|
||||||
#[default]
|
|
||||||
Info,
|
|
||||||
Debug,
|
|
||||||
Trace,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone, Copy, Deserr)]
|
#[derive(Debug, Default, Clone, Copy, Deserr)]
|
||||||
#[deserr(rename_all = lowercase)]
|
#[deserr(rename_all = lowercase)]
|
||||||
pub enum LogMode {
|
pub enum LogMode {
|
||||||
@ -49,38 +40,61 @@ pub enum LogMode {
|
|||||||
Profile,
|
Profile,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simple wrapper around the `Targets` from `tracing_subscriber` to implement `MergeWithError` on it.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
struct MyTargets(Targets);
|
||||||
|
|
||||||
|
/// Simple wrapper around the `ParseError` from `tracing_subscriber` to implement `MergeWithError` on it.
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
enum MyParseError {
|
||||||
|
#[error(transparent)]
|
||||||
|
ParseError(#[from] tracing_subscriber::filter::ParseError),
|
||||||
|
#[error(
|
||||||
|
"Empty string is not a valid target. If you want to get no logs use `OFF`. Usage: `info`, `info:meilisearch`, or you can write multiple filters in one target: `index_scheduler=info,milli=trace`"
|
||||||
|
)]
|
||||||
|
Example,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for MyTargets {
|
||||||
|
type Err = MyParseError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
if s.is_empty() {
|
||||||
|
Err(MyParseError::Example)
|
||||||
|
} else {
|
||||||
|
Ok(MyTargets(Targets::from_str(s).map_err(MyParseError::ParseError)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MergeWithError<MyParseError> for DeserrJsonError<BadRequest> {
|
||||||
|
fn merge(
|
||||||
|
_self_: Option<Self>,
|
||||||
|
other: MyParseError,
|
||||||
|
merge_location: ValuePointerRef,
|
||||||
|
) -> ControlFlow<Self, Self> {
|
||||||
|
Self::error::<Infallible>(
|
||||||
|
None,
|
||||||
|
ErrorKind::Unexpected { msg: other.to_string() },
|
||||||
|
merge_location,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserr)]
|
#[derive(Debug, Deserr)]
|
||||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct GetLogs {
|
pub struct GetLogs {
|
||||||
#[deserr(default, error = DeserrJsonError<BadRequest>)]
|
#[deserr(default = "info".parse().unwrap(), try_from(&String) = MyTargets::from_str -> DeserrJsonError<BadRequest>)]
|
||||||
pub target: String,
|
target: MyTargets,
|
||||||
|
|
||||||
#[deserr(default, error = DeserrJsonError<BadRequest>)]
|
#[deserr(default, error = DeserrJsonError<BadRequest>)]
|
||||||
pub mode: LogMode,
|
mode: LogMode,
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for LogLevel {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
LogLevel::Error => f.write_str("error"),
|
|
||||||
LogLevel::Warn => f.write_str("warn"),
|
|
||||||
LogLevel::Info => f.write_str("info"),
|
|
||||||
LogLevel::Debug => f.write_str("debug"),
|
|
||||||
LogLevel::Trace => f.write_str("trace"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct LogWriter {
|
struct LogWriter {
|
||||||
sender: mpsc::UnboundedSender<Vec<u8>>,
|
sender: mpsc::UnboundedSender<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for LogWriter {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
println!("hello");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Write for LogWriter {
|
impl Write for LogWriter {
|
||||||
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
|
||||||
self.sender.send(buf.to_vec()).map_err(std::io::Error::other)?;
|
self.sender.send(buf.to_vec()).map_err(std::io::Error::other)?;
|
||||||
@ -99,7 +113,6 @@ struct HandleGuard {
|
|||||||
|
|
||||||
impl Drop for HandleGuard {
|
impl Drop for HandleGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
println!("log streamer being dropped");
|
|
||||||
if let Err(e) = self.logs.modify(|layer| *layer.inner_mut() = None) {
|
if let Err(e) = self.logs.modify(|layer| *layer.inner_mut() = None) {
|
||||||
tracing::error!("Could not free the logs route: {e}");
|
tracing::error!("Could not free the logs route: {e}");
|
||||||
}
|
}
|
||||||
@ -203,7 +216,6 @@ pub async fn get_logs(
|
|||||||
_auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_ALL }>, Data<AuthController>>,
|
_auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_ALL }>, Data<AuthController>>,
|
||||||
logs: Data<LogRouteHandle>,
|
logs: Data<LogRouteHandle>,
|
||||||
body: AwebJson<GetLogs, DeserrJsonError>,
|
body: AwebJson<GetLogs, DeserrJsonError>,
|
||||||
_req: HttpRequest,
|
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
let opt = body.into_inner();
|
let opt = body.into_inner();
|
||||||
|
|
||||||
@ -212,8 +224,7 @@ pub async fn get_logs(
|
|||||||
logs.modify(|layer| match layer.inner_mut() {
|
logs.modify(|layer| match layer.inner_mut() {
|
||||||
None => {
|
None => {
|
||||||
// there is no one getting logs
|
// there is no one getting logs
|
||||||
*layer.filter_mut() =
|
*layer.filter_mut() = opt.target.0.clone();
|
||||||
tracing_subscriber::filter::Targets::from_str(&opt.target).unwrap();
|
|
||||||
let (new_layer, new_stream) = make_layer(&opt, logs.clone());
|
let (new_layer, new_stream) = make_layer(&opt, logs.clone());
|
||||||
|
|
||||||
*layer.inner_mut() = Some(new_layer);
|
*layer.inner_mut() = Some(new_layer);
|
||||||
@ -235,7 +246,6 @@ pub async fn get_logs(
|
|||||||
pub async fn cancel_logs(
|
pub async fn cancel_logs(
|
||||||
_auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_ALL }>, Data<AuthController>>,
|
_auth_controller: GuardedData<ActionPolicy<{ actions::METRICS_ALL }>, Data<AuthController>>,
|
||||||
logs: Data<LogRouteHandle>,
|
logs: Data<LogRouteHandle>,
|
||||||
_req: HttpRequest,
|
|
||||||
) -> Result<HttpResponse, ResponseError> {
|
) -> Result<HttpResponse, ResponseError> {
|
||||||
if let Err(e) = logs.modify(|layer| *layer.inner_mut() = None) {
|
if let Err(e) = logs.modify(|layer| *layer.inner_mut() = None) {
|
||||||
tracing::error!("Could not free the logs route: {e}");
|
tracing::error!("Could not free the logs route: {e}");
|
||||||
|
98
meilisearch/tests/logs/error.rs
Normal file
98
meilisearch/tests/logs/error.rs
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
use meili_snap::*;
|
||||||
|
|
||||||
|
use crate::common::Server;
|
||||||
|
use crate::json;
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn logs_bad_target() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
|
||||||
|
// Wrong type
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "target": true })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.target`: expected a string, but found a boolean: `true`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Wrong type
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "target": [] })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.target`: expected a string, but found an array: `[]`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Our help message
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "target": "" })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value at `.target`: Empty string is not a valid target. If you want to get no logs use `OFF`. Usage: `info`, `info:meilisearch`, or you can write multiple filters in one target: `index_scheduler=info,milli=trace`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// An error from the target parser
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "target": "==" })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value at `.target`: invalid filter directive: too many '=' in filter directive, expected 0 or 1",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn logs_bad_mode() {
|
||||||
|
let server = Server::new().await;
|
||||||
|
|
||||||
|
// Wrong type
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "mode": true })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.mode`: expected a string, but found a boolean: `true`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Wrong type
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "mode": [] })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Invalid value type at `.mode`: expected a string, but found an array: `[]`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
|
||||||
|
// Wrong value
|
||||||
|
let (response, code) = server.service.post("/logs", json!({ "mode": "tamo" })).await;
|
||||||
|
snapshot!(code, @"400 Bad Request");
|
||||||
|
snapshot!(response, @r###"
|
||||||
|
{
|
||||||
|
"message": "Unknown value `tamo` at `.mode`: expected one of `fmt`, `profile`",
|
||||||
|
"code": "bad_request",
|
||||||
|
"type": "invalid_request",
|
||||||
|
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
}
|
89
meilisearch/tests/logs/mod.rs
Normal file
89
meilisearch/tests/logs/mod.rs
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
mod error;
|
||||||
|
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use actix_web::http::header::ContentType;
|
||||||
|
use meili_snap::snapshot;
|
||||||
|
use meilisearch::{analytics, create_app, Opt};
|
||||||
|
use tracing::level_filters::LevelFilter;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::Layer;
|
||||||
|
|
||||||
|
use crate::common::{default_settings, Server};
|
||||||
|
use crate::json;
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn basic_test_log_route() {
|
||||||
|
let db_path = tempfile::tempdir().unwrap();
|
||||||
|
let server =
|
||||||
|
Server::new_with_options(Opt { ..default_settings(db_path.path()) }).await.unwrap();
|
||||||
|
|
||||||
|
let (route_layer, route_layer_handle) =
|
||||||
|
tracing_subscriber::reload::Layer::new(None.with_filter(
|
||||||
|
tracing_subscriber::filter::Targets::new().with_target("", LevelFilter::OFF),
|
||||||
|
));
|
||||||
|
|
||||||
|
let subscriber = tracing_subscriber::registry().with(route_layer).with(
|
||||||
|
tracing_subscriber::fmt::layer()
|
||||||
|
.with_line_number(true)
|
||||||
|
.with_span_events(tracing_subscriber::fmt::format::FmtSpan::ACTIVE)
|
||||||
|
.with_filter(tracing_subscriber::filter::LevelFilter::from_str("INFO").unwrap()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let app = actix_web::test::init_service(create_app(
|
||||||
|
server.service.index_scheduler.clone().into(),
|
||||||
|
server.service.auth.clone().into(),
|
||||||
|
server.service.options.clone(),
|
||||||
|
route_layer_handle,
|
||||||
|
analytics::MockAnalytics::new(&server.service.options),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// set the subscriber as the default for the application
|
||||||
|
tracing::subscriber::set_global_default(subscriber).unwrap();
|
||||||
|
|
||||||
|
let app = Rc::new(app);
|
||||||
|
|
||||||
|
// First, we start listening on the `/logs` route
|
||||||
|
let handle_app = app.clone();
|
||||||
|
let handle = tokio::task::spawn_local(async move {
|
||||||
|
let req = actix_web::test::TestRequest::post()
|
||||||
|
.uri("/logs")
|
||||||
|
.insert_header(ContentType::json())
|
||||||
|
.set_payload(
|
||||||
|
serde_json::to_vec(&json!({
|
||||||
|
"mode": "fmt",
|
||||||
|
"target": "info",
|
||||||
|
}))
|
||||||
|
.unwrap(),
|
||||||
|
);
|
||||||
|
let req = req.to_request();
|
||||||
|
let ret = actix_web::test::call_service(&*handle_app, req).await;
|
||||||
|
actix_web::test::read_body(ret).await
|
||||||
|
});
|
||||||
|
|
||||||
|
// We're going to create an index to get at least one info log saying we processed a batch of task
|
||||||
|
let (ret, _code) = server.create_index(json!({ "uid": "tamo" })).await;
|
||||||
|
snapshot!(ret, @r###"
|
||||||
|
{
|
||||||
|
"taskUid": 0,
|
||||||
|
"indexUid": "tamo",
|
||||||
|
"status": "enqueued",
|
||||||
|
"type": "indexCreation",
|
||||||
|
"enqueuedAt": "[date]"
|
||||||
|
}
|
||||||
|
"###);
|
||||||
|
server.wait_task(ret.uid()).await;
|
||||||
|
|
||||||
|
let req = actix_web::test::TestRequest::delete().uri("/logs");
|
||||||
|
let req = req.to_request();
|
||||||
|
let ret = actix_web::test::call_service(&*app, req).await;
|
||||||
|
let code = ret.status();
|
||||||
|
snapshot!(code, @"204 No Content");
|
||||||
|
|
||||||
|
let logs = handle.await.unwrap();
|
||||||
|
let logs = String::from_utf8(logs.to_vec()).unwrap();
|
||||||
|
assert!(logs.contains("INFO"), "{logs}");
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user