Production runtime
Maya publishes a post, and you want its database work to appear in the same
request trace as the rest of the service. Enable the optional instrumentation
Cargo feature alongside your backend features:
features = ["sqlite", "instrumentation"]Applications own the tracing subscriber. With the feature enabled, Rent emits
structured spans through tracing:
use tracing_subscriber::EnvFilter;
let filter = EnvFilter::new("my_app=info,rent_driver=debug,rent_migrate=info");
tracing_subscriber::fmt()
.with_env_filter(filter)
.try_init()?;Ordinary builds exclude these detailed Rent spans and turn off statement logging on Rent-created connections. Warnings, errors and the application's own logging remain available. See runtime and observability for Cargo feature unification and caller-created pool behavior.
Configure the pool explicitly for production traffic:
use std::time::Duration;
use rent::driver::{DatabasePool, PoolOptions};
let pool = DatabasePool::connect_with(
dialect,
&database_url,
PoolOptions {
max_connections: 20,
min_connections: 2,
acquire_timeout: Duration::from_secs(3),
idle_timeout: Some(Duration::from_secs(10 * 60)),
max_lifetime: Some(Duration::from_secs(30 * 60)),
test_before_acquire: true,
},
)
.await?;Statement timeouts
Bound slow statements in the database itself so a runaway query is cancelled server side. On PostgreSQL, pass the setting through the connection URL. Every pooled connection starts its session with it:
DATABASE_URL=postgres://app:secret@db:5432/app?options=-c%20statement_timeout%3D5sA statement that exceeds the limit fails with a database error. An explicit transaction needs rollback before
reuse. An idle-in-transaction timeout can terminate the connection entirely. MySQL and MariaDB
read their limits from server or account configuration (max_execution_time on MySQL, max_statement_time on
MariaDB). SQLite has no statement timeout.
For typed PostgreSQL configuration, use DatabasePool::connect_postgres_with(url, pool_options, session_options) and pass the resulting pool to the existing generated Client::new(pool) constructor.
PostgresSessionOptions exposes statement_timeout, lock_timeout and
idle_in_transaction_session_timeout as optional durations. Unset fields retain URL/server settings,
explicit values override them, and zero disables a timeout. The
runtime reference shows the full configuration
and pooler requirements.
Tracing
Parent Rent work with your request span to carry application identifiers without putting sensitive query values into logs:
use rent::tracing::Instrument as _;
let request = rent::tracing::info_span!(
"publish_post",
request_id,
post_id
);
let post = async {
client
.post()
.id_eq(post_id)
.only()
.await
}
.instrument(request)
.await?;Rent records operation names, dialects, statement lengths, bind counts, retry attempts, and transaction boundaries. SQL arguments are deliberately excluded.
Query observation
Use an application-owned subscriber to count query events:
impl<S: Subscriber> Layer<S> for QueryEvents {
fn on_event(&self, event: &Event<'_>, _context: Context<'_, S>) {
if event.metadata().target() == "sqlx::query" {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
}It retains only a count. It never records SQL text, summaries, arguments or credentials. Install it on the application
subscriber before opening connections. Use a global subscriber when you need to
include SQLite worker threads too. WithSubscriber::with_subscriber can scope async work, but alone does not reliably capture
query events emitted on SQLite worker threads.
SQLx query events see work that application read/write hooks miss, including raw queries. They are not a promise of one event per network round trip or every transaction-control command. Do not sum nested Rent spans as statements. When exact command counts matter, verify a selected capture against isolated server statement logs. Avoid enabling a general SQLx formatter in production merely to count queries: its fields can contain SQL literals.
The same counting approach works across the supported databases. The production-access example shows how to avoid N+1 queries when building a response.
Complete example
Download this project, or copy the files below into an empty directory.
Replace RENT_REPOSITORY_URL in Cargo.toml with the URL supplied with your preview access. Use the matching Rent CLI release.
rent generate
cargo runThe project includes its database setup. Each lesson uses a small, self-contained schema. You can run it without applying the earlier lessons.
Cargo.toml
[package]
name = "rent-lesson-19-runtime"
version = "0.1.0"
edition = "2024"
[workspace]
[[bin]]
name = "rent-lesson-19-runtime"
path = "main.rs"
[dependencies]
rent = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6", default-features = false, features = ["sqlite", "migrate", "extensions", "instrumentation"] }
anyhow = "1.0.104"
tokio = { version = "1.53.1", features = ["full"] }
tracing = "0.1.44"
tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] }
rent-schema-language = { git = "RENT_REPOSITORY_URL", tag = "v0.8.6" }
[features]
postgres = ["rent/postgres"]rent.toml
schema = "schema.rsl"
generated = "generated"
migrations = "migrations"
[database]
dialect = "sqlite"
backends = ["sqlite"]schema.rsl
datasource db {
provider = "sqlite"
}
model User {
id BigInt @id
name String
@@map("users")
}main.rs
use std::time::Duration;
use anyhow::{Result, ensure};
use rent::driver::{DatabasePool, PoolOptions};
use rent::sql::Dialect;
use rent::tracing::{Instrument as _, subscriber};
use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
#[path = "observation.rs"]
mod observation;
/// Builds a PostgreSQL pool without changing generated client constructors.
///
/// # Errors
///
/// Returns configuration or connection setup failures.
#[cfg(feature = "postgres")]
pub async fn postgres_pool(database_url: &str) -> Result<DatabasePool> {
use rent::driver::PostgresSessionOptions;
Ok(DatabasePool::connect_postgres_with(
database_url,
PoolOptions::default(),
PostgresSessionOptions {
statement_timeout: Some(Duration::from_secs(5)),
lock_timeout: Some(Duration::from_secs(1)),
idle_in_transaction_session_timeout: Some(Duration::from_secs(30)),
},
)
.await?)
}
async fn exercise() -> Result<()> {
rent_schema_language::parse_schema("schema.rsl", include_str!("schema.rsl"))?;
let events = observation::QueryEvents::default();
let subscriber = tracing_subscriber::registry().with(events.clone()).with(
tracing_subscriber::fmt::layer()
.with_test_writer()
.with_filter(EnvFilter::new("rent=debug")),
);
subscriber::set_global_default(subscriber)?;
let pool = DatabasePool::connect_with(
Dialect::Sqlite,
"sqlite::memory:",
PoolOptions {
max_connections: 4,
acquire_timeout: Duration::from_secs(2),
..PoolOptions::default()
},
)
.await?;
let request = rent::tracing::info_span!("publish_post", request_id = "req_01", post_id = 42);
let result = async { pool.execute("SELECT 1").await }.instrument(request).await?;
ensure!(result.rows_affected == 0);
async {
pool.execute("SELECT 2").await?;
let mut tx = pool.begin().await?;
tx.savepoint("observation_example").await?;
tx.fetch_i64(&rent::sql::ast::RenderedQuery::from_static("SELECT 3")).await?;
tx.rollback_to_savepoint("observation_example").await?;
tx.release_savepoint("observation_example").await?;
tx.rollback().await?;
Ok::<_, anyhow::Error>(())
}
.await?;
ensure!(events.count() >= 2, "raw and transaction queries are visible to SQLx logging");
Ok(())
}
#[tokio::main]
async fn main() -> Result<()> {
exercise().await
}
#[cfg(test)]
mod tests {
#[tokio::test]
async fn example_runs() -> anyhow::Result<()> {
super::exercise().await
}
}observation.rs
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use tracing::{Event, Subscriber};
use tracing_subscriber::{Layer, layer::Context};
/// `SQLx` query completion events, not protocol messages or network round trips.
#[derive(Clone, Default)]
pub struct QueryEvents(Arc<AtomicUsize>);
impl QueryEvents {
/// Number of query events observed by this subscriber.
pub fn count(&self) -> usize {
self.0.load(Ordering::Relaxed)
}
}
impl<S: Subscriber> Layer<S> for QueryEvents {
fn on_event(
&self,
event: &Event<'_>,
_context: Context<'_, S>,
) {
if event.metadata().target() == "sqlx::query" {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
}