monitor.db

MetricDB — SQLite persistence for monitor metrics and events.

Owns the aiosqlite connection, the schema, the WAL-vs-DELETE journal choice (network filesystems can’t WAL), and the flock guard that stops two live collectors writing one database. Extracted from MetricCollector; behavior is identical.

Schema v2 is session-scoped: one physical database file can hold many sessions (one per live run, appended over time), and every row is stamped with the session it belongs to. There is no migration path from the pre-session (v1) schema — opening or reading anything that is not v2 fails loud (UnsupportedDBError); see read_sessions().

exception otto.monitor.db.UnsupportedDBError

Bases: RuntimeError

Raised for any monitor database that is not schema v2.

Legacy pre-session databases are deliberately unsupported (spec 2026-07-12: no migration path); the message must say so.

class otto.monitor.db.SessionRow(id: str, label: str | None, note: str | None, start: str, end: str | None, lab_json: str, meta_json: str, chart_map_json: str, metrics: list[tuple[str, str, str, float, str | None]], events: list[tuple[int, str, str | None, str, str, str, str]], log_events: list[tuple[str, str, str, str]])

Bases: object

One session read back from a v2 archive, with all of its rows.

Row tuples deliberately mirror the SQL column order rather than being modeled — this is the review-path reader, consumed by the producer (Task 3), not a public API surface.

id : str
label : str | None
note : str | None
start : str
end : str | None
lab_json : str
meta_json : str
chart_map_json : str
metrics : list[tuple[str, str, str, float, str | None]]
events : list[tuple[int, str, str | None, str, str, str, str]]
log_events : list[tuple[str, str, str, str]]
class otto.monitor.db.MetricDB(path: str, frame: SessionFrame, lab_json: str, meta_json: str)

Bases: object

Persistent async SQLite store, bound to ONE live session frame.

The frame is a SessionFrame, and every write is stamped with its session id. A single database file accumulates one row in sessions per live run over time — reopen a fresh MetricDB (with a fresh frame) for the next run.

async open() None

Acquire the flock guard, open the connection, create this session’s row.

async close() None

Close the persistent DB connection and release the file lock.

async finalize(end: datetime) None

Stamp this session’s end timestamp. No-op if not open.

async write_chart_map(chart_map_json: str) None

Overwrite this session’s series-label → chart-key map. No-op if not open.

Called by the collector whenever a tick introduces a label the map did not already carry, NOT at finalize: a crashed session (end left NULL) must still carry its grouping, or every one of its series renders as its own ungrouped, unit-less chart on replay (see otto.monitor.export).

async write_point(ts: datetime, host: str, label: str, value: float) None

Insert one metric point. No-op if the connection is not open.

async write_log_event(ts: datetime, host: str, tab: str, fields: dict[str, str]) None

Insert one log-event row (fields JSON-encoded). No-op if not open.

async write_event(event: MonitorEvent) int

Insert event into the DB and return the rowid (0 if no DB configured).

async delete_event(event_id: int) None

Delete the event with the given id. No-op if the connection is not open.

async update_event(event: MonitorEvent) None

Update an existing event’s label, color, dash, and end_ts. No-op if not open.

otto.monitor.db.read_sessions(path: str) list[SessionRow]

Read every session (ordered by start) from a v2 archive. Fail-loud otherwise.

Synchronous sqlite3, opened read-only — review mode (the CLI’s historical viewer) runs before any event loop matters, so there is no need to route this through aiosqlite.