Add profile (preset) CRUD and startup replay (ticket 009)

Introduces `src/profile.rs`: a `Profile` bundles param/metric declarations
plus a placeholder dashboards field, and `ProfileStore` persists all
profiles as a single profiles.json file, resolved relative to the settings
file's directory (same convention as ticket 010's CSS storage). Adds CRUD
(create/list/get/delete/set_active) that keeps Settings.profiles.names and
active_profile in sync, plus record_param_declaration/record_metric_declaration
hooks that ticket 004's HTTP API will call to persist newly-declared
params/metrics into the active profile.

Wires main.rs to replay the active profile's declarations into the engine
before spawning the ingest task, so backfill/live-derivation semantics
apply uniformly to every ingested line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 12:57:20 +01:00
co-authored by Claude Sonnet 5
parent f259bdc49b
commit 4c8918ec8c
2 changed files with 795 additions and 0 deletions
+34
View File
@@ -7,6 +7,7 @@ mod filter_parser;
mod index_key;
mod ingest;
mod metric_primitive;
mod profile;
mod settings;
mod timestamp;
@@ -17,6 +18,7 @@ use clap::Parser;
use tokio::net::TcpListener;
use tokio::sync::mpsc;
use profile::ProfileStore;
use settings::Settings;
/// Observability dashboard from terminal output.
@@ -48,6 +50,38 @@ async fn main() {
let (engine_tx, engine_rx) = mpsc::channel(1024);
tokio::spawn(engine::run(engine_rx));
// Ticket 009: reload the active profile's param/metric declarations into the
// engine BEFORE starting ingest below. Ordering matters: declarations must be
// in place before any line is ingested so that backfill/live-derivation
// semantics (ticket 003) apply uniformly to every line, including ones that
// arrive immediately once ingest starts (there's no "declare after the fact
// and go back to reprocess early lines" step, so doing it after ingest starts
// could silently miss lines that raced ahead of the declarations).
{
let (active_profile_name, profiles_dir) = {
let settings_guard = settings.lock().unwrap();
(
settings_guard.profiles.active_profile.clone(),
profile::resolve_profiles_dir(&settings_guard, &settings_path),
)
};
if let Some(profile_name) = active_profile_name {
let store = ProfileStore::load(&profile::profiles_store_path(&profiles_dir));
match store.get(&profile_name) {
Some(active_profile) => {
profile::load_into_engine(&engine_tx, active_profile).await;
}
None => {
eprintln!(
"warning: active profile '{profile_name}' not found under {}; skipping",
profiles_dir.display()
);
}
}
}
}
tokio::spawn(ingest::run(engine_tx.clone()));
let app = api::router(engine_tx, &args.static_dir, settings, settings_path);