Add Metrics API: declare/list params and metrics, query bucket values (ticket 004)
Exposes param/metric declarations and derived bucket data over HTTP: POST /api/params and POST /api/metrics validate and declare against the engine, persisting into the active profile (if any) so declarations survive a restart; GET /api/metrics lists declarations; GET /api/metrics/:name queries bucket values with label, time-range, sequence, and limit filters. Adds a new EngineQuery::BucketValues variant that layers this filtering on top of ticket 003's bucket_values helper. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
f0348589f5
commit
49e8590cf3
+63
-5
@@ -1,12 +1,13 @@
|
||||
use crate::data_value::{DataValue, MetricPrimitiveKind, RegexToMetricPrimitiveMatcher};
|
||||
use crate::dto::{
|
||||
LogEntryDto, LogsParams, MetricDeclarationDto, ParamDeclarationDto, StatsDto,
|
||||
BucketValueDto, LogEntryDto, LogsParams, MetricDeclarationDto, ParamDeclarationDto, StatsDto,
|
||||
TimelineBucketDto, TimelineParams,
|
||||
};
|
||||
use crate::index_key::IndexKey;
|
||||
use crate::metric_primitive::{MetricPrimitive, Number};
|
||||
use crate::timestamp::Timestamp;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
@@ -16,6 +17,10 @@ pub struct IngestedLine {
|
||||
pub text: String,
|
||||
}
|
||||
|
||||
/// Also the request body for `POST /api/params` (ticket 004): `Deserialize` is
|
||||
/// derived directly on this engine type rather than adding a separate DTO,
|
||||
/// since its shape is already exactly what the API needs.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DeclareParamRequest {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
@@ -28,6 +33,11 @@ pub struct DeclareParamRequest {
|
||||
/// `RegexToMetricPrimitiveMatcher::capture_group` must name a capture group on that
|
||||
/// regex). Referencing an already-declared `ParamDeclaration` (001) by name is not
|
||||
/// supported yet — that's left for a future ticket.
|
||||
///
|
||||
/// Also the request body for `POST /api/metrics` (ticket 004) — see
|
||||
/// `DeclareParamRequest`'s doc comment for why `Deserialize` lives here rather
|
||||
/// than on a separate DTO.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct DeclareMetricRequest {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
@@ -41,6 +51,19 @@ pub enum EngineQuery {
|
||||
Stats,
|
||||
ParamDeclarations,
|
||||
MetricDeclarations,
|
||||
/// Bucket values for one metric (ticket 004's `GET /api/metrics/:name`).
|
||||
/// `labels_filter`, when present, keeps only buckets where every listed
|
||||
/// label matches — compared against each label value's `Display` form
|
||||
/// (see `MetricPrimitive`) rather than a typed value, since the filter
|
||||
/// arrives as plain strings off the query string.
|
||||
BucketValues {
|
||||
metric_name: String,
|
||||
labels_filter: Option<BTreeMap<String, String>>,
|
||||
start_ms: Option<i64>,
|
||||
end_ms: Option<i64>,
|
||||
since_sequence: Option<u64>,
|
||||
limit: Option<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
@@ -75,6 +98,7 @@ pub enum EngineResponse {
|
||||
Stats(StatsDto),
|
||||
ParamDeclarations(Vec<ParamDeclarationDto>),
|
||||
MetricDeclarations(Vec<MetricDeclarationDto>),
|
||||
BucketValues(Vec<BucketValueDto>),
|
||||
}
|
||||
|
||||
pub type EngineHandle = mpsc::Sender<EngineMessage>;
|
||||
@@ -289,10 +313,10 @@ fn apply_all_metrics(
|
||||
}
|
||||
|
||||
/// Bucket values for one metric, in insertion order, optionally filtered to a single
|
||||
/// label combination. Internal helper for now — the metrics-API ticket ([004]) will
|
||||
/// wrap this (or an `EngineQuery` built on it) with time/sequence-range filtering and
|
||||
/// HTTP plumbing.
|
||||
#[allow(dead_code)]
|
||||
/// exact label combination. Used directly by tests (exact-match filtering) and, via
|
||||
/// `EngineQuery::BucketValues` in `handle_query` below, by the metrics API (ticket
|
||||
/// 004) — which layers its own textual label-filter and time/sequence-range
|
||||
/// filtering on top rather than using this function's `labels_filter` parameter.
|
||||
fn bucket_values<'a>(
|
||||
index: &'a BTreeMap<IndexKey, DataValue>,
|
||||
metric_name: &'a str,
|
||||
@@ -428,6 +452,40 @@ fn handle_query(index: &BTreeMap<IndexKey, DataValue>, query: EngineQuery) -> En
|
||||
.collect();
|
||||
EngineResponse::MetricDeclarations(declarations)
|
||||
}
|
||||
EngineQuery::BucketValues {
|
||||
metric_name,
|
||||
labels_filter,
|
||||
start_ms,
|
||||
end_ms,
|
||||
since_sequence,
|
||||
limit,
|
||||
} => {
|
||||
let limit = limit.unwrap_or(500).min(10_000);
|
||||
let values: Vec<BucketValueDto> = bucket_values(index, &metric_name, None)
|
||||
.filter(|(labels, _)| {
|
||||
labels_filter.as_ref().is_none_or(|filter| {
|
||||
filter.iter().all(|(name, expected)| {
|
||||
labels
|
||||
.get(name)
|
||||
.is_some_and(|value| &value.to_string() == expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
.filter(|(_, (sequence, timestamp, _))| {
|
||||
since_sequence.is_none_or(|since| *sequence > since)
|
||||
&& start_ms.is_none_or(|start| timestamp.as_millis() >= start)
|
||||
&& end_ms.is_none_or(|end| timestamp.as_millis() <= end)
|
||||
})
|
||||
.take(limit)
|
||||
.map(|(labels, (sequence, timestamp, value))| BucketValueDto {
|
||||
labels: labels.clone(),
|
||||
sequence: *sequence,
|
||||
timestamp_ms: timestamp.as_millis(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect();
|
||||
EngineResponse::BucketValues(values)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user