Convert remaining clean-receiver free functions to methods
Profile (legacy-shape migration, load_into_engine, load_legacy_store), MetricDeclarationRecord/Dashboard seed constructors, PromqlQuery::parse, LogStreamHeader (detect/split_row/column_pattern/message_pattern), and api.rs's EngineHandle::ask/*Error::into_response/ParamDeclarationDto::reserved all had a single obvious receiver and are now methods on it instead of free functions taking that type as a parameter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A1ZJ3SrayzLzVndhVq9BmS
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
529f79e9da
commit
3c2995acc1
+60
-51
@@ -14,7 +14,7 @@ use crate::engine::{
|
||||
EngineResponse,
|
||||
};
|
||||
use crate::alerts::{deliver_ntfy, AlertError};
|
||||
use crate::profile::{self, Dashboard, ParamDeclarationRecord, Profile, ProfileError};
|
||||
use crate::profile::{Dashboard, ParamDeclarationRecord, Profile, ProfileError};
|
||||
use crate::settings::{CssError, SettingsHandle};
|
||||
use axum::extract::{Path as AxumPath, Query, State};
|
||||
use axum::http::{header, StatusCode};
|
||||
@@ -215,8 +215,9 @@ fn mime_for_extension(extension: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
async fn ask(engine: &EngineHandle, query: EngineQuery) -> Result<EngineResponse, StatusCode> {
|
||||
let read_handle = engine.reader_factory.handle();
|
||||
impl EngineHandle {
|
||||
async fn ask(&self, query: EngineQuery) -> Result<EngineResponse, StatusCode> {
|
||||
let read_handle = self.reader_factory.handle();
|
||||
if let Some(sort_by) = query.sort_by() {
|
||||
let needs_build = match read_handle.enter() {
|
||||
Some(guard) => !guard.has_sort_helper_index(sort_by),
|
||||
@@ -224,8 +225,7 @@ async fn ask(engine: &EngineHandle, query: EngineQuery) -> Result<EngineResponse
|
||||
};
|
||||
if needs_build {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
engine
|
||||
.tx
|
||||
self.tx
|
||||
.send(EngineMessage::EnsureSortHelperIndex { param_name: sort_by.to_string(), respond_to })
|
||||
.await
|
||||
.map_err(|_| StatusCode::SERVICE_UNAVAILABLE)?;
|
||||
@@ -234,6 +234,7 @@ async fn ask(engine: &EngineHandle, query: EngineQuery) -> Result<EngineResponse
|
||||
}
|
||||
let guard = read_handle.enter().ok_or(StatusCode::SERVICE_UNAVAILABLE)?;
|
||||
Ok(guard.handle_query_ro(query))
|
||||
}
|
||||
}
|
||||
|
||||
async fn promoted_param_records(
|
||||
@@ -252,7 +253,7 @@ async fn promoted_param_records(
|
||||
return Vec::new();
|
||||
}
|
||||
let Ok(EngineResponse::ParamDeclarations(declarations)) =
|
||||
ask(engine, EngineQuery::ParamDeclarations).await
|
||||
engine.ask(EngineQuery::ParamDeclarations).await
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
@@ -274,7 +275,7 @@ async fn get_logs(
|
||||
State(state): State<ApiState>,
|
||||
Query(params): Query<LogsParams>,
|
||||
) -> impl IntoResponse {
|
||||
match ask(&state.engine, EngineQuery::Logs(params)).await {
|
||||
match state.engine.ask(EngineQuery::Logs(params)).await {
|
||||
Ok(EngineResponse::Logs(entries)) => Json(entries).into_response(),
|
||||
Ok(EngineResponse::Error(err)) => {
|
||||
(StatusCode::BAD_REQUEST, Json(ErrorDto { error: err })).into_response()
|
||||
@@ -288,7 +289,7 @@ async fn get_timeline(
|
||||
State(state): State<ApiState>,
|
||||
Query(params): Query<TimelineParams>,
|
||||
) -> impl IntoResponse {
|
||||
match ask(&state.engine, EngineQuery::Timeline(params)).await {
|
||||
match state.engine.ask(EngineQuery::Timeline(params)).await {
|
||||
Ok(EngineResponse::Timeline(buckets)) => Json(buckets).into_response(),
|
||||
Ok(EngineResponse::Error(err)) => {
|
||||
(StatusCode::BAD_REQUEST, Json(ErrorDto { error: err })).into_response()
|
||||
@@ -299,7 +300,7 @@ async fn get_timeline(
|
||||
}
|
||||
|
||||
async fn get_stats(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
match ask(&state.engine, EngineQuery::Stats).await {
|
||||
match state.engine.ask(EngineQuery::Stats).await {
|
||||
Ok(EngineResponse::Stats(stats)) => Json(stats).into_response(),
|
||||
Ok(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
Err(status) => status.into_response(),
|
||||
@@ -307,9 +308,9 @@ async fn get_stats(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
async fn list_params(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
match ask(&state.engine, EngineQuery::ParamDeclarations).await {
|
||||
match state.engine.ask(EngineQuery::ParamDeclarations).await {
|
||||
Ok(EngineResponse::ParamDeclarations(mut declarations)) => {
|
||||
declarations.extend(reserved_param_declaration_dtos());
|
||||
declarations.extend(ParamDeclarationDto::reserved());
|
||||
Json(declarations).into_response()
|
||||
}
|
||||
Ok(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
@@ -317,7 +318,8 @@ async fn list_params(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
}
|
||||
}
|
||||
|
||||
fn reserved_param_declaration_dtos() -> [ParamDeclarationDto; 3] {
|
||||
impl ParamDeclarationDto {
|
||||
fn reserved() -> [ParamDeclarationDto; 3] {
|
||||
[
|
||||
ParamDeclarationDto {
|
||||
name: "ottyl_seq".to_string(),
|
||||
@@ -350,6 +352,7 @@ fn reserved_param_declaration_dtos() -> [ParamDeclarationDto; 3] {
|
||||
reserved: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
async fn declare_param(
|
||||
@@ -402,7 +405,7 @@ async fn clear_params(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
}
|
||||
|
||||
async fn list_metrics(State(state): State<ApiState>) -> impl IntoResponse {
|
||||
match ask(&state.engine, EngineQuery::MetricDeclarations).await {
|
||||
match state.engine.ask(EngineQuery::MetricDeclarations).await {
|
||||
Ok(EngineResponse::MetricDeclarations(declarations)) => Json(declarations).into_response(),
|
||||
Ok(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
Err(status) => status.into_response(),
|
||||
@@ -521,7 +524,7 @@ async fn get_metric_bucket_values(
|
||||
since_sequence: params.since_sequence,
|
||||
limit: params.limit,
|
||||
};
|
||||
match ask(&state.engine, query).await {
|
||||
match state.engine.ask(query).await {
|
||||
Ok(EngineResponse::BucketValues(values)) => Json(values).into_response(),
|
||||
Ok(_) => StatusCode::INTERNAL_SERVER_ERROR.into_response(),
|
||||
Err(status) => status.into_response(),
|
||||
@@ -537,7 +540,7 @@ async fn run_promql_query(
|
||||
start_ms: params.start_ms,
|
||||
end_ms: params.end_ms,
|
||||
};
|
||||
match ask(&state.engine, query).await {
|
||||
match state.engine.ask(query).await {
|
||||
Ok(EngineResponse::Promql(Ok(series))) => Json(series).into_response(),
|
||||
Ok(EngineResponse::Promql(Err(err))) => {
|
||||
(StatusCode::BAD_REQUEST, Json(ErrorDto { error: err })).into_response()
|
||||
@@ -547,11 +550,13 @@ async fn run_promql_query(
|
||||
}
|
||||
}
|
||||
|
||||
fn css_error_response(err: CssError) -> axum::response::Response {
|
||||
match err {
|
||||
CssError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
|
||||
CssError::NotImported => (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
|
||||
CssError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
impl CssError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
CssError::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(),
|
||||
CssError::NotImported => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||
CssError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,7 +581,7 @@ async fn import_css(
|
||||
.expect("entry was just inserted by import_css");
|
||||
(StatusCode::CREATED, Json(CssEntryDto::from(entry))).into_response()
|
||||
}
|
||||
Err(err) => css_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -593,7 +598,7 @@ async fn link_css(
|
||||
.expect("entry was just pushed by link_css");
|
||||
(StatusCode::CREATED, Json(CssEntryDto::from(entry))).into_response()
|
||||
}
|
||||
Err(err) => css_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -605,7 +610,7 @@ async fn remove_css(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.remove_css(&state.settings_path, &css_dir, &id) {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => css_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -622,14 +627,16 @@ async fn get_css_content(
|
||||
content,
|
||||
)
|
||||
.into_response(),
|
||||
Err(err) => css_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
fn alert_error_response(err: AlertError) -> axum::response::Response {
|
||||
match err {
|
||||
AlertError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
|
||||
AlertError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
impl AlertError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
AlertError::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(),
|
||||
AlertError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,7 +652,7 @@ async fn create_alert_rule(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.create_alert_rule(&state.settings_path, body.name, body.enabled, body.condition) {
|
||||
Ok(rule) => (StatusCode::CREATED, Json(rule)).into_response(),
|
||||
Err(err) => alert_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -657,7 +664,7 @@ async fn update_alert_rule(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.update_alert_rule(&state.settings_path, &id, body.name, body.enabled, body.condition) {
|
||||
Ok(rule) => Json(rule).into_response(),
|
||||
Err(err) => alert_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -665,7 +672,7 @@ async fn delete_alert_rule(State(state): State<ApiState>, AxumPath(id): AxumPath
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.remove_alert_rule(&state.settings_path, &id) {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => alert_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -721,12 +728,14 @@ async fn update_settings(
|
||||
Json(new_max_logs.0).into_response()
|
||||
}
|
||||
|
||||
fn profile_error_response(err: ProfileError) -> axum::response::Response {
|
||||
match err {
|
||||
ProfileError::NotFound => (StatusCode::NOT_FOUND, err.to_string()).into_response(),
|
||||
ProfileError::DuplicateName => (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
|
||||
ProfileError::CannotDeleteDefault => (StatusCode::BAD_REQUEST, err.to_string()).into_response(),
|
||||
ProfileError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()).into_response(),
|
||||
impl ProfileError {
|
||||
fn into_response(self) -> axum::response::Response {
|
||||
match self {
|
||||
ProfileError::NotFound => (StatusCode::NOT_FOUND, self.to_string()).into_response(),
|
||||
ProfileError::DuplicateName => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||
ProfileError::CannotDeleteDefault => (StatusCode::BAD_REQUEST, self.to_string()).into_response(),
|
||||
ProfileError::Io(_) => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -746,7 +755,7 @@ async fn create_profile(
|
||||
let profile = settings.profile(&body.name).expect("profile was just created by create_profile");
|
||||
(StatusCode::CREATED, Json(ProfileSummaryDto::from(profile))).into_response()
|
||||
}
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,13 +766,13 @@ async fn delete_profile(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.delete_profile(&state.settings_path, &name) {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn reload_engine_declarations(engine: &EngineHandle, profile: &Profile) {
|
||||
if let Ok(EngineResponse::ParamDeclarations(params)) =
|
||||
ask(engine, EngineQuery::ParamDeclarations).await
|
||||
engine.ask(EngineQuery::ParamDeclarations).await
|
||||
{
|
||||
for param in params.into_iter().filter(|param| !param.auto_derived) {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
@@ -778,7 +787,7 @@ async fn reload_engine_declarations(engine: &EngineHandle, profile: &Profile) {
|
||||
}
|
||||
}
|
||||
if let Ok(EngineResponse::MetricDeclarations(metrics)) =
|
||||
ask(engine, EngineQuery::MetricDeclarations).await
|
||||
engine.ask(EngineQuery::MetricDeclarations).await
|
||||
{
|
||||
for metric in metrics {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
@@ -792,7 +801,7 @@ async fn reload_engine_declarations(engine: &EngineHandle, profile: &Profile) {
|
||||
}
|
||||
}
|
||||
}
|
||||
profile::load_into_engine(engine, profile).await;
|
||||
profile.load_into_engine(engine).await;
|
||||
}
|
||||
|
||||
async fn activate_profile(
|
||||
@@ -815,7 +824,7 @@ async fn activate_profile(
|
||||
let settings = state.settings.lock().unwrap();
|
||||
Json(SettingsDto::from(&*settings)).into_response()
|
||||
}
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -829,7 +838,7 @@ async fn list_dashboards(
|
||||
let dtos: Vec<DashboardDto> = dashboards.iter().map(DashboardDto::from).collect();
|
||||
Json(dtos).into_response()
|
||||
}
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,7 +850,7 @@ async fn create_dashboard(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.create_dashboard(&state.settings_path, &name, body.name) {
|
||||
Ok(dashboard) => (StatusCode::CREATED, Json(DashboardDto::from(&dashboard))).into_response(),
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,9 +862,9 @@ async fn get_dashboard(
|
||||
match settings.list_dashboards(&name) {
|
||||
Ok(dashboards) => match dashboards.iter().find(|d| d.id == dashboard_id) {
|
||||
Some(dashboard) => Json(DashboardDto::from(dashboard)).into_response(),
|
||||
None => profile_error_response(ProfileError::NotFound),
|
||||
None => ProfileError::NotFound.into_response(),
|
||||
},
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,7 +881,7 @@ async fn save_dashboard(
|
||||
};
|
||||
match settings.save_dashboard(&state.settings_path, &name, &dashboard_id, dashboard) {
|
||||
Ok(dashboard) => Json(DashboardDto::from(&dashboard)).into_response(),
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -883,7 +892,7 @@ async fn delete_dashboard(
|
||||
let mut settings = state.settings.lock().unwrap();
|
||||
match settings.delete_dashboard(&state.settings_path, &name, &dashboard_id) {
|
||||
Ok(()) => StatusCode::NO_CONTENT.into_response(),
|
||||
Err(err) => profile_error_response(err),
|
||||
Err(err) => err.into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1512,9 +1521,9 @@ mod tests {
|
||||
|
||||
let fresh_settings_handle = std::sync::Arc::new(std::sync::Mutex::new(Settings::default()));
|
||||
let fresh_engine_tx = crate::engine::spawn(fresh_settings_handle, 16);
|
||||
profile::load_into_engine(&fresh_engine_tx, profile).await;
|
||||
profile.load_into_engine(&fresh_engine_tx).await;
|
||||
|
||||
let response = ask(&fresh_engine_tx, EngineQuery::MetricDeclarations).await.unwrap();
|
||||
let response = fresh_engine_tx.ask(EngineQuery::MetricDeclarations).await.unwrap();
|
||||
let EngineResponse::MetricDeclarations(metrics) = response else {
|
||||
panic!("unexpected response");
|
||||
};
|
||||
|
||||
+45
-45
@@ -13,7 +13,7 @@ use crate::filter_datetime;
|
||||
use crate::filter_parser::{self, CompareOp, FilterExpr, FilterValue};
|
||||
use crate::index_key::IndexKey;
|
||||
use crate::metric_primitive::{MetricPrimitive, Number};
|
||||
use crate::promql::{self, AggOp, LabelMatcher, PromqlQuery};
|
||||
use crate::promql::{AggOp, LabelMatcher, PromqlQuery};
|
||||
use crate::rate;
|
||||
use crate::settings::SettingsHandle;
|
||||
use crate::timestamp::Timestamp;
|
||||
@@ -208,7 +208,7 @@ async fn run(
|
||||
let ingest_lag_high_watermark_ms =
|
||||
ingest_lag_high_watermark_ms(&ingest_lag_samples, processed_at);
|
||||
|
||||
let is_new_header = crate::logfmt::detect_log_stream_header(&text).is_some();
|
||||
let is_new_header = crate::logfmt::LogStreamHeader::detect(&text).is_some();
|
||||
log_stream_header = log_stream_header_transition(&log_stream_header, &text);
|
||||
let row_header = if is_new_header { None } else { log_stream_header.clone() };
|
||||
|
||||
@@ -527,7 +527,7 @@ fn log_stream_header_transition(
|
||||
current: &Option<crate::logfmt::LogStreamHeader>,
|
||||
text: &str,
|
||||
) -> Option<crate::logfmt::LogStreamHeader> {
|
||||
crate::logfmt::detect_log_stream_header(text).or_else(|| current.clone())
|
||||
crate::logfmt::LogStreamHeader::detect(text).or_else(|| current.clone())
|
||||
}
|
||||
|
||||
fn derive_log_stream_params_from_row(
|
||||
@@ -538,7 +538,7 @@ fn derive_log_stream_params_from_row(
|
||||
if !crate::logfmt::is_log_stream_line(text) {
|
||||
return;
|
||||
}
|
||||
let row = crate::logfmt::split_log_stream_row(text, header);
|
||||
let row = header.split_row(text);
|
||||
|
||||
for (i, column) in header.columns.iter().enumerate() {
|
||||
let Some(value) = row.get(i) else { continue };
|
||||
@@ -550,7 +550,7 @@ fn derive_log_stream_params_from_row(
|
||||
index_key,
|
||||
DataValue::ParamDeclaration(ParamDeclaration {
|
||||
name: column.name.clone(),
|
||||
message_match_regex: crate::logfmt::log_stream_column_pattern(header, i, &column.name),
|
||||
message_match_regex: header.column_pattern(i, &column.name),
|
||||
capture_group: column.name.clone(),
|
||||
json_field_path: None,
|
||||
kind: infer_kind(value.trim()),
|
||||
@@ -565,7 +565,7 @@ fn derive_log_stream_params_from_row(
|
||||
index.entry(IndexKey::ParamDeclaration { name: "message".to_string() }).or_insert_with(|| {
|
||||
DataValue::ParamDeclaration(ParamDeclaration {
|
||||
name: "message".to_string(),
|
||||
message_match_regex: crate::logfmt::log_stream_message_pattern(header, "message"),
|
||||
message_match_regex: header.message_pattern("message"),
|
||||
capture_group: "message".to_string(),
|
||||
json_field_path: None,
|
||||
kind: infer_kind(message.trim()),
|
||||
@@ -583,7 +583,7 @@ fn auto_declare_log_stream_params(
|
||||
header: &mut Option<crate::logfmt::LogStreamHeader>,
|
||||
text: &str,
|
||||
) {
|
||||
let is_new_header = crate::logfmt::detect_log_stream_header(text).is_some();
|
||||
let is_new_header = crate::logfmt::LogStreamHeader::detect(text).is_some();
|
||||
*header = log_stream_header_transition(header, text);
|
||||
if is_new_header {
|
||||
return;
|
||||
@@ -1802,7 +1802,7 @@ fn handle_query_inner(
|
||||
EngineResponse::BucketValues(values)
|
||||
}
|
||||
EngineQuery::Promql { query, start_ms, end_ms } => {
|
||||
let response = promql::parse_query(&query)
|
||||
let response = PromqlQuery::parse(&query)
|
||||
.and_then(|parsed| snapshot.evaluate_promql(&parsed, start_ms, end_ms));
|
||||
EngineResponse::Promql(response)
|
||||
}
|
||||
@@ -2797,7 +2797,7 @@ mod tests {
|
||||
declare_requests_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("requests").unwrap();
|
||||
let parsed = PromqlQuery::parse("requests").unwrap();
|
||||
assert!(index.evaluate_promql(&parsed, None, None).unwrap().len() == 1);
|
||||
|
||||
assert!(index.remove_metric_declaration("requests"));
|
||||
@@ -2816,7 +2816,7 @@ mod tests {
|
||||
Timestamp::from_millis(0),
|
||||
MetricPrimitive::Number(Number::Float(1.0)),
|
||||
);
|
||||
let parsed = promql::parse_query("scraped_metric").unwrap();
|
||||
let parsed = PromqlQuery::parse("scraped_metric").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
}
|
||||
@@ -2824,7 +2824,7 @@ mod tests {
|
||||
#[test]
|
||||
fn promql_rejects_metric_with_no_declaration_and_no_data() {
|
||||
let index = EngineSnapshot::default();
|
||||
let parsed = promql::parse_query("never_declared").unwrap();
|
||||
let parsed = PromqlQuery::parse("never_declared").unwrap();
|
||||
let err = index.evaluate_promql(&parsed, None, None).unwrap_err();
|
||||
assert!(err.contains("unknown metric"), "unexpected error message: {err}");
|
||||
}
|
||||
@@ -3053,7 +3053,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "request path=/b");
|
||||
ingest_log(&mut index, 2, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query(r#"requests{path="/a"}"#).unwrap();
|
||||
let parsed = PromqlQuery::parse(r#"requests{path="/a"}"#).unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3067,7 +3067,7 @@ mod tests {
|
||||
declare_requests_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query(r#"requests{path="/nope"}"#).unwrap();
|
||||
let parsed = PromqlQuery::parse(r#"requests{path="/nope"}"#).unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert!(series.is_empty());
|
||||
}
|
||||
@@ -3079,7 +3079,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
|
||||
let parsed = promql::parse_query("sum(requests)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(requests)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3095,7 +3095,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
ingest_log(&mut index, 1, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("sum(requests) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(requests) by (path)").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("path").cmp(&b.labels.get("path")));
|
||||
|
||||
@@ -3154,7 +3154,7 @@ mod tests {
|
||||
declare_string_valued_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request status=ok");
|
||||
|
||||
let parsed = promql::parse_query("sum(statuses)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(statuses)").unwrap();
|
||||
let err = index.evaluate_promql(&parsed, None, None).unwrap_err();
|
||||
|
||||
assert!(err.contains("statuses"), "error should name the offending metric: {err}");
|
||||
@@ -3167,7 +3167,7 @@ mod tests {
|
||||
declare_string_valued_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request status=ok");
|
||||
|
||||
let parsed = promql::parse_query("avg(statuses)").unwrap();
|
||||
let parsed = PromqlQuery::parse("avg(statuses)").unwrap();
|
||||
let err = index.evaluate_promql(&parsed, None, None).unwrap_err();
|
||||
|
||||
assert!(err.contains("statuses"));
|
||||
@@ -3180,7 +3180,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request status=ok");
|
||||
ingest_log(&mut index, 1, "request status=error");
|
||||
|
||||
let parsed = promql::parse_query("count(statuses)").unwrap();
|
||||
let parsed = PromqlQuery::parse("count(statuses)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3195,7 +3195,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
|
||||
let parsed = promql::parse_query("sum(requests)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(requests)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3209,7 +3209,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request status=ok");
|
||||
ingest_log(&mut index, 1000, "request status=error");
|
||||
|
||||
let parsed = promql::parse_query("sum(rate(statuses[2s]))").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(rate(statuses[2s]))").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None);
|
||||
|
||||
assert!(series.is_ok(), "sum(rate(...)) never reads the coerced value, so it should not be rejected: {series:?}");
|
||||
@@ -3221,7 +3221,7 @@ mod tests {
|
||||
declare_string_valued_metric(&mut index);
|
||||
ingest_log(&mut index, 0, "request status=ok");
|
||||
|
||||
let parsed = promql::parse_query("statuses").unwrap();
|
||||
let parsed = PromqlQuery::parse("statuses").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None);
|
||||
|
||||
assert!(series.is_ok());
|
||||
@@ -3235,7 +3235,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "request path=/a host=web2");
|
||||
ingest_log(&mut index, 2, "request path=/a host=web3");
|
||||
|
||||
let parsed = promql::parse_query("sum(requests) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(requests) by (path)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3256,7 +3256,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a host=web1");
|
||||
ingest_log(&mut index, 1, "request path=/a host=web2");
|
||||
|
||||
let parsed = promql::parse_query("avg(requests) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("avg(requests) by (path)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3271,7 +3271,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 1, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("sum(requests) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(requests) by (path)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3285,7 +3285,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 1000, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("rate(requests[2s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(requests[2s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3300,7 +3300,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1000, "request path=/a");
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
|
||||
let parsed = promql::parse_query("sum(rate(requests[2s])) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(rate(requests[2s])) by (path)").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("path").cmp(&b.labels.get("path")));
|
||||
|
||||
@@ -3318,7 +3318,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 1, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("rate(requests[2s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(requests[2s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, Some(100_000)).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3335,7 +3335,7 @@ mod tests {
|
||||
declare_requests_metric(&mut index);
|
||||
ingest_log(&mut index, 50_000, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("rate(requests[2s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(requests[2s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, Some(0), None).unwrap();
|
||||
|
||||
assert_eq!(series[0].points, vec![(0, 0.0), (50_000, 0.5)]);
|
||||
@@ -3349,7 +3349,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "request path=/a");
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
|
||||
let parsed = promql::parse_query("sum(rate(requests[2s])) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(rate(requests[2s])) by (path)").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, Some(100_000)).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("path").cmp(&b.labels.get("path")));
|
||||
|
||||
@@ -3365,7 +3365,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 1000, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("rate(requests[2s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(requests[2s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, Some(0), Some(1000)).unwrap();
|
||||
|
||||
assert_eq!(series[0].points, vec![(0, 0.5), (1000, 1.0)]);
|
||||
@@ -3387,7 +3387,7 @@ mod tests {
|
||||
ingest_log(&mut index, *ms, text);
|
||||
}
|
||||
|
||||
let parsed = promql::parse_query("rate(requests[2s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(requests[2s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
let (_, dashboard_rate) = *series[0].points.last().unwrap();
|
||||
@@ -3431,7 +3431,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "request path=/a");
|
||||
ingest_log(&mut index, 0, "request path=/b");
|
||||
|
||||
let parsed = promql::parse_query("sum(rate(requests[2s]))").unwrap();
|
||||
let parsed = PromqlQuery::parse("sum(rate(requests[2s]))").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3447,7 +3447,7 @@ mod tests {
|
||||
ingest_log(&mut index, 5, "request path=/a");
|
||||
ingest_log(&mut index, 10, "request path=/a");
|
||||
|
||||
let parsed = promql::parse_query("requests").unwrap();
|
||||
let parsed = PromqlQuery::parse("requests").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, Some(1), Some(9)).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3503,7 +3503,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "request path=/a duration=9");
|
||||
ingest_log(&mut index, 2, "request path=/b duration=42");
|
||||
|
||||
let parsed = promql::parse_query("gauge(durations)").unwrap();
|
||||
let parsed = PromqlQuery::parse("gauge(durations)").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("path").cmp(&b.labels.get("path")));
|
||||
|
||||
@@ -3522,7 +3522,7 @@ mod tests {
|
||||
ingest_log(&mut index, 5, "request path=/a duration=9");
|
||||
ingest_log(&mut index, 10, "request path=/a duration=42");
|
||||
|
||||
let parsed = promql::parse_query("gauge(durations)").unwrap();
|
||||
let parsed = PromqlQuery::parse("gauge(durations)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, Some(9)).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3537,7 +3537,7 @@ mod tests {
|
||||
ingest_log(&mut index, seq, &format!("request path=/a duration={duration}"));
|
||||
}
|
||||
|
||||
let parsed = promql::parse_query("histogram(durations[10])").unwrap();
|
||||
let parsed = PromqlQuery::parse("histogram(durations[10])").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("le").cmp(&b.labels.get("le")));
|
||||
|
||||
@@ -3563,7 +3563,7 @@ mod tests {
|
||||
ingest_log(&mut index, seq, &format!("request path={path} duration={duration}"));
|
||||
}
|
||||
|
||||
let parsed = promql::parse_query("histogram(durations[10]) by (path)").unwrap();
|
||||
let parsed = PromqlQuery::parse("histogram(durations[10]) by (path)").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| {
|
||||
a.labels.get("path").cmp(&b.labels.get("path")).then(a.labels.get("le").cmp(&b.labels.get("le")))
|
||||
@@ -3631,7 +3631,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "user=alice logged in");
|
||||
ingest_log(&mut index, 10, "user=alice logged in");
|
||||
|
||||
let parsed = promql::parse_query("login_gap").unwrap();
|
||||
let parsed = PromqlQuery::parse("login_gap").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
|
||||
assert_eq!(series.len(), 1);
|
||||
@@ -3678,7 +3678,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "all good");
|
||||
ingest_log(&mut index, 2, "all good again");
|
||||
|
||||
let parsed = promql::parse_query("log_category").unwrap();
|
||||
let parsed = PromqlQuery::parse("log_category").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
series.sort_by(|a, b| a.labels.get("category").cmp(&b.labels.get("category")));
|
||||
|
||||
@@ -3750,7 +3750,7 @@ mod tests {
|
||||
ingest_log(&mut index, 1, "line two");
|
||||
ingest_log(&mut index, 2, "line three");
|
||||
|
||||
let parsed = promql::parse_query("count(ottyl_seq)").unwrap();
|
||||
let parsed = PromqlQuery::parse("count(ottyl_seq)").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
assert_eq!(series[0].points, vec![(0, 1.0), (1, 1.0), (2, 1.0)]);
|
||||
@@ -3762,7 +3762,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "line zero");
|
||||
ingest_log(&mut index, 5, "line five");
|
||||
|
||||
let parsed = promql::parse_query("ottyl_seq").unwrap();
|
||||
let parsed = PromqlQuery::parse("ottyl_seq").unwrap();
|
||||
let mut series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
series[0].points.sort_by_key(|(ts, _)| *ts);
|
||||
@@ -3775,7 +3775,7 @@ mod tests {
|
||||
ingest_log(&mut index, 0, "a");
|
||||
ingest_log(&mut index, 1, "b");
|
||||
|
||||
let parsed = promql::parse_query("ottyl_ts").unwrap();
|
||||
let parsed = PromqlQuery::parse("ottyl_ts").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
assert_eq!(series[0].points, vec![(0, 0.0), (1, 1.0)]);
|
||||
@@ -3788,7 +3788,7 @@ mod tests {
|
||||
ingest_log(&mut index, sequence, "line");
|
||||
}
|
||||
|
||||
let parsed = promql::parse_query("rate(ottyl_ts[5s])").unwrap();
|
||||
let parsed = PromqlQuery::parse("rate(ottyl_ts[5s])").unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert_eq!(series.len(), 1);
|
||||
assert!(!series[0].points.is_empty(), "rate(ottyl_ts[...]) must not silently return no data");
|
||||
@@ -3799,7 +3799,7 @@ mod tests {
|
||||
let mut index = EngineSnapshot::default();
|
||||
ingest_log(&mut index, 0, "line");
|
||||
|
||||
let parsed = promql::parse_query(r#"ottyl_seq{path="/a"}"#).unwrap();
|
||||
let parsed = PromqlQuery::parse(r#"ottyl_seq{path="/a"}"#).unwrap();
|
||||
let series = index.evaluate_promql(&parsed, None, None).unwrap();
|
||||
assert!(series.is_empty());
|
||||
}
|
||||
@@ -3809,7 +3809,7 @@ mod tests {
|
||||
let mut index = EngineSnapshot::default();
|
||||
ingest_log(&mut index, 0, r#"{"level":"error"}"#);
|
||||
|
||||
let parsed = promql::parse_query("count(ottyl_fmt)").unwrap();
|
||||
let parsed = PromqlQuery::parse("count(ottyl_fmt)").unwrap();
|
||||
let err = index.evaluate_promql(&parsed, None, None).unwrap_err();
|
||||
assert!(err.contains("ottyl_fmt"), "error should name the rejected metric: {err}");
|
||||
assert!(err.contains("categorical") || err.contains("filter"), "error should explain why: {err}");
|
||||
|
||||
+37
-35
@@ -138,7 +138,8 @@ fn char_slice(s: &str, start: usize, end: Option<usize>) -> &str {
|
||||
&s[byte_start..byte_end.max(byte_start)]
|
||||
}
|
||||
|
||||
pub fn detect_log_stream_header(line: &str) -> Option<LogStreamHeader> {
|
||||
impl LogStreamHeader {
|
||||
pub fn detect(line: &str) -> Option<LogStreamHeader> {
|
||||
if is_log_stream_line(line) {
|
||||
return None;
|
||||
}
|
||||
@@ -157,15 +158,15 @@ pub fn detect_log_stream_header(line: &str) -> Option<LogStreamHeader> {
|
||||
}
|
||||
let tab_delimited = WHITESPACE_RUN.find_iter(line).any(|m| m.as_str().contains('\t'));
|
||||
Some(LogStreamHeader { columns, tab_delimited })
|
||||
}
|
||||
|
||||
pub fn split_log_stream_row<'a>(line: &'a str, header: &LogStreamHeader) -> Vec<&'a str> {
|
||||
if header.tab_delimited {
|
||||
return split_columns(line, Some(header.columns.len() + 1));
|
||||
}
|
||||
let mut result = Vec::with_capacity(header.columns.len() + 1);
|
||||
for (i, column) in header.columns.iter().enumerate() {
|
||||
match header.columns.get(i + 1) {
|
||||
|
||||
pub fn split_row<'a>(&self, line: &'a str) -> Vec<&'a str> {
|
||||
if self.tab_delimited {
|
||||
return split_columns(line, Some(self.columns.len() + 1));
|
||||
}
|
||||
let mut result = Vec::with_capacity(self.columns.len() + 1);
|
||||
for (i, column) in self.columns.iter().enumerate() {
|
||||
match self.columns.get(i + 1) {
|
||||
Some(next) => result.push(char_slice(line, column.start, Some(next.start)).trim()),
|
||||
None => {
|
||||
let tail = char_slice(line, column.start, None);
|
||||
@@ -178,10 +179,10 @@ pub fn split_log_stream_row<'a>(line: &'a str, header: &LogStreamHeader) -> Vec<
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_stream_column_pattern(header: &LogStreamHeader, index: usize, capture_group: &str) -> String {
|
||||
if header.tab_delimited {
|
||||
pub fn column_pattern(&self, index: usize, capture_group: &str) -> String {
|
||||
if self.tab_delimited {
|
||||
let mut pattern = String::from("^");
|
||||
for _ in 0..index {
|
||||
pattern.push_str(COLUMN_VALUE_PATTERN);
|
||||
@@ -190,26 +191,27 @@ pub fn log_stream_column_pattern(header: &LogStreamHeader, index: usize, capture
|
||||
pattern.push_str(&format!("(?P<{capture_group}>{COLUMN_VALUE_PATTERN})"));
|
||||
return pattern;
|
||||
}
|
||||
let start = header.columns[index].start;
|
||||
let value_pattern = match header.columns.get(index + 1) {
|
||||
let start = self.columns[index].start;
|
||||
let value_pattern = match self.columns.get(index + 1) {
|
||||
Some(next) => bounded_column_value_pattern(next.start.saturating_sub(start)),
|
||||
None => COLUMN_VALUE_PATTERN.to_string(),
|
||||
};
|
||||
format!("^.{{{start}}}(?P<{capture_group}>{value_pattern})")
|
||||
}
|
||||
}
|
||||
|
||||
pub fn log_stream_message_pattern(header: &LogStreamHeader, capture_group: &str) -> String {
|
||||
if header.tab_delimited {
|
||||
pub fn message_pattern(&self, capture_group: &str) -> String {
|
||||
if self.tab_delimited {
|
||||
let mut pattern = String::from("^");
|
||||
for _ in 0..header.columns.len() {
|
||||
for _ in 0..self.columns.len() {
|
||||
pattern.push_str(COLUMN_VALUE_PATTERN);
|
||||
pattern.push_str(COLUMN_SEPARATOR_PATTERN);
|
||||
}
|
||||
pattern.push_str(&format!("(?P<{capture_group}>.*)$"));
|
||||
return pattern;
|
||||
}
|
||||
let start = header.columns.last().map(|c| c.start).unwrap_or(0);
|
||||
let start = self.columns.last().map(|c| c.start).unwrap_or(0);
|
||||
format!("^.{{{start}}}{COLUMN_VALUE_PATTERN}{COLUMN_SEPARATOR_PATTERN}(?P<{capture_group}>.*)$")
|
||||
}
|
||||
}
|
||||
|
||||
fn go_log_prefix(input: &str) -> IResult<&str, (&str, &str, &str)> {
|
||||
@@ -368,7 +370,7 @@ mod tests {
|
||||
#[test]
|
||||
fn detects_log_stream_header_columns_generically() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).expect("fixture header must be detected");
|
||||
let header = LogStreamHeader::detect(header_line).expect("fixture header must be detected");
|
||||
assert_eq!(column_names(&header), vec!["Timestamp", "Thread", "Type", "Activity", "PID", "TTL"]);
|
||||
assert!(!header.tab_delimited);
|
||||
}
|
||||
@@ -376,21 +378,21 @@ mod tests {
|
||||
#[test]
|
||||
fn a_log_stream_data_line_is_never_detected_as_its_own_header() {
|
||||
let data_line = LOG_STREAM_FIXTURE.lines().nth(1).unwrap();
|
||||
assert!(detect_log_stream_header(data_line).is_none());
|
||||
assert!(LogStreamHeader::detect(data_line).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plain_prose_is_not_detected_as_a_log_stream_header() {
|
||||
assert!(detect_log_stream_header("just some plain prose here").is_none());
|
||||
assert!(detect_log_stream_header("ts=1788648105773 msg=\"entity_moved\"").is_none());
|
||||
assert!(LogStreamHeader::detect("just some plain prose here").is_none());
|
||||
assert!(LogStreamHeader::detect("ts=1788648105773 msg=\"entity_moved\"").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn splits_log_stream_row_into_named_columns_plus_trailing_message() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).unwrap();
|
||||
let header = LogStreamHeader::detect(header_line).unwrap();
|
||||
let data_line = "2026-09-06 01:48:54.518117+0100 0x4dfed2 Default 0x0 606 0 WindowServer: (HSTouchHIDService) something happened";
|
||||
let row = split_log_stream_row(data_line, &header);
|
||||
let row = header.split_row(data_line);
|
||||
assert_eq!(row.len(), header.columns.len() + 1);
|
||||
assert_eq!(row[0], "2026-09-06 01:48:54.518117+0100");
|
||||
assert_eq!(row[1], "0x4dfed2");
|
||||
@@ -403,12 +405,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn a_single_tab_separates_columns_exactly_like_a_run_of_spaces() {
|
||||
let header = detect_log_stream_header("Timestamp\tThread\tType").unwrap();
|
||||
let header = LogStreamHeader::detect("Timestamp\tThread\tType").unwrap();
|
||||
assert_eq!(column_names(&header), vec!["Timestamp", "Thread", "Type"]);
|
||||
assert!(header.tab_delimited);
|
||||
|
||||
let spaced = split_log_stream_row("2026-09-06 01:48:54\t0x4dfed2\t\tDefault trailing message", &header);
|
||||
let tabbed = split_log_stream_row("2026-09-06 01:48:54\t0x4dfed2\tDefault trailing message", &header);
|
||||
let spaced = header.split_row("2026-09-06 01:48:54\t0x4dfed2\t\tDefault trailing message");
|
||||
let tabbed = header.split_row("2026-09-06 01:48:54\t0x4dfed2\tDefault trailing message");
|
||||
assert_eq!(spaced[0], "2026-09-06 01:48:54");
|
||||
assert_eq!(spaced[1], "0x4dfed2");
|
||||
assert_eq!(spaced[2], "Default trailing message");
|
||||
@@ -420,22 +422,22 @@ mod tests {
|
||||
#[test]
|
||||
fn column_and_message_patterns_capture_the_same_fields_split_log_stream_row_finds() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).unwrap();
|
||||
let header = LogStreamHeader::detect(header_line).unwrap();
|
||||
let line = "2026-09-06 01:48:54.518117+0100 0x4dfed2 Default 0x0 606 0 WindowServer: (HSTouchHIDService) something happened";
|
||||
|
||||
let type_re = Regex::new(&log_stream_column_pattern(&header, 2, "Type")).unwrap();
|
||||
let type_re = Regex::new(&header.column_pattern(2, "Type")).unwrap();
|
||||
assert_eq!(type_re.captures(line).unwrap().name("Type").unwrap().as_str(), "Default");
|
||||
|
||||
let pid_re = Regex::new(&log_stream_column_pattern(&header, 4, "PID")).unwrap();
|
||||
let pid_re = Regex::new(&header.column_pattern(4, "PID")).unwrap();
|
||||
assert_eq!(pid_re.captures(line).unwrap().name("PID").unwrap().as_str(), "606");
|
||||
|
||||
let message_re = Regex::new(&log_stream_message_pattern(&header, "message")).unwrap();
|
||||
let message_re = Regex::new(&header.message_pattern("message")).unwrap();
|
||||
assert_eq!(
|
||||
message_re.captures(line).unwrap().name("message").unwrap().as_str(),
|
||||
"WindowServer: (HSTouchHIDService) something happened"
|
||||
);
|
||||
|
||||
let timestamp_re = Regex::new(&log_stream_column_pattern(&header, 0, "Timestamp")).unwrap();
|
||||
let timestamp_re = Regex::new(&header.column_pattern(0, "Timestamp")).unwrap();
|
||||
assert_eq!(
|
||||
timestamp_re.captures(line).unwrap().name("Timestamp").unwrap().as_str(),
|
||||
"2026-09-06 01:48:54.518117+0100"
|
||||
@@ -445,8 +447,8 @@ mod tests {
|
||||
#[test]
|
||||
fn is_log_stream_line_rejects_non_columnar_lines_that_a_fixed_offset_column_regex_would_still_match() {
|
||||
let header_line = LOG_STREAM_FIXTURE.lines().next().unwrap();
|
||||
let header = detect_log_stream_header(header_line).unwrap();
|
||||
let timestamp_re = Regex::new(&log_stream_column_pattern(&header, 0, "Timestamp")).unwrap();
|
||||
let header = LogStreamHeader::detect(header_line).unwrap();
|
||||
let timestamp_re = Regex::new(&header.column_pattern(0, "Timestamp")).unwrap();
|
||||
|
||||
let continuation_line = "so_gencnt: 4042707 so_state: 0x0";
|
||||
assert!(timestamp_re.captures(continuation_line).is_some(), "sanity: regex alone still matches garbage");
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ async fn main() {
|
||||
};
|
||||
match (active_profile_name, active_profile) {
|
||||
(Some(_), Some(active_profile)) => {
|
||||
profile::load_into_engine(&engine_tx, &active_profile).await;
|
||||
active_profile.load_into_engine(&engine_tx).await;
|
||||
}
|
||||
(Some(profile_name), None) => {
|
||||
eprintln!(
|
||||
|
||||
+50
-40
@@ -153,40 +153,41 @@ impl From<&MetricDeclarationRecord> for DeclareMetricRequest {
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_legacy_metric_params(profile: &mut Profile) -> bool {
|
||||
impl Profile {
|
||||
fn migrate_legacy_metric_params(&mut self) -> bool {
|
||||
let mut migrated = false;
|
||||
for metric_index in 0..profile.metrics.len() {
|
||||
if profile.metrics[metric_index].legacy_params.is_empty() {
|
||||
for metric_index in 0..self.metrics.len() {
|
||||
if self.metrics[metric_index].legacy_params.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let metric_name = profile.metrics[metric_index].name.clone();
|
||||
let message_match_regex = profile.metrics[metric_index].message_match_regex.clone();
|
||||
let legacy = std::mem::take(&mut profile.metrics[metric_index].legacy_params);
|
||||
let metric_name = self.metrics[metric_index].name.clone();
|
||||
let message_match_regex = self.metrics[metric_index].message_match_regex.clone();
|
||||
let legacy = std::mem::take(&mut self.metrics[metric_index].legacy_params);
|
||||
|
||||
let mut label_params = Vec::new();
|
||||
let mut value_param = None;
|
||||
for matcher in &legacy {
|
||||
let param_name =
|
||||
resolve_or_create_legacy_param(profile, &metric_name, &message_match_regex, matcher);
|
||||
self.resolve_or_create_legacy_param(&metric_name, &message_match_regex, matcher);
|
||||
match matcher.role {
|
||||
LegacyParamRole::Label => label_params.push(param_name),
|
||||
LegacyParamRole::Value => value_param = Some(param_name),
|
||||
}
|
||||
}
|
||||
profile.metrics[metric_index].label_params = label_params;
|
||||
profile.metrics[metric_index].value_param = value_param;
|
||||
self.metrics[metric_index].label_params = label_params;
|
||||
self.metrics[metric_index].value_param = value_param;
|
||||
migrated = true;
|
||||
}
|
||||
migrated
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_or_create_legacy_param(
|
||||
profile: &mut Profile,
|
||||
fn resolve_or_create_legacy_param(
|
||||
&mut self,
|
||||
metric_name: &str,
|
||||
message_match_regex: &str,
|
||||
matcher: &LegacyMetricParamRecord,
|
||||
) -> String {
|
||||
if let Some(existing) = profile.params.iter().find(|p| p.name == matcher.param_name) {
|
||||
) -> String {
|
||||
if let Some(existing) = self.params.iter().find(|p| p.name == matcher.param_name) {
|
||||
if existing.message_match_regex == message_match_regex
|
||||
&& existing.capture_group == matcher.capture_group
|
||||
&& existing.json_field_path == matcher.json_field_path
|
||||
@@ -197,8 +198,8 @@ fn resolve_or_create_legacy_param(
|
||||
}
|
||||
|
||||
let implicit_name = format!("{metric_name}.{}", matcher.param_name);
|
||||
if !profile.params.iter().any(|p| p.name == implicit_name) {
|
||||
profile.params.push(ParamDeclarationRecord {
|
||||
if !self.params.iter().any(|p| p.name == implicit_name) {
|
||||
self.params.push(ParamDeclarationRecord {
|
||||
name: implicit_name.clone(),
|
||||
message_match_regex: message_match_regex.to_string(),
|
||||
capture_group: matcher.capture_group.clone(),
|
||||
@@ -209,13 +210,13 @@ fn resolve_or_create_legacy_param(
|
||||
});
|
||||
}
|
||||
implicit_name
|
||||
}
|
||||
}
|
||||
|
||||
fn migrate_duration_value_params(profile: &mut Profile) -> bool {
|
||||
fn migrate_duration_value_params(&mut self) -> bool {
|
||||
let value_param_names: std::collections::HashSet<&str> =
|
||||
profile.metrics.iter().filter_map(|metric| metric.value_param.as_deref()).collect();
|
||||
self.metrics.iter().filter_map(|metric| metric.value_param.as_deref()).collect();
|
||||
let mut migrated = false;
|
||||
for param in &mut profile.params {
|
||||
for param in &mut self.params {
|
||||
if !param.elapsed_since_last
|
||||
&& param.kind == ProfileMetricPrimitiveKind::Duration
|
||||
&& value_param_names.contains(param.name.as_str())
|
||||
@@ -225,6 +226,7 @@ fn migrate_duration_value_params(profile: &mut Profile) -> bool {
|
||||
}
|
||||
}
|
||||
migrated
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -269,7 +271,8 @@ impl Dashboard {
|
||||
}
|
||||
}
|
||||
|
||||
fn seeded_error_split_metric() -> MetricDeclarationRecord {
|
||||
impl MetricDeclarationRecord {
|
||||
fn seeded_error_split() -> MetricDeclarationRecord {
|
||||
MetricDeclarationRecord {
|
||||
name: SEEDED_METRIC_NAME.to_string(),
|
||||
message_match_regex: r"(?i)error".to_string(),
|
||||
@@ -279,9 +282,11 @@ fn seeded_error_split_metric() -> MetricDeclarationRecord {
|
||||
unmatched_labels: Some(BTreeMap::from([(SEEDED_METRIC_LABEL.to_string(), "other".to_string())])),
|
||||
legacy_params: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn seeded_default_dashboard() -> Dashboard {
|
||||
impl Dashboard {
|
||||
fn seeded_default() -> Dashboard {
|
||||
let mut dashboard = Dashboard::new("Overview".to_string());
|
||||
dashboard.panels = vec![
|
||||
Panel {
|
||||
@@ -298,6 +303,7 @@ fn seeded_default_dashboard() -> Dashboard {
|
||||
},
|
||||
];
|
||||
dashboard
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
@@ -322,8 +328,9 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
|
||||
for record in &profile.params {
|
||||
impl Profile {
|
||||
pub async fn load_into_engine(&self, engine: &EngineHandle) {
|
||||
for record in &self.params {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
let request: DeclareParamRequest = record.into();
|
||||
let name = request.name.clone();
|
||||
@@ -333,23 +340,23 @@ pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("warning: engine unavailable while loading profile '{}'", profile.name);
|
||||
eprintln!("warning: engine unavailable while loading profile '{}'", self.name);
|
||||
return;
|
||||
}
|
||||
match response.await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => eprintln!(
|
||||
"warning: profile '{}' param declaration '{name}' rejected by engine: {err}",
|
||||
profile.name
|
||||
self.name
|
||||
),
|
||||
Err(_) => eprintln!(
|
||||
"warning: engine dropped response while declaring param '{name}' from profile '{}'",
|
||||
profile.name
|
||||
self.name
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
for record in &profile.metrics {
|
||||
for record in &self.metrics {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
let request: DeclareMetricRequest = record.into();
|
||||
let name = request.name.clone();
|
||||
@@ -359,21 +366,22 @@ pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
eprintln!("warning: engine unavailable while loading profile '{}'", profile.name);
|
||||
eprintln!("warning: engine unavailable while loading profile '{}'", self.name);
|
||||
return;
|
||||
}
|
||||
match response.await {
|
||||
Ok(Ok(())) => {}
|
||||
Ok(Err(err)) => eprintln!(
|
||||
"warning: profile '{}' metric declaration '{name}' rejected by engine: {err}",
|
||||
profile.name
|
||||
self.name
|
||||
),
|
||||
Err(_) => eprintln!(
|
||||
"warning: engine dropped response while declaring metric '{name}' from profile '{}'",
|
||||
profile.name
|
||||
self.name
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -403,7 +411,8 @@ impl From<io::Error> for ProfileError {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_legacy_profile_store(path: &Path) -> Option<Vec<Profile>> {
|
||||
impl Profile {
|
||||
pub(crate) fn load_legacy_store(path: &Path) -> Option<Vec<Profile>> {
|
||||
let contents = fs::read_to_string(path).ok()?;
|
||||
match serde_json::from_str::<Vec<Profile>>(&contents) {
|
||||
Ok(profiles) => Some(profiles),
|
||||
@@ -415,13 +424,14 @@ pub(crate) fn load_legacy_profile_store(path: &Path) -> Option<Vec<Profile>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn migrate_legacy_profile_shapes(profiles: &mut [Profile]) -> bool {
|
||||
let mut any_migrated = false;
|
||||
for profile in profiles {
|
||||
let legacy_migrated = migrate_legacy_metric_params(profile);
|
||||
let duration_migrated = migrate_duration_value_params(profile);
|
||||
let legacy_migrated = profile.migrate_legacy_metric_params();
|
||||
let duration_migrated = profile.migrate_duration_value_params();
|
||||
if legacy_migrated || duration_migrated {
|
||||
any_migrated = true;
|
||||
}
|
||||
@@ -465,8 +475,8 @@ impl Settings {
|
||||
pub fn ensure_default_profile(&mut self, settings_path: &Path) -> Result<(), ProfileError> {
|
||||
if self.profile(DEFAULT_PROFILE_NAME).is_none() {
|
||||
let mut profile = Profile::new(DEFAULT_PROFILE_NAME.to_string());
|
||||
profile.metrics.push(seeded_error_split_metric());
|
||||
profile.dashboards.push(seeded_default_dashboard());
|
||||
profile.metrics.push(MetricDeclarationRecord::seeded_error_split());
|
||||
profile.dashboards.push(Dashboard::seeded_default());
|
||||
self.profiles.push(profile);
|
||||
}
|
||||
if self.active_profile.is_none() {
|
||||
@@ -798,7 +808,7 @@ mod tests {
|
||||
|
||||
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(settings.clone()));
|
||||
let engine_tx = engine::spawn(settings_handle, 128);
|
||||
load_into_engine(&engine_tx, &default_profile).await;
|
||||
default_profile.load_into_engine(&engine_tx).await;
|
||||
|
||||
for (sequence, text) in [(0u64, "all good"), (1, "an ERROR happened"), (2, "fine again")] {
|
||||
engine_tx
|
||||
@@ -981,7 +991,7 @@ mod tests {
|
||||
|
||||
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(settings.clone()));
|
||||
let engine_tx = crate::engine::spawn(settings_handle, 16);
|
||||
load_into_engine(&engine_tx, &profile).await;
|
||||
profile.load_into_engine(&engine_tx).await;
|
||||
|
||||
engine_tx
|
||||
.tx
|
||||
@@ -1133,7 +1143,7 @@ mod tests {
|
||||
let settings_handle = std::sync::Arc::new(std::sync::Mutex::new(Settings::default()));
|
||||
tokio::runtime::Runtime::new().unwrap().block_on(async {
|
||||
let engine_tx = crate::engine::spawn(settings_handle, 16);
|
||||
load_into_engine(&engine_tx, &profile).await;
|
||||
profile.load_into_engine(&engine_tx).await;
|
||||
engine_tx
|
||||
.tx
|
||||
.send(EngineMessage::Ingest(IngestedLine {
|
||||
@@ -1334,7 +1344,7 @@ mod tests {
|
||||
|
||||
let fresh_settings_handle = std::sync::Arc::new(std::sync::Mutex::new(Settings::default()));
|
||||
let fresh_engine_tx = crate::engine::spawn(fresh_settings_handle, 16);
|
||||
load_into_engine(&fresh_engine_tx, &profile).await;
|
||||
profile.load_into_engine(&fresh_engine_tx).await;
|
||||
|
||||
let EngineResponse::MetricDeclarations(metrics) =
|
||||
ask(&fresh_engine_tx, EngineQuery::MetricDeclarations).await
|
||||
|
||||
+37
-35
@@ -32,7 +32,8 @@ enum RangeSelector {
|
||||
BareWidth(f64),
|
||||
}
|
||||
|
||||
pub fn parse_query(input: &str) -> Result<PromqlQuery, String> {
|
||||
impl PromqlQuery {
|
||||
pub fn parse(input: &str) -> Result<PromqlQuery, String> {
|
||||
let mut parser = Parser::new(input);
|
||||
let query = parser.parse_query()?;
|
||||
parser.skip_whitespace();
|
||||
@@ -48,6 +49,7 @@ pub fn parse_query(input: &str) -> Result<PromqlQuery, String> {
|
||||
);
|
||||
}
|
||||
Ok(query)
|
||||
}
|
||||
}
|
||||
|
||||
struct Parser<'a> {
|
||||
@@ -328,7 +330,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_bare_metric_name() {
|
||||
let query = parse_query("requests").unwrap();
|
||||
let query = PromqlQuery::parse("requests").unwrap();
|
||||
assert_eq!(query.agg, None);
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
assert!(query.matchers.is_empty());
|
||||
@@ -337,7 +339,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_metric_with_single_label_matcher() {
|
||||
let query = parse_query(r#"requests{path="/a"}"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"requests{path="/a"}"#).unwrap();
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
assert_eq!(
|
||||
query.matchers,
|
||||
@@ -347,7 +349,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_metric_with_multiple_label_matchers() {
|
||||
let query = parse_query(r#"requests{path="/a", method="GET"}"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"requests{path="/a", method="GET"}"#).unwrap();
|
||||
assert_eq!(query.matchers.len(), 2);
|
||||
assert_eq!(query.matchers[0].name, "path");
|
||||
assert_eq!(query.matchers[1].name, "method");
|
||||
@@ -355,67 +357,67 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_range_selector() {
|
||||
let query = parse_query("requests[5m]").unwrap();
|
||||
let query = PromqlQuery::parse("requests[5m]").unwrap();
|
||||
assert_eq!(query.range, Some(Duration::from_secs(300)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_label_matchers_and_range_together() {
|
||||
let query = parse_query(r#"requests{path="/a"}[1h]"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"requests{path="/a"}[1h]"#).unwrap();
|
||||
assert_eq!(query.matchers.len(), 1);
|
||||
assert_eq!(query.range, Some(Duration::from_secs(3600)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_aggregation() {
|
||||
let query = parse_query(r#"sum(requests{path="/a"})"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"sum(requests{path="/a"})"#).unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Sum));
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_avg_aggregation() {
|
||||
let query = parse_query("avg(requests)").unwrap();
|
||||
let query = PromqlQuery::parse("avg(requests)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Avg));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_rate_with_range() {
|
||||
let query = parse_query("rate(requests[5m])").unwrap();
|
||||
let query = PromqlQuery::parse("rate(requests[5m])").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Rate));
|
||||
assert_eq!(query.range, Some(Duration::from_secs(300)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_rate_without_range() {
|
||||
let err = parse_query("rate(requests)").unwrap_err();
|
||||
let err = PromqlQuery::parse("rate(requests)").unwrap_err();
|
||||
assert!(err.contains("range selector"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_input() {
|
||||
assert!(parse_query("").is_err());
|
||||
assert!(PromqlQuery::parse("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unclosed_brace() {
|
||||
assert!(parse_query(r#"requests{path="/a""#).is_err());
|
||||
assert!(PromqlQuery::parse(r#"requests{path="/a""#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unclosed_bracket() {
|
||||
assert!(parse_query("requests[5m").is_err());
|
||||
assert!(PromqlQuery::parse("requests[5m").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_aggregation() {
|
||||
let err = parse_query("median(requests)").unwrap_err();
|
||||
let err = PromqlQuery::parse("median(requests)").unwrap_err();
|
||||
assert!(err.contains("unexpected trailing input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_count_aggregation() {
|
||||
let query = parse_query("count(requests)").unwrap();
|
||||
let query = PromqlQuery::parse("count(requests)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Count));
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
assert!(!query.inner_rate);
|
||||
@@ -423,37 +425,37 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_count_with_by_clause() {
|
||||
let query = parse_query("count(requests) by (path)").unwrap();
|
||||
let query = PromqlQuery::parse("count(requests) by (path)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Count));
|
||||
assert_eq!(query.by_labels, vec!["path".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_regex_label_matcher() {
|
||||
let err = parse_query(r#"requests{path=~"/a"}"#).unwrap_err();
|
||||
let err = PromqlQuery::parse(r#"requests{path=~"/a"}"#).unwrap_err();
|
||||
assert!(err.contains("regex label matchers"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_missing_metric_name() {
|
||||
assert!(parse_query(r#"{path="/a"}"#).is_err());
|
||||
assert!(PromqlQuery::parse(r#"{path="/a"}"#).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unknown_duration_unit() {
|
||||
let err = parse_query("requests[5x]").unwrap_err();
|
||||
let err = PromqlQuery::parse("requests[5x]").unwrap_err();
|
||||
assert!(err.contains("duration unit"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_trailing_garbage() {
|
||||
let err = parse_query("requests extra").unwrap_err();
|
||||
let err = PromqlQuery::parse("requests extra").unwrap_err();
|
||||
assert!(err.contains("unexpected trailing input"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_histogram_with_bare_width() {
|
||||
let query = parse_query("histogram(latency_ms[10])").unwrap();
|
||||
let query = PromqlQuery::parse("histogram(latency_ms[10])").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Histogram));
|
||||
assert_eq!(query.metric_name, "latency_ms");
|
||||
assert_eq!(query.bucket_width, Some(10.0));
|
||||
@@ -462,27 +464,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_histogram_without_width() {
|
||||
let err = parse_query("histogram(latency_ms)").unwrap_err();
|
||||
let err = PromqlQuery::parse("histogram(latency_ms)").unwrap_err();
|
||||
assert!(err.contains("bucket width"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_gauge_with_no_range_selector() {
|
||||
let query = parse_query(r#"gauge(connections{host="a"})"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"gauge(connections{host="a"})"#).unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Gauge));
|
||||
assert_eq!(query.matchers, vec![LabelMatcher { name: "host".to_string(), value: "a".to_string() }]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rate_range_selector_is_still_a_duration_not_a_bare_width() {
|
||||
let query = parse_query("rate(requests[5m])").unwrap();
|
||||
let query = PromqlQuery::parse("rate(requests[5m])").unwrap();
|
||||
assert_eq!(query.range, Some(Duration::from_secs(300)));
|
||||
assert_eq!(query.bucket_width, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_of_rate() {
|
||||
let query = parse_query("sum(rate(requests[1m]))").unwrap();
|
||||
let query = PromqlQuery::parse("sum(rate(requests[1m]))").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Sum));
|
||||
assert!(query.inner_rate);
|
||||
assert_eq!(query.metric_name, "requests");
|
||||
@@ -492,7 +494,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_avg_of_rate_with_label_matchers() {
|
||||
let query = parse_query(r#"avg(rate(requests{path="/a"}[5m]))"#).unwrap();
|
||||
let query = PromqlQuery::parse(r#"avg(rate(requests{path="/a"}[5m]))"#).unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Avg));
|
||||
assert!(query.inner_rate);
|
||||
assert_eq!(query.matchers, vec![LabelMatcher { name: "path".to_string(), value: "/a".to_string() }]);
|
||||
@@ -500,13 +502,13 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn rejects_sum_of_rate_without_range() {
|
||||
let err = parse_query("sum(rate(requests))").unwrap_err();
|
||||
let err = PromqlQuery::parse("sum(rate(requests))").unwrap_err();
|
||||
assert!(err.contains("range selector"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_with_by_clause() {
|
||||
let query = parse_query("sum(requests) by (path)").unwrap();
|
||||
let query = PromqlQuery::parse("sum(requests) by (path)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Sum));
|
||||
assert!(!query.inner_rate);
|
||||
assert_eq!(query.by_labels, vec!["path".to_string()]);
|
||||
@@ -514,44 +516,44 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn parses_sum_of_rate_with_by_clause_multiple_labels() {
|
||||
let query = parse_query("sum(rate(requests[1m])) by (path, method)").unwrap();
|
||||
let query = PromqlQuery::parse("sum(rate(requests[1m])) by (path, method)").unwrap();
|
||||
assert!(query.inner_rate);
|
||||
assert_eq!(query.by_labels, vec!["path".to_string(), "method".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sum_without_by_clause_has_empty_by_labels() {
|
||||
let query = parse_query("sum(requests)").unwrap();
|
||||
let query = PromqlQuery::parse("sum(requests)").unwrap();
|
||||
assert!(query.by_labels.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_empty_by_clause() {
|
||||
let err = parse_query("sum(requests) by ()").unwrap_err();
|
||||
let err = PromqlQuery::parse("sum(requests) by ()").unwrap_err();
|
||||
assert!(err.contains("at least one label"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unterminated_by_clause() {
|
||||
assert!(parse_query("sum(requests) by (path").is_err());
|
||||
assert!(PromqlQuery::parse("sum(requests) by (path").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_with_prefix_by_clause() {
|
||||
let query = parse_query("sum by (path) (requests)").unwrap();
|
||||
let query = PromqlQuery::parse("sum by (path) (requests)").unwrap();
|
||||
assert_eq!(query.agg, Some(AggOp::Sum));
|
||||
assert_eq!(query.by_labels, vec!["path".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_sum_with_prefix_by_clause_multiple_labels() {
|
||||
let query = parse_query("sum by (path, method) (requests)").unwrap();
|
||||
let query = PromqlQuery::parse("sum by (path, method) (requests)").unwrap();
|
||||
assert_eq!(query.by_labels, vec!["path".to_string(), "method".to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_by_clause_both_before_and_after_aggregation() {
|
||||
let err = parse_query("sum by (path) (requests) by (method)").unwrap_err();
|
||||
let err = PromqlQuery::parse("sum by (path) (requests) by (method)").unwrap_err();
|
||||
assert!(err.contains("only appear once"));
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -197,7 +197,7 @@ impl Settings {
|
||||
.map(|parent| parent.join("profiles"))
|
||||
.unwrap_or_else(|| PathBuf::from("profiles"))
|
||||
.join("profiles.json");
|
||||
let Some(profiles) = crate::profile::load_legacy_profile_store(&legacy_path) else {
|
||||
let Some(profiles) = crate::profile::Profile::load_legacy_store(&legacy_path) else {
|
||||
return;
|
||||
};
|
||||
self.profiles = profiles;
|
||||
|
||||
Reference in New Issue
Block a user