Trim comments across Rust and webapp source to direct one-liners (ticket 030)
Pass over src/*.rs, webapp/src/**, and the in-app "?" docs prose: condensed multi-paragraph doc comments to single lines that state the non-obvious why, deleted comments that only restated adjacent code, and tightened the Docs.tsx help-panel wording. No logic, structure, or code formatting changed — verified via cargo test (143 passed) and tsc -b && vite build. 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
3cef6dc8ae
commit
00c21c8032
+41
-212
@@ -1,33 +1,5 @@
|
||||
//! Named, persistable bundles of parsing/indexing rules ("profiles" / "presets"),
|
||||
//! ticket 009.
|
||||
//!
|
||||
//! A `Profile` bundles the param declarations (ticket 001) and metric
|
||||
//! declarations (ticket 002) needed to derive a given CLI app's structure from
|
||||
//! its raw log output, plus any saved dashboards for that app. Profiles are
|
||||
//! collected in a `ProfileStore`, persisted as a single `profiles.json` file
|
||||
//! under `Settings.profiles.profiles_dir` (resolved relative to the settings
|
||||
//! file's directory — the same convention ticket 010 uses for CSS storage). A
|
||||
//! single file (rather than one file per profile) is simplest and avoids
|
||||
//! filename-sanitization concerns for profile names.
|
||||
//!
|
||||
//! `Settings.profiles` (see `settings.rs`) only tracks lightweight pointers —
|
||||
//! known profile names and which one is active — while this module owns the
|
||||
//! actual declaration content. That mirrors how `Settings.css_entries` holds
|
||||
//! lightweight `CssEntry` records while the CSS content itself lives in
|
||||
//! separate files (ticket 010).
|
||||
//!
|
||||
//! On startup, `main.rs` loads the active profile's declarations into a fresh
|
||||
//! engine by replaying `EngineMessage::DeclareParam`/`DeclareMetric` for each
|
||||
//! stored declaration (see `load_into_engine` below) — this reproduces the
|
||||
//! same parsing/indexing rules the profile had when last saved, without
|
||||
//! requiring the user to re-declare anything by hand after a restart.
|
||||
//!
|
||||
//! Several public methods here (e.g. `ProfileStore::delete`,
|
||||
//! `record_param_declaration`) have no caller yet within this worktree — CRUD
|
||||
//! over profiles isn't exposed over HTTP until ticket 004 lands the API layer
|
||||
//! and calls into this module. `#![allow(dead_code)]` mirrors how
|
||||
//! `engine.rs`/`data_value.rs` handle the same not-yet-wired situation for
|
||||
//! tickets 001/002.
|
||||
//! Named, persistable bundles of parsing/indexing rules and dashboards for a CLI app, persisted as a single `profiles.json`.
|
||||
//! `Settings.profiles` only tracks lightweight name/active pointers; this module owns the actual content.
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::collections::hash_map::DefaultHasher;
|
||||
@@ -46,22 +18,17 @@ use crate::engine::{DeclareMetricRequest, DeclareParamRequest, EngineHandle, Eng
|
||||
use crate::settings::Settings;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// Name of the profile that always exists and can't be deleted (see
|
||||
/// `ProfileStore::ensure_default`).
|
||||
/// Name of the profile that always exists and can't be deleted (see `ProfileStore::ensure_default`).
|
||||
pub const DEFAULT_PROFILE_NAME: &str = "Default";
|
||||
|
||||
/// Mirrors `data_value::MetricPrimitiveKind` with `Serialize`/`Deserialize` so
|
||||
/// profiles can round-trip through JSON. `MetricPrimitiveKind` itself only
|
||||
/// derives `Serialize` (it's an engine-internal type owned by tickets
|
||||
/// 001/002/003, out of scope for this ticket to modify), so this module keeps
|
||||
/// its own mirror and converts at the boundary.
|
||||
/// Mirrors `data_value::MetricPrimitiveKind` with `Serialize`/`Deserialize` so profiles can round-trip through JSON, since the engine-internal type only derives `Serialize`.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProfileMetricPrimitiveKind {
|
||||
String,
|
||||
Number,
|
||||
Timestamp,
|
||||
/// Mirrors `MetricPrimitiveKind::Duration` (ticket 023).
|
||||
/// Mirrors `MetricPrimitiveKind::Duration`.
|
||||
Duration,
|
||||
}
|
||||
|
||||
@@ -87,7 +54,7 @@ impl From<ProfileMetricPrimitiveKind> for MetricPrimitiveKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// A stored param declaration, matching `DeclareParamRequest`'s shape (ticket 001).
|
||||
/// A stored param declaration, matching `DeclareParamRequest`'s shape.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct ParamDeclarationRecord {
|
||||
pub name: String,
|
||||
@@ -124,26 +91,21 @@ pub struct MetricParamRecord {
|
||||
pub param_name: String,
|
||||
pub capture_group: String,
|
||||
pub kind: ProfileMetricPrimitiveKind,
|
||||
/// See `ParamRole`. `#[serde(default)]` so profiles saved before ticket
|
||||
/// 022 still load, with every param defaulting to `Label`.
|
||||
/// See `ParamRole`; `#[serde(default)]` so profiles saved before this field existed still load, defaulting to `Label`.
|
||||
#[serde(default)]
|
||||
pub role: ParamRole,
|
||||
}
|
||||
|
||||
/// A stored metric declaration, matching `DeclareMetricRequest`'s shape (ticket 002).
|
||||
/// A stored metric declaration, matching `DeclareMetricRequest`'s shape.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct MetricDeclarationRecord {
|
||||
pub name: String,
|
||||
pub message_match_regex: String,
|
||||
pub params: Vec<MetricParamRecord>,
|
||||
/// See `crate::engine::DeclareMetricRequest::matched_labels` (ticket
|
||||
/// 025). `#[serde(default)]` so profiles saved before that ticket still
|
||||
/// load.
|
||||
/// See `crate::engine::DeclareMetricRequest::matched_labels`; `#[serde(default)]` so older profiles still load.
|
||||
#[serde(default)]
|
||||
pub matched_labels: Option<std::collections::BTreeMap<String, String>>,
|
||||
/// See `crate::engine::DeclareMetricRequest::unmatched_labels` (ticket
|
||||
/// 025). `#[serde(default)]` so profiles saved before that ticket still
|
||||
/// load.
|
||||
/// See `crate::engine::DeclareMetricRequest::unmatched_labels`; `#[serde(default)]` so older profiles still load.
|
||||
#[serde(default)]
|
||||
pub unmatched_labels: Option<std::collections::BTreeMap<String, String>>,
|
||||
}
|
||||
@@ -190,8 +152,7 @@ impl From<&MetricDeclarationRecord> for DeclareMetricRequest {
|
||||
}
|
||||
}
|
||||
|
||||
/// A chart type a dashboard panel can render (ticket 014). Kept in sync with
|
||||
/// the frontend's `'bar' | 'line' | 'pie'` union (see `webapp/src/lib/api.ts`).
|
||||
/// A chart type a dashboard panel can render; kept in sync with the frontend's `'bar' | 'line' | 'pie'` union (see `webapp/src/lib/api.ts`).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ChartType {
|
||||
@@ -200,8 +161,7 @@ pub enum ChartType {
|
||||
Pie,
|
||||
}
|
||||
|
||||
/// One panel within a dashboard: a PromQL query (ticket 013) plus how to
|
||||
/// chart its result.
|
||||
/// One panel within a dashboard: a PromQL query plus how to chart its result.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Panel {
|
||||
pub id: String,
|
||||
@@ -210,7 +170,7 @@ pub struct Panel {
|
||||
pub chart_type: ChartType,
|
||||
}
|
||||
|
||||
/// A named set of panels, saved as part of a profile (ticket 014).
|
||||
/// A named set of panels, saved as part of a profile.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Dashboard {
|
||||
pub id: String,
|
||||
@@ -219,10 +179,7 @@ pub struct Dashboard {
|
||||
pub panels: Vec<Panel>,
|
||||
}
|
||||
|
||||
/// Generates an id for a newly-created dashboard/panel. Mirrors
|
||||
/// `settings::generate_imported_css_id`: hashes a discriminating seed
|
||||
/// (dashboard/panel name) together with the current time, avoiding a new
|
||||
/// dependency (e.g. `uuid`) for what's otherwise a random-ish opaque string.
|
||||
/// Generates an id by hashing a seed (dashboard/panel name) with the current time, avoiding a `uuid` dependency for what's otherwise an opaque string.
|
||||
fn generate_id(seed: &str) -> String {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
seed.hash(&mut hasher);
|
||||
@@ -235,28 +192,17 @@ fn generate_id(seed: &str) -> String {
|
||||
}
|
||||
|
||||
impl Dashboard {
|
||||
/// A new dashboard named `name`, with no panels yet.
|
||||
pub fn new(name: String) -> Dashboard {
|
||||
let id = generate_id(&name);
|
||||
Dashboard { id, name, panels: Vec::new() }
|
||||
}
|
||||
}
|
||||
|
||||
/// Ticket 025: name of the metric seeded into a brand-new `Default` profile,
|
||||
/// used to drive its starter dashboard's two panels.
|
||||
/// Name of the metric seeded into a brand-new `Default` profile, used to drive its starter dashboard's two panels.
|
||||
const SEEDED_METRIC_NAME: &str = "log_category";
|
||||
const SEEDED_METRIC_LABEL: &str = "category";
|
||||
|
||||
/// A counter (ticket 022) that classifies every ingested line into exactly
|
||||
/// two buckets — `category="error"` or `category="other"` — without
|
||||
/// capturing any text: `matched_labels`/`unmatched_labels` (ticket 025) apply
|
||||
/// those fixed values directly, rather than a param capturing the literal
|
||||
/// matched substring, so a case-insensitive match like `"ERROR"` vs `"Error"`
|
||||
/// collapses into one clean series instead of fragmenting by source casing.
|
||||
/// A plain regex alternation can't express the "didn't match" half of this
|
||||
/// without lookaround, which the `regex` crate doesn't support (see
|
||||
/// `crate::engine::DeclareMetricRequest::unmatched_labels`) — hence going
|
||||
/// through the metric-declaration mechanism instead of the PromQL surface.
|
||||
/// A counter that splits every line into `category="error"`/`"other"` via `matched_labels`/`unmatched_labels` rather than a captured param, so case variants like "ERROR" collapse into one series; the `regex` crate lacks the lookaround needed to express "didn't match" any other way.
|
||||
fn seeded_error_split_metric() -> MetricDeclarationRecord {
|
||||
MetricDeclarationRecord {
|
||||
name: SEEDED_METRIC_NAME.to_string(),
|
||||
@@ -267,10 +213,7 @@ fn seeded_error_split_metric() -> MetricDeclarationRecord {
|
||||
}
|
||||
}
|
||||
|
||||
/// The starter dashboard seeded into a brand-new `Default` profile (ticket
|
||||
/// 025): a line chart and a pie chart, both querying the bare
|
||||
/// `SEEDED_METRIC_NAME` counter so each renders both the "error" and "other"
|
||||
/// series/slices from a single PromQL query, same as any user-created panel.
|
||||
/// The starter dashboard seeded into a brand-new `Default` profile: a line chart and a pie chart, both querying the seeded error-split counter.
|
||||
fn seeded_default_dashboard() -> Dashboard {
|
||||
let mut dashboard = Dashboard::new("Overview".to_string());
|
||||
dashboard.panels = vec![
|
||||
@@ -290,9 +233,7 @@ fn seeded_default_dashboard() -> Dashboard {
|
||||
dashboard
|
||||
}
|
||||
|
||||
/// A named bundle of parsing/indexing rules for a particular CLI app: which
|
||||
/// params and metrics to derive from its log output, plus any saved
|
||||
/// dashboards.
|
||||
/// A named bundle of parsing/indexing rules for a particular CLI app, plus any saved dashboards.
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Profile {
|
||||
pub name: String,
|
||||
@@ -300,13 +241,11 @@ pub struct Profile {
|
||||
pub params: Vec<ParamDeclarationRecord>,
|
||||
#[serde(default)]
|
||||
pub metrics: Vec<MetricDeclarationRecord>,
|
||||
/// Saved dashboards (ticket 014), each a named set of PromQL-backed panels.
|
||||
#[serde(default)]
|
||||
pub dashboards: Vec<Dashboard>,
|
||||
}
|
||||
|
||||
impl Profile {
|
||||
/// A new, empty profile with the given name.
|
||||
pub fn new(name: String) -> Profile {
|
||||
Profile {
|
||||
name,
|
||||
@@ -317,18 +256,7 @@ impl Profile {
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends every param/metric declaration stored in `profile` to the engine at
|
||||
/// `engine`, as `EngineMessage::DeclareParam`/`DeclareMetric`. Used on startup
|
||||
/// to reload the active profile's rules into a fresh engine (see `main.rs`),
|
||||
/// and in tests to simulate that reload.
|
||||
///
|
||||
/// A stored declaration should generally already be valid — it round-tripped
|
||||
/// through the engine's own validation when originally declared (see
|
||||
/// `record_param_declaration`/`record_metric_declaration` below) — but this
|
||||
/// doesn't assume that: any declaration the engine rejects (or any send/recv
|
||||
/// failure, e.g. the engine channel closing) is logged to stderr and skipped
|
||||
/// rather than panicking, so one bad stored declaration can't take the whole
|
||||
/// process down.
|
||||
/// Replays every param/metric declaration stored in `profile` into `engine` (used on startup and in tests); any rejected declaration or send/recv failure is logged to stderr and skipped rather than panicking.
|
||||
pub async fn load_into_engine(engine: &EngineHandle, profile: &Profile) {
|
||||
for record in &profile.params {
|
||||
let (respond_to, response) = oneshot::channel();
|
||||
@@ -388,8 +316,7 @@ pub enum ProfileError {
|
||||
NotFound,
|
||||
/// A profile with the given name already exists.
|
||||
DuplicateName,
|
||||
/// The `Default` profile can't be deleted — there must always be at
|
||||
/// least one profile to fall back to.
|
||||
/// The `Default` profile can't be deleted — there must always be at least one profile to fall back to.
|
||||
CannotDeleteDefault,
|
||||
/// Underlying I/O failure reading/writing the profile store or settings file.
|
||||
Io(io::Error),
|
||||
@@ -414,9 +341,7 @@ impl From<io::Error> for ProfileError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolves `settings.profiles.profiles_dir` relative to `settings_path`'s
|
||||
/// parent directory (or treats it as absolute if it already is one) — the
|
||||
/// same directory convention ticket 010 uses for CSS storage.
|
||||
/// Resolves `settings.profiles.profiles_dir` relative to `settings_path`'s parent directory (or as absolute if it already is one).
|
||||
pub fn resolve_profiles_dir(settings: &Settings, settings_path: &Path) -> PathBuf {
|
||||
let profiles_dir = Path::new(&settings.profiles.profiles_dir);
|
||||
if profiles_dir.is_absolute() {
|
||||
@@ -429,25 +354,19 @@ pub fn resolve_profiles_dir(settings: &Settings, settings_path: &Path) -> PathBu
|
||||
}
|
||||
}
|
||||
|
||||
/// Path to the single file holding all profiles, within `profiles_dir` (see
|
||||
/// `resolve_profiles_dir`).
|
||||
/// Path to the single file holding all profiles, within `profiles_dir`.
|
||||
pub fn profiles_store_path(profiles_dir: &Path) -> PathBuf {
|
||||
profiles_dir.join("profiles.json")
|
||||
}
|
||||
|
||||
/// On-disk collection of all known profiles, persisted as a single JSON file
|
||||
/// (see module docs for why one file rather than one-per-profile).
|
||||
/// On-disk collection of all known profiles, persisted as a single JSON file.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ProfileStore {
|
||||
profiles: Vec<Profile>,
|
||||
}
|
||||
|
||||
impl ProfileStore {
|
||||
/// Loads the store from `store_path`. A missing file is treated as an
|
||||
/// empty store (not an error) — there's no reason to fail startup just
|
||||
/// because no profiles have been created yet. Any I/O or parse error
|
||||
/// while reading an existing file is logged to stderr and treated as an
|
||||
/// empty store, mirroring `Settings::load`'s non-fatal-error handling.
|
||||
/// Loads the store from `store_path`; a missing or unreadable/unparsable file is treated as an empty store rather than failing startup.
|
||||
pub fn load(store_path: &Path) -> ProfileStore {
|
||||
match fs::read_to_string(store_path) {
|
||||
Ok(contents) => match serde_json::from_str::<Vec<Profile>>(&contents) {
|
||||
@@ -471,8 +390,7 @@ impl ProfileStore {
|
||||
}
|
||||
}
|
||||
|
||||
/// Persists the store to `store_path` as pretty-printed JSON, creating
|
||||
/// any missing parent directories first.
|
||||
/// Persists the store to `store_path` as pretty-printed JSON, creating any missing parent directories first.
|
||||
pub fn save(&self, store_path: &Path) -> io::Result<()> {
|
||||
if let Some(parent) = store_path.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
@@ -482,26 +400,15 @@ impl ProfileStore {
|
||||
fs::write(store_path, contents)
|
||||
}
|
||||
|
||||
/// All known profiles.
|
||||
pub fn list(&self) -> &[Profile] {
|
||||
&self.profiles
|
||||
}
|
||||
|
||||
/// Looks up a profile by name.
|
||||
pub fn get(&self, name: &str) -> Option<&Profile> {
|
||||
self.profiles.iter().find(|p| p.name == name)
|
||||
}
|
||||
|
||||
/// Creates a new profile named `name`, persists the store to
|
||||
/// `store_path`, and records the name in `settings.profiles.names`
|
||||
/// (persisting `settings` to `settings_path` too, keeping the two files in
|
||||
/// sync). Fails with `ProfileError::DuplicateName` if a profile with that
|
||||
/// name already exists.
|
||||
///
|
||||
/// The new profile starts as a copy of the currently active profile's
|
||||
/// params/metrics/dashboards (or empty, if none is active) — a preset is
|
||||
/// most useful as a variation on the one you were just using, not a
|
||||
/// blank slate.
|
||||
/// Creates a new profile named `name` (persisting store and settings), copying the active profile's params/metrics/dashboards as a starting point rather than starting blank. Fails with `DuplicateName` if the name exists.
|
||||
pub fn create(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -528,13 +435,7 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes the profile named `name`, persists the store to `store_path`,
|
||||
/// and drops it from `settings.profiles.names` (persisting `settings` to
|
||||
/// `settings_path` too). If the deleted profile was active, clears
|
||||
/// `settings.profiles.active_profile`. Fails with `ProfileError::NotFound`
|
||||
/// if no such profile exists, or `ProfileError::CannotDeleteDefault` for
|
||||
/// the `Default` profile — there must always be at least one profile to
|
||||
/// fall back to.
|
||||
/// Deletes the profile named `name` (persisting store and settings), clearing `active_profile` if it was active. Fails with `NotFound`, or `CannotDeleteDefault` for the `Default` profile.
|
||||
pub fn delete(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -557,21 +458,7 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Guarantees a `Default` profile exists in this store, that it's tracked
|
||||
/// in `settings.profiles.names`, and that `settings.profiles.active_profile`
|
||||
/// falls back to it when nothing else is active — so the app always has
|
||||
/// at least one usable profile, even on a first run with no settings file
|
||||
/// yet. Persists both the store and `settings` if either changed.
|
||||
///
|
||||
/// A newly-created `Default` profile (not one that already existed) is
|
||||
/// seeded with a starter dashboard (ticket 025): a line chart and a pie
|
||||
/// chart of "error" vs. non-"error" log volume, both querying
|
||||
/// `seeded_error_split_metric`'s counter — editable/deletable afterward
|
||||
/// like any other dashboard/metric, and never re-seeded or duplicated on
|
||||
/// a later call once the profile exists.
|
||||
///
|
||||
/// Called once on startup (see `main.rs`) before anything else touches
|
||||
/// profiles.
|
||||
/// Ensures a `Default` profile exists (seeding a starter dashboard/metric only on first creation), is tracked in `settings.profiles.names`, and falls back to active if nothing else is. Called once on startup before anything else touches profiles.
|
||||
pub fn ensure_default(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -595,14 +482,7 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Sets (or clears, with `name: None`) the active profile: updates
|
||||
/// `settings.profiles.active_profile` and persists `settings` to
|
||||
/// `settings_path`. Fails with `ProfileError::NotFound` if `name` is
|
||||
/// `Some` and no such profile exists in this store.
|
||||
///
|
||||
/// This only updates `Settings`; it does not itself reload the newly
|
||||
/// active profile's declarations into a running engine (that happens on
|
||||
/// the next startup — see `load_into_engine` and `main.rs`).
|
||||
/// Sets (or clears, with `None`) the active profile in `settings`. Fails with `NotFound` if `name` is `Some` and no such profile exists. Doesn't itself reload declarations into a running engine — that happens on next startup via `load_into_engine`.
|
||||
pub fn set_active(
|
||||
&self,
|
||||
settings: &mut Settings,
|
||||
@@ -619,22 +499,7 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records a newly-declared param into `profile_name`'s stored
|
||||
/// declarations (replacing any existing record with the same param name,
|
||||
/// so re-declaring updates it in place), persisting the store to
|
||||
/// `store_path`.
|
||||
///
|
||||
/// **Integration hook for ticket 004 (HTTP API)**: when the HTTP API
|
||||
/// declares a param against the engine (sending
|
||||
/// `EngineMessage::DeclareParam`), it should also call this — with the
|
||||
/// same `DeclareParamRequest` — against the currently-active profile, so
|
||||
/// the declaration survives a restart (see `load_into_engine`). This
|
||||
/// ticket (009) cannot wire that call itself: there's no declare-param
|
||||
/// HTTP route yet for it to hook into. Call shape:
|
||||
///
|
||||
/// ```ignore
|
||||
/// profile_store.record_param_declaration(&store_path, &active_profile_name, &request)?;
|
||||
/// ```
|
||||
/// Records/updates a declared param into `profile_name`'s stored declarations (matched by param name), persisting the store. Intended to be called by the HTTP API alongside declaring against the engine, so declarations survive a restart.
|
||||
pub fn record_param_declaration(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -655,13 +520,7 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Records a newly-declared metric into `profile_name`'s stored
|
||||
/// declarations (replacing any existing record with the same metric name),
|
||||
/// persisting the store to `store_path`.
|
||||
///
|
||||
/// **Integration hook for ticket 004 (HTTP API)**: same as
|
||||
/// `record_param_declaration` above, but for
|
||||
/// `EngineMessage::DeclareMetric`/`DeclareMetricRequest`.
|
||||
/// Records/updates a declared metric into `profile_name`'s stored declarations (matched by metric name), persisting the store; same integration intent as `record_param_declaration`.
|
||||
pub fn record_metric_declaration(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -682,16 +541,13 @@ impl ProfileStore {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Lists the dashboards saved on `profile_name`. Fails with
|
||||
/// `ProfileError::NotFound` if no such profile exists.
|
||||
/// Lists the dashboards saved on `profile_name`; fails with `NotFound` if it doesn't exist.
|
||||
pub fn list_dashboards(&self, profile_name: &str) -> Result<&[Dashboard], ProfileError> {
|
||||
let profile = self.get(profile_name).ok_or(ProfileError::NotFound)?;
|
||||
Ok(&profile.dashboards)
|
||||
}
|
||||
|
||||
/// Creates a new, empty dashboard named `name` on `profile_name`,
|
||||
/// persisting the store to `store_path`. Returns the new dashboard.
|
||||
/// Fails with `ProfileError::NotFound` if no such profile exists.
|
||||
/// Creates a new, empty dashboard named `name` on `profile_name`, persisting the store; fails with `NotFound` if the profile doesn't exist.
|
||||
pub fn create_dashboard(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -709,12 +565,7 @@ impl ProfileStore {
|
||||
Ok(dashboard)
|
||||
}
|
||||
|
||||
/// Replaces the dashboard identified by `dashboard_id` on `profile_name`
|
||||
/// with `dashboard` (e.g. after adding/removing/editing panels),
|
||||
/// persisting the store to `store_path`. `dashboard.id` is forced to
|
||||
/// `dashboard_id` so a caller can't accidentally change a dashboard's
|
||||
/// identity through this call. Fails with `ProfileError::NotFound` if
|
||||
/// either the profile or the dashboard doesn't exist.
|
||||
/// Replaces the dashboard identified by `dashboard_id` with `dashboard`, persisting the store; `dashboard.id` is forced to `dashboard_id` so a caller can't change a dashboard's identity through this call.
|
||||
pub fn save_dashboard(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -738,10 +589,7 @@ impl ProfileStore {
|
||||
Ok(dashboard)
|
||||
}
|
||||
|
||||
/// Deletes the dashboard identified by `dashboard_id` from
|
||||
/// `profile_name`, persisting the store to `store_path`. Fails with
|
||||
/// `ProfileError::NotFound` if either the profile or the dashboard
|
||||
/// doesn't exist.
|
||||
/// Deletes the dashboard identified by `dashboard_id` from `profile_name`, persisting the store; fails with `NotFound` if either doesn't exist.
|
||||
pub fn delete_dashboard(
|
||||
&mut self,
|
||||
store_path: &Path,
|
||||
@@ -838,7 +686,6 @@ mod tests {
|
||||
assert!(store.get("web").is_some());
|
||||
assert_eq!(settings.profiles.names, vec!["web".to_string(), "worker".to_string()]);
|
||||
|
||||
// Duplicate creation is rejected.
|
||||
let dup = store.create(&store_path, &mut settings, &settings_path, "web".to_string());
|
||||
assert!(matches!(dup, Err(ProfileError::DuplicateName)));
|
||||
|
||||
@@ -870,8 +717,7 @@ mod tests {
|
||||
assert!(settings.profiles.names.contains(&DEFAULT_PROFILE_NAME.to_string()));
|
||||
assert_eq!(settings.profiles.active_profile, Some(DEFAULT_PROFILE_NAME.to_string()));
|
||||
|
||||
// Idempotent: doesn't duplicate the profile, and doesn't clobber an
|
||||
// already-active (non-Default) profile.
|
||||
// Idempotent: doesn't duplicate the profile or clobber an already-active (non-Default) profile.
|
||||
store.create(&store_path, &mut settings, &settings_path, "web".to_string()).unwrap();
|
||||
store.set_active(&mut settings, &settings_path, Some("web".to_string())).unwrap();
|
||||
store.ensure_default(&store_path, &mut settings, &settings_path).unwrap();
|
||||
@@ -908,8 +754,7 @@ mod tests {
|
||||
assert!(dashboard.panels.iter().any(|p| p.chart_type == ChartType::Line));
|
||||
assert!(dashboard.panels.iter().any(|p| p.chart_type == ChartType::Pie));
|
||||
|
||||
// Reload (simulating a restart) and ensure_default again: must not
|
||||
// re-seed or duplicate the dashboard/metric.
|
||||
// Reload (simulating a restart) and ensure_default again: must not re-seed or duplicate.
|
||||
let mut store = ProfileStore::load(&store_path);
|
||||
store.ensure_default(&store_path, &mut settings, &settings_path).unwrap();
|
||||
let default_profile = store.get(DEFAULT_PROFILE_NAME).unwrap();
|
||||
@@ -1076,19 +921,7 @@ mod tests {
|
||||
assert_eq!(profile.metrics.len(), 1);
|
||||
}
|
||||
|
||||
/// Acceptance criterion: declaring params/metrics into a profile, "restarting"
|
||||
/// (dropping and reloading the store from disk, rather than a literal process
|
||||
/// restart), and replaying the saved profile against a fresh engine reproduces
|
||||
/// the same declarations and still ingests lines correctly — without
|
||||
/// re-declaring anything by hand.
|
||||
///
|
||||
/// Note: ticket 003 (metric bucket derivation) isn't implemented yet in this
|
||||
/// worktree, so there's no bucket index to compare directly. What this test
|
||||
/// verifies instead is the part ticket 009 owns: that a profile's declarations
|
||||
/// round-trip through disk byte-for-byte and, once replayed via
|
||||
/// `load_into_engine`, produce identical engine-visible declarations (which is
|
||||
/// exactly the state ticket 003's derivation logic will read from) — and that
|
||||
/// the engine keeps ingesting lines normally afterwards.
|
||||
/// Acceptance criterion: declaring params/metrics, "restarting" (reloading the store from disk), and replaying into a fresh engine reproduces the same declarations and still ingests lines, without re-declaring by hand.
|
||||
#[tokio::test]
|
||||
async fn profile_round_trip_reproduces_declarations_in_fresh_engine() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -1096,8 +929,7 @@ mod tests {
|
||||
let settings_path = dir.path().join("settings.json");
|
||||
let mut settings = Settings::default();
|
||||
|
||||
// Declare directly into a profile (as ticket 004's HTTP API will do via the
|
||||
// `record_*_declaration` hooks once it exists).
|
||||
// Declare directly into a profile, as the HTTP API will do via the `record_*_declaration` hooks.
|
||||
let mut store = ProfileStore::load(&store_path);
|
||||
store.create(&store_path, &mut settings, &settings_path, "web".to_string()).unwrap();
|
||||
store
|
||||
@@ -1135,8 +967,7 @@ mod tests {
|
||||
tokio::spawn(crate::engine::run(engine_rx, settings_handle));
|
||||
load_into_engine(&engine_tx, &profile).await;
|
||||
|
||||
// Ingest a line and confirm the engine is functioning with the replayed
|
||||
// declarations in place (no re-declaration needed).
|
||||
// Ingest a line and confirm the engine works with the replayed declarations (no re-declaration needed).
|
||||
engine_tx
|
||||
.send(EngineMessage::Ingest(IngestedLine {
|
||||
sequence: 0,
|
||||
@@ -1185,9 +1016,7 @@ mod tests {
|
||||
assert_eq!(logs[0].text, "level=info request duration=42");
|
||||
}
|
||||
|
||||
/// Acceptance criterion (ticket 014): creating a dashboard (with a panel),
|
||||
/// then reloading the profile store from disk (simulating a restart),
|
||||
/// still has the dashboard and its panel.
|
||||
/// Acceptance criterion: a dashboard (with a panel) survives reloading the profile store from disk.
|
||||
#[test]
|
||||
fn dashboard_create_save_delete_round_trip_and_persists_across_reload() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user