@robotops/rosql
v0.3.2
Published
The query language for ROS2 telemetry data
Maintainers
Readme
ROSQL (pronounced "RAW-skul") is Robot Ops Structured Query Language — a SQL-like language and driver purpose-built for ROS2 telemetry data stored via OpenTelemetry. It lets robotics engineers query traces, logs, and metrics using familiar SQL-like syntax with first-class support for ROS2 concepts — nodes, actions, topics, and message causality.
Robot observability is hard. ROS2 systems generate a firehose of traces, logs, and sensor data across dozens of nodes, but general-purpose query languages have no awareness of topics, action graphs, or message causality. ROSQL closes that gap: write queries in the language of your robot, not your database.
Built in Rust and available as a library, CLI, gRPC server, and WASM package, ROSQL is created and used by Robot Ops, Inc. to power the Robot Ops observability platform.
Docs, cookbook, and live demo → rosql.org
ROSQL is a trademark of Robot Ops, Inc.
Architecture
ROS2 System
│
│ OTel attributes (ros.node, ros.action.*, ros.topic, ParentSpanId)
▼
Robot Ops Agent (robotops.com) or OTel Collector (community)
│
│ OTLP gRPC
▼
Datastore (e.g. PostgreSQL + TimescaleDB, ClickHouse, or any SQL-compatible DB)
│
│ OTel standard schema
▼
rosql (parse + compile + execute)
│
▼
Query resultsUsage modes
┌─────────────────────────────────────────────────────┐
│ Mode 1: Parse + Execute (library) │
│ ROSQL text → parser → AST → ROSQLBackend → DB │
├─────────────────────────────────────────────────────┤
│ Mode 2: CLI + gRPC Server │
│ rosql parse / compile / query / serve │
├─────────────────────────────────────────────────────┤
│ Mode 3: WASM (frontend editor) │
│ parse() / validate() / get_completions() │
└─────────────────────────────────────────────────────┘Quick start
What ROSQL looks like
Express in one sentence what would take a page of SQL: find every navigation failure that happened while the battery was critically low.
SELECT trace_id, span_name_col, service_name, duration, status_code, span_attributes
FROM traces
WHERE status = 'ERROR' AND action_name = '/navigate_to_pose'
DURING(
FROM topics WHERE topic_name = '/battery_state'
AND fields['percentage'] < 15
)
SINCE 6 hours ago{
"columns": ["trace_id", "span_name_col", "service_name", "duration", "status_code", "span_attributes"],
"rows": [
["a3f1c9d2e8b04f7a", "navigate_to_pose", "bt_navigator", 1423187000, "ERROR",
{"ros.node": "/bt_navigator", "ros.action.name": "/navigate_to_pose", "ros.action.status": "aborted"}],
["c7d2f1a3b9e54c2b", "navigate_to_pose", "bt_navigator", 873456000, "ERROR",
{"ros.node": "/bt_navigator", "ros.action.name": "/navigate_to_pose", "ros.action.status": "aborted"}]
],
"metadata": { "rows_returned": 2, "elapsed_ms": 14 }
}
durationis stored as nanoseconds (BIGINT). 1423187000 ns ≈ 1.42 s.
Or trace the full message causality chain from a single span — something SQL has no primitive for. MESSAGE JOURNEY walks parent_span_id → span_id recursively and returns all columns from otel_traces:
MESSAGE JOURNEY FOR TRACE 'a3f1c9d2e8b04f7a'{
"columns": ["timestamp", "trace_id", "span_id", "parent_span_id", "span_name_col", "span_kind", "service_name", "duration", "status_code", "span_attributes", "resource_attributes"],
"rows": [
["2025-03-25T14:32:11.012Z", "a3f1c9d2e8b04f7a", "f0e1d2c3b4a59687", "", "goal_received", "INTERNAL", "bt_navigator", 0, "OK", {"ros.node": "/bt_navigator"}, {"host.name": "robot_03"}],
["2025-03-25T14:32:11.013Z", "a3f1c9d2e8b04f7a", "1a2b3c4d5e6f7089", "f0e1d2c3b4a59687", "compute_path", "INTERNAL", "planner_server", 38000000, "OK", {"ros.node": "/planner_server", "ros.topic": "/plan"}, {"host.name": "robot_03"}],
["2025-03-25T14:32:11.051Z", "a3f1c9d2e8b04f7a", "9876543210abcdef", "1a2b3c4d5e6f7089", "follow_path", "INTERNAL", "controller_server", 412000000, "ERROR", {"ros.node": "/controller_server", "ros.topic": "/cmd_vel"}, {"host.name": "robot_03"}],
["2025-03-25T14:32:11.463Z", "a3f1c9d2e8b04f7a", "fedcba9876543210", "9876543210abcdef", "obstacle_detected", "INTERNAL", "local_costmap", 6000000, "OK", {"ros.node": "/local_costmap", "ros.topic": "/costmap_update"}, {"host.name": "robot_03"}]
],
"metadata": { "rows_returned": 4, "elapsed_ms": 3 }
}As a CLI
# Install
cargo install rosql --features server,postgres
# Execute a query against your database
rosql query "FROM traces WHERE status = 'ERROR' SINCE 1 hour ago" \
--backend postgres --url postgresql://user:pass@localhost:5432/telemetry
# Compile to SQL (inspect what ROSQL generates — no DB needed)
rosql compile "FROM traces WHERE duration > 500 ms" --backend postgres
# Trace a message causality chain
rosql query "MESSAGE JOURNEY FOR TRACE 'a3f1c9d2e8b04f7a'" \
--backend postgres --url postgresql://user:pass@localhost:5432/telemetryAs a library
cargo add rosqluse rosql::{parse, drivers::{SqlBackend, ExecOptions}};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let backend = SqlBackend::new("postgresql://user:pass@localhost/telemetry").await?;
let query = parse("
SELECT trace_id, span_name_col, service_name, duration, status_code
FROM traces WHERE status = 'ERROR' AND action_name = '/navigate_to_pose'
DURING(
FROM topics WHERE topic_name = '/battery_state' AND fields['percentage'] < 15
)
SINCE 6 hours ago
")?;
let result = backend.execute(&query, &ExecOptions::default()).await?;
println!("{}", serde_json::to_string_pretty(&result)?);
Ok(())
}Driver support
| Backend | Feature flag | Status |
|---------|-------------|--------|
| PostgreSQL / TimescaleDB | postgres | |
| MySQL / MariaDB |
mysql | |
| DuckDB (embedded) |
duckdb | |
| AWS Athena |
athena | |
| Google BigQuery |
bigquery | |
Feature flags
| Feature | What it enables | Dependencies |
|---------|----------------|--------------|
| (default) | Parser, AST, unit system, SQL compiler, proto types | logos, serde, prost |
| postgres | PostgreSQL / TimescaleDB driver | sqlx, tokio |
| mysql | MySQL / MariaDB driver | sqlx, tokio |
| duckdb | DuckDB embedded driver | duckdb (bundled), tokio |
| server | rosql CLI binary + gRPC server | tonic, tokio, clap |
| wasm | WASM exports for frontend editors | wasm-bindgen |
CLI
# Parse → JSON AST
rosql parse "FROM traces WHERE duration > 500 ms SINCE 1 hour ago"
# Compile → SQL (shows what SQL ROSQL generates)
rosql compile "FROM traces WHERE duration > 500 ms" --backend postgres
# Execute → query results as JSON
rosql query "FROM traces WHERE status = 'ERROR'" \
--backend postgres --url postgresql://user:pass@localhost:5432/db
# Validate syntax
rosql validate "SELECT * FROM logs"
# Autocomplete suggestions at cursor position
rosql completions "FROM " 5
# Start gRPC server on Unix socket
rosql serve --socket /tmp/rosql.sock
# Schema profiles (match your OTel Collector exporter):
# --schema otel-postgres (lowercase columns, default)
# --schema otel-clickhouse (PascalCase columns)Examples
-- Find slow navigation actions
SELECT span_name, duration
FROM traces
WHERE action_name = '/navigate_to_pose' AND duration > 500 ms
SINCE 1 hour ago
ORDER BY duration DESC
LIMIT 20
-- Cross-signal correlation: errors during low battery
FROM traces WHERE status = 'ERROR'
DURING(
FROM topics WHERE topic_name = '/battery_state'
AND fields['percentage'] < 20
)
SINCE yesterday
-- Message causality graph
MESSAGE JOURNEY FOR TRACE 'abc123def456'
-- Robot health assessment
HEALTH() FOR ROBOT 'robot_42' SINCE 30 minutes ago
-- Pipeline syntax
FROM traces
| WHERE duration > 500 ms
| FACET robot_id
| COMPARE TO last weekSee examples/ for a full walkthrough with Docker Compose, PostgreSQL fixture data, and runnable queries.
WASM API
The @robotops/rosql npm package exposes parsing and validation for browser editors:
import init, { parse, validate, get_completions } from '@robotops/rosql';
await init();
const result = parse('FROM traces WHERE duration > 500 ms SINCE 1 hour ago');
console.log(result);
const errors = validate('INSERT INTO logs');
console.log(errors); // { valid: false, errors: [...] }
const completions = get_completions('FROM ', 5);
console.log(completions); // [{ label: 'logs', ... }, { label: 'traces', ... }, ...]Schema
ROSQL expects telemetry data in the OpenTelemetry schema conventions for ROS2. This includes standard OTel tables (otel_traces, otel_logs, otel_metrics) with ROS2-specific span attributes (ros.node, ros.action.*, ros.topic).
Performance
See BENCHMARKS.md for performance data (coming soon — #35).
Pre-built binaries
| Platform | Architecture | Download | |----------|-------------|----------| | Linux | x86_64 | GitHub Releases | | Linux | aarch64 | GitHub Releases |
Other platforms: build from source.
Building from source
git clone https://github.com/RobotOpsInc/rosql
cd rosql
# Library only (default)
cargo build --release
# CLI binary
cargo build --release --features server --bin rosql
# CLI with PostgreSQL query execution
cargo build --release --features server,postgres --bin rosqlLocal development
git clone https://github.com/RobotOpsInc/rosql
cd rosql
just build # build default features
just test # run tests
just build-wasm # build WASM package
just check # full CI: build + test + clippy + fmt + buf-lintPrerequisites: Rust (stable, 1.80+), protoc, buf (optional). See CONTRIBUTING.md for details.
Contributing
ROSQL is in early development (v0.1) and contributions are welcome.
- See CONTRIBUTING.md for development workflow, build variants, and release process
- File bugs and feature requests in the issue tracker
- Questions? Email [email protected]
Robot Ops platform
For fleet-scale telemetry with managed ingestion, storage, and dashboards — including lifecycle anchors, fleet-wide anomaly detection, and ClickHouse performance — see the Robot Ops platform.
License
Apache 2.0 — see LICENSE.
