Skip to content
Tutorials

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%3D5s

A 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 run

The 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);
        }
    }
}