Daily & Context-Scoped Log Storage in SQLite #15
Labels
No labels
prio_critical
prio_low
type_bug
type_contact
type_issue
type_lead
type_question
type_story
type_task
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
lhumina_code/hero_proc#15
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Minimal Spec — Daily & Context-Scoped Log Storage in SQLite
Goal
Change log storage from a single SQLite database to a partitioned SQLite layout:
srcCurrent Table
Existing schema remains the logical base:
New Storage Model
Partitioning rules
Logs are stored:
Each database contains only the logs for:
context_nameDirectory Layout
Base path:
Example:
Path rules
contextis normalized to a safe filesystem nameyearis 4 digitsdayis day-of-year, zero-padded to 3 digits (001–366)"default"Example for March 20, 2026:
Database Naming
Use a fixed filename inside each partition directory:
Optional side files may exist:
Fixed filename is preferred over embedding date/context in the filename because the directory already defines identity.
Partition Selection
For each log write:
context_nameepochFor each search:
API Requirements
Compatibility
External API should remain mostly unchanged.
New/required search input
UI and backend must support explicit time range selection:
todaylast 24hlast 7dfrom/toBackend search must no longer assume one single database.
Search semantics
Search API must transparently support:
Result format should remain unchanged as much as possible.
Schema Per Partition
Each daily database uses the same schema, with
context_nameretained for compatibility, even though the DB is already context-scoped.Indexing Requirements
Required indexes
Prefix search on
srcsrccontains dot-separated names, for example:We need efficient prefix search such as:
Requirement
Queries must use a form that can benefit from the
srcindex.Preferred:
instead of relying only on:
Because range queries are more predictable for indexed prefix search.
Backend should convert prefix input:
hero.procinto bounds:
hero.procImplementation detail may vary, but the requirement is:
Write Path
Behavior
On write:
context_name + epochRequirements
Read/Search Path
General
Search may span one or many daily databases.
Requirements
Query strategy
For short ranges, search directly across selected partitions.
For longer ranges:
Connection Management
Goal
Do not keep all SQLite files open forever.
Requirements
Implement a small DB-handle cache:
Expected behavior
Suggested policy
UI Changes
Admin/UI log viewer must include time range selection.
Minimum options
Behavior
Migration / Compatibility
Minimal requirement
No forced migration of old single-file log DB is required initially.
System may support:
Optional later migration can be added separately.
Startup behavior
New installations should use partitioned storage directly.
Non-Functional Requirements
Performance
System must scale to:
srcReliability
Observability
Add internal metrics/logging for:
Test Requirements
1. Integration Tests
Cover:
src2. Performance Tests
Add performance test tooling in admin section of
hero_prog.Requirements
Synthetic generator must support:
srcprefixesMust measure
insert throughput
search latency for:
srcopen/close handle behavior
memory usage during long-range search
3. Large Dataset Test Cases
Minimum scenarios:
Scenario A
Scenario B
Scenario C
Scenario D
Acceptance Criteria
Implementation is accepted when:
srcprefix search is indexed and fastRecommended Defaults
default/logs/<context>/<year>/<day>/logs.sqliteImplementation Spec for Issue #15 — Daily & Context-Scoped Log Storage in SQLite
Objective
Replace the current single-SQLite log storage with a partitioned SQLite layout where each partition is identified by
(context, year, day-of-year). External API — RPC methods,LogEntry,LogFilter,LoggingApi,HeroProcDb— remains structurally stable. The server's background cleanup task is adapted. The UI gains a time-range selector.Requirements
/logs/<context>/<year>/<day>/logs.sqlite(contextnormalised,year4 digits,day3-digit day-of-year 001–366)"default"on disk(context_name, epoch)→ open/create DB lazily → insertepoch,loglevel,error,src; prefix search via range queries (WHERE src >= ? AND src < ?)list_sources,delete_by_src,delete_older_thanadapted to iterate partitionsFiles to Modify/Create
New files:
crates/hero_proc_lib/src/db/logs/partition.rs— Partition key derivation, path resolution, schema initcrates/hero_proc_lib/src/db/logs/pool.rs— LRU connection pool (PartitionPool)crates/hero_proc_lib/src/db/logs/store.rs—PartitionedLogStore: insert, query, count, list_sources, delete, export/importModified files:
crates/hero_proc_lib/src/db/logs/mod.rs— Delegate toPartitionedLogStorecrates/hero_proc_lib/src/db/logs/model.rs— Update schema init (dropcontext_namecol, add indexes)crates/hero_proc_lib/src/db/factory.rs—LoggingApiholdsArc<PartitionedLogStore>;HeroProcDb::newacceptslogs_base_dircrates/hero_proc_server/src/main.rs— UpdateHeroProcDb::newcall; verify cleanup taskcrates/hero_proc_server/src/rpc/log.rs— Verify compile; add optional context param tolist_sourcescrates/hero_proc_ui/templates/index.html— Time-range toolbar in#tab-logscrates/hero_proc_ui/static/js/dashboard.js— Wire time-range selector toloadLogs()Cargo.toml— Addlru = "0.12"Implementation Plan
Step 1: Partition key primitives and per-partition DB init
Files:
partition.rs,model.rsPartitionKey { context_safe, year, day }(Hash, Eq, Clone)partition_key(context, epoch)— useschronoto derive year + day-of-year; normalises context withname_fixpartition_path(base_dir, key)→<base>/<ctx>/<year>/<day_padded>/logs.sqliteopen_partition(path)— WAL mode +init_partition_schemalogstable withoutcontext_namecol; indexes on epoch, loglevel, error, srcDependencies: none
Step 2: LRU connection pool
Files:
pool.rs,Cargo.tomllru = "0.12"dependencyPartitionPool { base_dir, cache: Mutex<LruCache<PartitionKey, PoolEntry>>, max_open, idle_secs }with_conn(key, f): evict idle, lookup or open, callfon&mut Connectionevict_idle(): remove entries past idle timeoutDependencies: Step 1
Step 3:
PartitionedLogStore— write pathFiles:
store.rsinsert(entry)andinsert_batch(entries)grouped by partitioncontext_namecolumn (encoded in path)Dependencies: Step 2
Step 4:
PartitionedLogStore— read pathFiles:
store.rsresolve_partitions(base_dir, context_filter, epoch_from, epoch_to)— walks dir tree, filters by time rangequery(filter)— per-partition filtered SQL + in-memory merge sort + paginationcount(filter),list_sources(context_filter)srcprefix via range query; reconstructcontext_namefromPartitionKeyDependencies: Step 3
Step 5:
PartitionedLogStore— delete + export/importFiles:
store.rsdelete_by_src(pattern)— across all partitionsdelete_older_than(epoch)— removes entire.sqlite/-wal/-shmfiles for expired days; row-level delete for boundary dayexport_logs(filter),import_logs(data)— reuse flate2 compressionDependencies: Step 4
Step 6: Rewrite
LoggingApiinfactory.rsFiles:
factory.rs,mod.rsLoggingApi { store: Arc<PartitionedLogStore> }HeroProcDb::newderiveslogs_base_dir = db_path.parent().join("logs")init_schemacall for logs on main DBwith_defaults()uses$HOME/hero/var/logsTempDirDependencies: Step 5
Step 7: Update server main and RPC handler
Files:
main.rs,rpc/log.rsHeroProcDb::newcall signaturedelete_older_than) compilesDependencies: Step 6
Step 8: Update existing tests
Files:
integration_tests.rs,logs/mod.rstestsmake_db()to supplylogs_base_dirtemp pathopen_log_db(":memory:")withPartitionedLogStore::new(tmp_dir, 8, 60)Dependencies: Step 6
Step 9: New integration & performance tests
Files:
store.rstests ortests/moduletest_partitioning_creates_correct_dirstest_multi_day_query(7 days, epoch range filter)test_src_prefix_search(range query, not LIKE %)test_idle_close(LRU eviction)test_context_isolation#[ignore]perf test: 1M entries, 7-day range < 500 msDependencies: Step 5
Step 10: UI time-range filter
Files:
index.html,dashboard.js<select>+ custom date inputs in#tab-logstoolbarepoch_from/epoch_tointoloadLogs()filterDependencies: Step 7
Acceptance Criteria
cargo test -p hero_proc_libtests passcargo test -p hero_proc_servertests pass<base>/logs/<context>/<year>/<day>/logs.sqlitesrcprefix uses range scan (>=/<) notLIKE %delete_older_thanremoves entire SQLite files for expired daysepoch_from/epoch_toexport_logs/import_logsround-trips correctlylogs.sourcesreturns distinct sources across all partitionsNotes
context_namecolumn removed from per-partition schema (encoded in path); reconstructed fromPartitionKeyon read-back.name_fixnormalisation is lossy — canonical form iscontext_safe.logidis unique only within a partition. Combined(context_name, epoch, logid)is globally unique. No RPC handler does barelogidpoint-lookup — safe.PartitionPoolusesstd::sync::Mutex(library remains async-free). Server calls viaspawn_blockingif needed.chronoalready a workspace dep — useDateTime<Utc>::from_timestamp(epoch, 0).ordinal()for day-of-year.lru = "0.12"must be added to workspace + crate deps.delete_older_than, also remove-waland-shmcompanion files.hero_proc.dbretains all non-log tables; only thelogstable init is removed fromfactory.rs.Test Results
hero_proc_lib
hero_proc_server
Status: ✅ All tests passing
Failure details (if any)
No failures.
Implementation committed:
7a1b59cBrowse:
7a1b59c