Turning Linux /proc into Domain Queries with TeaQL Rust
Reading system information on Linux is not difficult. Open /proc/meminfo, /proc/[pid]/stat, and /proc/[pid]/task, parse the text, and send the results to a terminal UI.
The harder part is keeping that code maintainable. As process fields grow, filtering becomes more complex, or the same system data needs to serve monitoring agents, alerting jobs, and management APIs, file reads and string parsing scattered through application code quickly become a liability.
Linux System Info using TeaQL demonstrates another approach: model Linux system information as domain objects, let TeaQL generate type-safe Rust query APIs, and use teaql-provider-linux to execute those queries against /proc. The example then builds an interactive process and thread monitor with ratatui.
Model First, Parse Later
The example defines three entities in modeling/MODEL.xml:
SystemInfo: hostname, CPU count, total and available memory, load averages, and uptime;Process: PID, name, state, parent PID, command line, thread count, memory, and CPU ticks;Thread: TID, name, state, owning process, and CPU ticks.
Relationships are part of the model as well: a system has processes, and a process has threads. After TeaQL Code Generator runs, the application receives more than three Rust structs. It also gets field accessors, typed filters, ordering methods, and relationship-aware query APIs.
This changes how application code sees Linux. The upper layers no longer need to know which line in /proc contains a value. They express their needs in terms of SystemInfo, Process, and Thread.
Connecting Domain Queries to Linux
At startup, the application creates the generated runtime context and registers the Linux data service executor:
use linux_system_info_core::runtime::module_with_behaviors_and_checkers;
use teaql_provider_linux::LinuxDataServiceExecutor;
let mut ctx = module_with_behaviors_and_checkers().into_context();
ctx.register_executor(LinuxDataServiceExecutor::new());
Generated queries can now be executed by LinuxDataServiceExecutor. The application depends on TeaQL's query contract, while the provider is responsible for reading /proc, converting fields, and assembling entities.
The benefit is not limited to writing less parsing code. Filtering, ordering, pagination, and relationship navigation all use the same domain API, and the compiler can expose field or method changes early.
Four Leaderboards, Four Query Trees
The terminal dashboard refreshes every two seconds. It displays the leading processes by memory use, uptime, accumulated CPU ticks, and thread count. A recommended query for the 50 processes with the highest resident memory looks like this:
use linux_system_info_core::Q;
let processes = Q::processes()
.with_memory_vms_kb_greater_than(0_i64)
.order_by_memory_rss_kb_desc()
.limit(50)
.comment("Order Linux processes by resident memory")
.purpose("Refresh the memory leaderboard")
.execute_for_list(&ctx)
.await?;
There is no SQL and no /proc path in this query. The generated methods express the filter, RSS ordering, and result limit. Replace the ordering method with order_by_cpu_user_ticks_desc() or order_by_thread_count_desc() to produce two of the other leaderboards.
The longest-running view orders processes by creation time and lets the UI subtract create_time from the current time. One detail matters here: the example's CPU values are accumulated Linux user and system ticks, not the sampled CPU percentages shown by tools such as top. Displaying percentages would require storing two samples and calculating their deltas.
The comment and purpose calls record what the query does and why it exists. For a monitoring application, this is more than a readability convention. Query intent can remain visible when a request enters logging, tracing, or governance infrastructure.
Drilling Down from Processes to Threads
When a user selects a process and presses Enter, the application queries that process by PID and then loads its threads:
let process = Q::processes()
.with_pid_is(pid)
.comment("Read process details by PID")
.purpose("Display Linux process details")
.execute_for_one(&ctx)
.await?;
let threads = Q::threads()
.with_process_pid_is(pid)
.order_by_tid_asc()
.comment("Read the process threads in TID order")
.purpose("Display the Linux process thread list")
.execute_for_list(&ctx)
.await?;
This is where the value of a model-driven API becomes especially clear. PID and the process-to-thread relationship are represented in the generated API. Application code does not need to construct /proc/{pid}/task paths or repeatedly convert raw fields.
ratatui Handles Interaction and Presentation
The terminal application is split into three focused parts:
service.rsqueries TeaQL and refreshes application data;app.rsmanages views, list selection, keyboard input, and refresh timing;ui.rsusesratatuito render the system summary, four process tables, and thread details.
The left and right arrow keys or Tab move between leaderboards. The up and down arrow keys or j and k select a process. Enter opens the detail view, while Esc or q goes back or exits.
The UI does not parse Linux data, and the provider does not know anything about terminal layout. Generated domain objects connect the two. The same query layer could later power an HTTP API, Prometheus exporter, background inspection job, or security audit service.
Running the Example
The application requires Linux because its data comes from /proc:
git clone https://github.com/teaql/teaql-rust-app-examples.git
cd teaql-rust-app-examples/003-linux-sysinfo-using-teaql/rust-app-console
cargo run
The example currently references the TeaQL Rust crates through local path dependencies in Cargo.toml. Before running it, place the TeaQL Rust source at the expected location or update those paths for your environment.
Generated code is already included in the example. If you change MODEL.xml, rerun TeaQL Code Generator instead of manually editing generated files under rust-lib-core.
Where to Take It Next
The most interesting part of this project is not that it reimplements a feature-complete htop. It shows that Linux system data can participate in TeaQL's model-generation-provider architecture.
Natural next steps include:
- calculating CPU percentages from deltas between samples;
- filtering suspicious processes by state, UID, or command line;
- alerting on memory, thread count, or process age thresholds;
- exposing the same queries through a node-monitoring API;
- adding aggregation and historical storage for system metrics.
Database tables are not the only possible source for a domain model. When a provider implements TeaQL's execution contract, an operating-system interface such as /proc can offer the same type-safe, composable, intent-aware query experience. That is the main design lesson from this Linux system monitor.
