From c414081e818196c7514bbf6c94cdb90e794c0310 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:20:40 +0200 Subject: [PATCH 01/14] Rebase sqlx-otel integration onto current main --- Cargo.lock | 140 +++++++++-------------------------- Cargo.toml | 7 +- live-search/Cargo.toml | 4 +- live-search/src/bootstrap.rs | 19 +++-- live-search/src/state.rs | 19 ++++- 5 files changed, 67 insertions(+), 122 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 99fc34d..624f136 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,8 +337,8 @@ dependencies = [ "futures-core", "futures-util", "http", - "opentelemetry 0.32.0", - "opentelemetry-semantic-conventions 0.32.1", + "opentelemetry", + "opentelemetry-semantic-conventions", "pin-project-lite", "tower", "tracing", @@ -1331,7 +1331,7 @@ dependencies = [ "rwf-domain", "serde", "serde_json", - "sqlx 0.9.0", + "sqlx", "testcontainers", "testcontainers-modules", "tokio", @@ -1414,7 +1414,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1690,7 +1690,7 @@ dependencies = [ "criterion", "futures", "jsonwebtoken", - "opentelemetry 0.32.0", + "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "reqwest", @@ -1698,7 +1698,7 @@ dependencies = [ "rwf-domain", "serde", "serde_json", - "sqlx 0.9.0", + "sqlx", "subtle", "thiserror 2.0.18", "tokio", @@ -1947,8 +1947,6 @@ version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ - "allocator-api2", - "equivalent", "foldhash 0.1.5", ] @@ -1969,15 +1967,6 @@ version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -[[package]] -name = "hashlink" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1" -dependencies = [ - "hashbrown 0.15.5", -] - [[package]] name = "hashlink" version = "0.11.1" @@ -3521,14 +3510,14 @@ dependencies = [ "leptos_meta", "leptos_router", "moka", - "opentelemetry 0.32.0", + "opentelemetry", "opentelemetry-otlp", "opentelemetry_sdk", "rwf-config", "rwf-domain", "serde", "serde_json", - "sqlx 0.9.0", + "sqlx", "sqlx-otel", "stylance", "thiserror 2.0.18", @@ -3812,7 +3801,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.61.2", ] [[package]] @@ -3922,20 +3911,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" -[[package]] -name = "opentelemetry" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bcd6ae87133e903af7ef497404dda70c60d0ea14895fc8a5e6722754fc2a0" -dependencies = [ - "futures-core", - "futures-sink", - "js-sys", - "pin-project-lite", - "thiserror 2.0.18", - "tracing", -] - [[package]] name = "opentelemetry" version = "0.32.0" @@ -3959,7 +3934,7 @@ dependencies = [ "async-trait", "bytes", "http", - "opentelemetry 0.32.0", + "opentelemetry", "reqwest", ] @@ -3970,7 +3945,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", - "opentelemetry 0.32.0", + "opentelemetry", "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", @@ -3985,17 +3960,11 @@ version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" dependencies = [ - "opentelemetry 0.32.0", + "opentelemetry", "opentelemetry_sdk", "prost", ] -[[package]] -name = "opentelemetry-semantic-conventions" -version = "0.31.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e62e29dfe041afb8ed2a6c9737ab57db4907285d999ef8ad3a59092a36bdc846" - [[package]] name = "opentelemetry-semantic-conventions" version = "0.32.1" @@ -4011,7 +3980,7 @@ dependencies = [ "futures-channel", "futures-executor", "futures-util", - "opentelemetry 0.32.0", + "opentelemetry", "percent-encoding", "portable-atomic", "rand 0.9.4", @@ -4436,7 +4405,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4862,7 +4831,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4921,7 +4890,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5508,59 +5477,19 @@ dependencies = [ "lock_api", ] -[[package]] -name = "sqlx" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc" -dependencies = [ - "sqlx-core 0.8.6", -] - [[package]] name = "sqlx" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "378620ccc25c62c89d8be1c819e76a88d59bdcc3304733330788948e619bfd71" dependencies = [ - "sqlx-core 0.9.0", + "sqlx-core", "sqlx-macros", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", ] -[[package]] -name = "sqlx-core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6" -dependencies = [ - "base64 0.22.1", - "bytes", - "crc", - "crossbeam-queue", - "either", - "event-listener", - "futures-core", - "futures-intrusive", - "futures-io", - "futures-util", - "hashbrown 0.15.5", - "hashlink 0.10.0", - "indexmap 2.14.0", - "log", - "memchr", - "once_cell", - "percent-encoding", - "serde", - "sha2 0.10.9", - "smallvec", - "thiserror 2.0.18", - "tracing", - "url", -] - [[package]] name = "sqlx-core" version = "0.9.0" @@ -5580,7 +5509,7 @@ dependencies = [ "futures-io", "futures-util", "hashbrown 0.16.1", - "hashlink 0.11.1", + "hashlink", "indexmap 2.14.0", "log", "memchr", @@ -5607,7 +5536,7 @@ checksum = "bd2b84f2bc39a5705ef27ec785a11c934a41bbd4a24941e257927cddc26b60bf" dependencies = [ "proc-macro2", "quote", - "sqlx-core 0.9.0", + "sqlx-core", "sqlx-macros-core", "syn", ] @@ -5628,7 +5557,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", - "sqlx-core 0.9.0", + "sqlx-core", "sqlx-mysql", "sqlx-postgres", "sqlx-sqlite", @@ -5660,7 +5589,7 @@ dependencies = [ "serde", "sha1 0.11.0", "sha2 0.11.0", - "sqlx-core 0.9.0", + "sqlx-core", "thiserror 2.0.18", "tracing", "uuid", @@ -5668,14 +5597,17 @@ dependencies = [ [[package]] name = "sqlx-otel" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03d5eae86367bfc6ed9e8e9c26ef1115788f9856669ea4507afcf034bde4ad4b" +checksum = "a231242dc2b2d3e3c799e62c68fb3a01481e049811c43b788bb1f3155d2031a4" dependencies = [ "futures", - "opentelemetry 0.31.0", - "opentelemetry-semantic-conventions 0.31.0", - "sqlx 0.8.6", + "opentelemetry", + "opentelemetry-semantic-conventions", + "sqlx", + "sqlx-core", + "sqlx-postgres", + "tokio", ] [[package]] @@ -5707,7 +5639,7 @@ dependencies = [ "serde_json", "sha2 0.11.0", "smallvec", - "sqlx-core 0.9.0", + "sqlx-core", "stringprep", "thiserror 2.0.18", "tracing", @@ -5734,7 +5666,7 @@ dependencies = [ "log", "percent-encoding", "serde", - "sqlx-core 0.9.0", + "sqlx-core", "thiserror 2.0.18", "tracing", "url", @@ -5957,7 +5889,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6534,7 +6466,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" dependencies = [ "js-sys", - "opentelemetry 0.32.0", + "opentelemetry", "smallvec", "tracing", "tracing-core", @@ -6550,8 +6482,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a10464b0f9cac62a3e26c08c7c9b93c5ae62f5165792b2e7e90c52940eaabef5" dependencies = [ "http", - "opentelemetry 0.32.0", - "opentelemetry-semantic-conventions 0.32.1", + "opentelemetry", + "opentelemetry-semantic-conventions", "tracing", "tracing-opentelemetry", ] @@ -7239,7 +7171,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 43c7296..74542f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,10 +28,6 @@ leptos_router = "0.8" reqwest = { version = "0.13", default-features = false, features = ["json", "rustls", "stream"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -# sqlx-otel 0.5 is now compatible with sqlx 0.9 + opentelemetry 0.32, but the -# dependency/lockfile upgrade is intentionally kept out of this correctness PR -# so `--locked` builds remain reproducible. Upgrade Cargo.toml and Cargo.lock -# together in the dependency-refresh pass. sqlx = { version = "0.9", default-features = false, features = ["postgres", "runtime-tokio", "tls-rustls", "json", "macros", "migrate", "chrono", "uuid"] } subtle = "2.6" thiserror = "2.0.18" @@ -63,7 +59,8 @@ opentelemetry_sdk = { version = "0.32", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.32", features = ["reqwest-client", "http-proto", "reqwest-rustls"] } tracing-opentelemetry = "0.33" axum-tracing-opentelemetry = "0.38" -sqlx-otel = "0.3" +# 0.5 aligns with this workspace's SQLx 0.9, OpenTelemetry 0.32 and Rust 1.94. +sqlx-otel = { version = "0.5", default-features = false, features = ["postgres", "runtime-tokio"] } console-subscriber = "0.5" criterion = { version = "0.8", features = ["html_reports"] } insta = "1" diff --git a/live-search/Cargo.toml b/live-search/Cargo.toml index 9bb71cd..4730440 100644 --- a/live-search/Cargo.toml +++ b/live-search/Cargo.toml @@ -67,9 +67,6 @@ opentelemetry_sdk = { workspace = true, optional = true } opentelemetry-otlp = { workspace = true, optional = true } tracing-opentelemetry = { workspace = true, optional = true } axum-tracing-opentelemetry = { workspace = true, optional = true } -# Kept optional for a future dependency refresh. sqlx-otel 0.3 is not part of -# the active `otel` feature because it targets SQLx 0.8 while this workspace -# uses SQLx 0.9. Upgrade it and Cargo.lock together before enabling DB spans. sqlx-otel = { workspace = true, optional = true } # Dev tools — gated behind `dev-tools` feature console-subscriber = { workspace = true, optional = true } @@ -93,6 +90,7 @@ otel = [ "dep:opentelemetry-otlp", "dep:tracing-opentelemetry", "dep:axum-tracing-opentelemetry", + "dep:sqlx-otel", ] dev-tools = ["dep:console-subscriber"] diff --git a/live-search/src/bootstrap.rs b/live-search/src/bootstrap.rs index c49b84d..7c63505 100644 --- a/live-search/src/bootstrap.rs +++ b/live-search/src/bootstrap.rs @@ -126,22 +126,29 @@ pub async fn run() -> anyhow::Result { }; tracing::info!("{}", cfg.live_search.connection_budget_summary()); - let pool = db::create_pool(&database_url, &pool_tunables) + let raw_pool = db::create_pool(&database_url, &pool_tunables) .await .context("failed to create database pool")?; // Every service sharing this database resolves the exact same SQLx - // migration history. + // migration history. Keep migrations on the raw SQLx pool because the + // instrumentation wrapper is for application query execution, not SQLx's + // migration/listener-specific APIs. sqlx::migrate!("../migrations") - .run(&pool) + .run(&raw_pool) .await .context("failed to run database migrations")?; + #[cfg(feature = "otel")] + let app_pool = sqlx_otel::PoolBuilder::from(raw_pool.clone()).build(); + #[cfg(not(feature = "otel"))] + let app_pool = raw_pool.clone(); + let cache_handle = CacheHandle::default(); let (tx, _rx) = broadcast::channel::(cfg.live_search.sse_broadcast_buffer); let ctx = Arc::new(state::AppContext::new( - pool.clone(), + app_pool, tx.clone(), cache_handle.clone(), )); @@ -151,7 +158,7 @@ pub async fn run() -> anyhow::Result { let mut tasks = JoinSet::new(); let listener_token = shutdown.child_token(); - let pool_for_listener = pool.clone(); + let pool_for_listener = raw_pool.clone(); let listener_span = tracing::info_span!("pg_listener"); let cache_for_listener = cache_handle.clone(); tasks.spawn( @@ -235,6 +242,6 @@ pub async fn run() -> anyhow::Result { Ok(ServerHandle { shutdown, tasks, - pool, + pool: raw_pool, }) } diff --git a/live-search/src/state.rs b/live-search/src/state.rs index 3d304f8..83e605a 100644 --- a/live-search/src/state.rs +++ b/live-search/src/state.rs @@ -22,12 +22,23 @@ use std::sync::Arc; use std::sync::OnceLock; -use sqlx::PgPool; use tokio::sync::broadcast; use crate::cache::CacheHandle; use crate::events::SseEvent; +/// Pool used by request-facing application queries. +/// +/// The `otel` build wraps the raw SQLx pool with `sqlx-otel` so queries emit +/// OpenTelemetry database spans. Infrastructure that needs SQLx's concrete +/// `PgPool` API (migrations and LISTEN/NOTIFY) keeps using the raw pool. +#[cfg(feature = "otel")] +pub type AppPool = sqlx_otel::Pool; + +/// Pool used by request-facing application queries when telemetry is disabled. +#[cfg(not(feature = "otel"))] +pub type AppPool = sqlx::PgPool; + /// Error returned when [`set`] is called more than once. #[derive(Debug, thiserror::Error)] #[non_exhaustive] @@ -42,8 +53,8 @@ pub enum AppContextInitError { /// Construct once at startup, then share via [`Arc`] (either through the /// global [`set`] / [`get`] API or via `leptos::provide_context`). pub struct AppContext { - /// Database connection pool. - pub pool: PgPool, + /// Database pool used by application queries. + pub pool: AppPool, /// Broadcast sender for SSE events. pub broadcast: broadcast::Sender, /// Search result cache. @@ -54,7 +65,7 @@ impl AppContext { /// Create a new application context from its constituent parts. #[must_use] pub const fn new( - pool: PgPool, + pool: AppPool, broadcast: broadcast::Sender, cache: CacheHandle, ) -> Self { From 3f5f7885314f2fc4ad11ea8402eaa2a51070b3e3 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:26:17 +0200 Subject: [PATCH 02/14] Centralize application pool instrumentation --- live-search/src/state.rs | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/live-search/src/state.rs b/live-search/src/state.rs index 83e605a..5d20950 100644 --- a/live-search/src/state.rs +++ b/live-search/src/state.rs @@ -29,8 +29,8 @@ use crate::events::SseEvent; /// Pool used by request-facing application queries. /// -/// The `otel` build wraps the raw SQLx pool with `sqlx-otel` so queries emit -/// OpenTelemetry database spans. Infrastructure that needs SQLx's concrete +/// The `otel` build wraps the raw `SQLx` pool with `sqlx-otel` so queries emit +/// OpenTelemetry database spans. Infrastructure that needs `SQLx`'s concrete /// `PgPool` API (migrations and LISTEN/NOTIFY) keeps using the raw pool. #[cfg(feature = "otel")] pub type AppPool = sqlx_otel::Pool; @@ -39,6 +39,26 @@ pub type AppPool = sqlx_otel::Pool; #[cfg(not(feature = "otel"))] pub type AppPool = sqlx::PgPool; +/// Convert the raw `SQLx` pool into the request-facing application pool. +/// +/// With `otel` enabled this wraps the same underlying pool handle in +/// `sqlx-otel`; without telemetry it is an identity conversion. Keeping this +/// construction in one place ensures production and integration tests exercise +/// the same instrumentation boundary. +#[cfg(feature = "otel")] +#[must_use] +pub fn app_pool_from_raw(raw_pool: sqlx::PgPool) -> AppPool { + sqlx_otel::PoolBuilder::from(raw_pool).build() +} + +/// Convert the raw `SQLx` pool into the request-facing application pool when +/// telemetry is disabled. +#[cfg(not(feature = "otel"))] +#[must_use] +pub const fn app_pool_from_raw(raw_pool: sqlx::PgPool) -> AppPool { + raw_pool +} + /// Error returned when [`set`] is called more than once. #[derive(Debug, thiserror::Error)] #[non_exhaustive] From 62c8d330ede55da036c20261bc361b0d4a59def5 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:26:42 +0200 Subject: [PATCH 03/14] Keep SQLx OpenTelemetry integration trace-only --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 74542f0..3e3d3a9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -60,7 +60,9 @@ opentelemetry-otlp = { version = "0.32", features = ["reqwest-client", "http-pro tracing-opentelemetry = "0.33" axum-tracing-opentelemetry = "0.38" # 0.5 aligns with this workspace's SQLx 0.9, OpenTelemetry 0.32 and Rust 1.94. -sqlx-otel = { version = "0.5", default-features = false, features = ["postgres", "runtime-tokio"] } +# Only tracing is enabled here; sqlx-otel runtime features are for its optional +# pool metrics task, and live-search does not currently install an OTel MeterProvider. +sqlx-otel = { version = "0.5", default-features = false, features = ["postgres"] } console-subscriber = "0.5" criterion = { version = "0.8", features = ["html_reports"] } insta = "1" From 86a136f6a17bc59c5d2c03a7c2c6fc6fc96fe36b Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:27:15 +0200 Subject: [PATCH 04/14] Keep readiness probes on raw database pool --- live-search/src/bootstrap.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/live-search/src/bootstrap.rs b/live-search/src/bootstrap.rs index 7c63505..4eb2ed2 100644 --- a/live-search/src/bootstrap.rs +++ b/live-search/src/bootstrap.rs @@ -79,16 +79,12 @@ async fn health_handler() -> impl IntoResponse { (StatusCode::OK, "ok") } -/// Dependency readiness: verify application state exists and `PostgreSQL` can -/// answer a trivial query. This is intentionally separate from liveness so an -/// orchestrator can stop routing traffic without restart-looping a live process. -async fn readiness_handler() -> impl IntoResponse { - let Some(ctx) = state::get() else { - return (StatusCode::SERVICE_UNAVAILABLE, "app state unavailable"); - }; - +/// Dependency readiness: verify the raw `PostgreSQL` pool can answer a trivial +/// query. Readiness intentionally bypasses application query instrumentation so +/// orchestrator probes do not generate repetitive database spans. +async fn readiness_handler(pool: sqlx::PgPool) -> impl IntoResponse { match sqlx::query_scalar::<_, i32>("SELECT 1") - .fetch_one(&ctx.pool) + .fetch_one(&pool) .await { Ok(1) => (StatusCode::OK, "ready"), @@ -139,10 +135,7 @@ pub async fn run() -> anyhow::Result { .await .context("failed to run database migrations")?; - #[cfg(feature = "otel")] - let app_pool = sqlx_otel::PoolBuilder::from(raw_pool.clone()).build(); - #[cfg(not(feature = "otel"))] - let app_pool = raw_pool.clone(); + let app_pool = state::app_pool_from_raw(raw_pool.clone()); let cache_handle = CacheHandle::default(); let (tx, _rx) = broadcast::channel::(cfg.live_search.sse_broadcast_buffer); @@ -174,6 +167,7 @@ pub async fn run() -> anyhow::Result { let leptos_routes = generate_route_list(app::App); let broadcast_for_sse = ctx.broadcast.clone(); + let readiness_pool = raw_pool.clone(); let router = Router::new() .nest_service("/pkg", ServeDir::new("./pkg")) .route( @@ -194,7 +188,13 @@ pub async fn run() -> anyhow::Result { } }) .route("/health", get(health_handler)) - .route("/readyz", get(readiness_handler)) + .route( + "/readyz", + get(move || { + let pool = readiness_pool.clone(); + async move { readiness_handler(pool).await } + }), + ) .fallback(fallback_handler); #[cfg(feature = "otel")] From 9a9af106074e194a437b317d0b854d168fbe124b Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:27:42 +0200 Subject: [PATCH 05/14] Add focused OpenTelemetry integration test feature --- e2e-tests/Cargo.toml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/e2e-tests/Cargo.toml b/e2e-tests/Cargo.toml index 86c86d5..54d4dc8 100644 --- a/e2e-tests/Cargo.toml +++ b/e2e-tests/Cargo.toml @@ -29,8 +29,18 @@ nursery = { level = "warn", priority = -1 } [features] default = [] # Browser-based tests (chromiumoxide) — gated because they require a real -# browser installation. Run with `--features browser-tests`. +# browser installation. Run with `--features browser-tests`. browser-tests = [] +# Focused runtime check for live-search's optional SQLx OpenTelemetry path. +# This enables the production `otel` feature and only the test-side tracing +# SDK pieces needed to capture and inspect emitted spans. +otel-tests = [ + "live-search/otel", + "dep:opentelemetry", + "dep:opentelemetry_sdk", + "dep:tracing-opentelemetry", + "dep:tracing-subscriber", +] # The test helpers live in this library crate ([lib] + src/common/) and are # imported by each `tests/*.rs` binary. As a result, what used to be @@ -60,6 +70,10 @@ tower-http = { workspace = true, features = ["fs"] } # tracing for subscriber init in live_search_env tracing = { workspace = true } +tracing-subscriber = { workspace = true, optional = true } +tracing-opentelemetry = { workspace = true, optional = true } +opentelemetry = { workspace = true, optional = true } +opentelemetry_sdk = { workspace = true, optional = true, features = ["testing"] } # Test infrastructure testcontainers = { workspace = true } From ed0bc848995a39a252b06f055f91675279fa1689 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:28:26 +0200 Subject: [PATCH 06/14] Exercise production application pool wrapping in E2E fixture --- e2e-tests/src/common/live_search_env.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/e2e-tests/src/common/live_search_env.rs b/e2e-tests/src/common/live_search_env.rs index ead8895..9848ce4 100644 --- a/e2e-tests/src/common/live_search_env.rs +++ b/e2e-tests/src/common/live_search_env.rs @@ -79,9 +79,12 @@ impl LiveSearchEnv { let tx_for_sse = tx.clone(); let shutdown = CancellationToken::new(); - // Server functions use the same AppContext mechanism as production. + // Server functions use the same AppContext and raw->application pool + // conversion as production. The raw pool remains available below for + // the PgListener, which requires SQLx's concrete PgPool API. + let app_pool = state::app_pool_from_raw(server_pool.clone()); let ctx = Arc::new(live_search::state::AppContext::new( - server_pool.clone(), + app_pool, tx.clone(), cache_handle.clone(), )); From 3cf6b7b6b1383193cb0d06eb25080d23ab685949 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:29:18 +0200 Subject: [PATCH 07/14] Test SQLx OpenTelemetry span parenting --- e2e-tests/tests/otel_db_test.rs | 62 +++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 e2e-tests/tests/otel_db_test.rs diff --git a/e2e-tests/tests/otel_db_test.rs b/e2e-tests/tests/otel_db_test.rs new file mode 100644 index 0000000..09176c2 --- /dev/null +++ b/e2e-tests/tests/otel_db_test.rs @@ -0,0 +1,62 @@ +#![cfg(feature = "otel-tests")] + +use anyhow::{Context, Result}; +use opentelemetry::trace::{SpanKind, TracerProvider as _}; +use opentelemetry_sdk::trace::{InMemorySpanExporterBuilder, SdkTracerProvider}; +use tracing::Instrument as _; +use tracing_subscriber::layer::SubscriberExt as _; + +use e2e_tests::common::TestEnv; + +/// Verify the optional SQLx instrumentation emits a database client span below +/// the active request/tracing span instead of creating an orphan root span. +#[tokio::test] +async fn sqlx_span_is_child_of_tracing_parent() -> Result<()> { + let exporter = InMemorySpanExporterBuilder::new().build(); + let provider = SdkTracerProvider::builder() + .with_simple_exporter(exporter.clone()) + .build(); + opentelemetry::global::set_tracer_provider(provider.clone()); + + let tracer = provider.tracer("live-search-otel-test"); + let subscriber = tracing_subscriber::registry() + .with(tracing_opentelemetry::layer().with_tracer(tracer)); + let dispatch = tracing::Dispatch::new(subscriber); + let _dispatch_guard = tracing::dispatcher::set_default(&dispatch); + + let db = TestEnv::postgres().await?; + let app_pool = live_search::state::app_pool_from_raw(db.pool().clone()); + + let request_span = tracing::info_span!("otel_db_test_request"); + let value = async { + sqlx::query_scalar::<_, i32>("SELECT 1") + .fetch_one(&app_pool) + .await + } + .instrument(request_span.clone()) + .await?; + assert_eq!(value, 1); + + // The tracing parent is exported when its final handle is dropped. + drop(request_span); + provider + .force_flush() + .context("failed to flush test OpenTelemetry spans")?; + + let spans = exporter + .get_finished_spans() + .context("failed to read exported OpenTelemetry spans")?; + let parent = spans + .iter() + .find(|span| span.name.as_ref() == "otel_db_test_request") + .context("request tracing span was not exported")?; + let db_span = spans + .iter() + .find(|span| span.span_kind == SpanKind::Client) + .context("sqlx-otel did not emit a database client span")?; + + assert_eq!(db_span.span_context.trace_id(), parent.span_context.trace_id()); + assert_eq!(db_span.parent_span_id, parent.span_context.span_id()); + + Ok(()) +} From 22cd0db17b8550dabf4d297c588032e4c0d19cc3 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:30:11 +0200 Subject: [PATCH 08/14] Run SQLx OpenTelemetry parenting test in CI --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 180dce0..8c6f743 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -341,6 +341,10 @@ jobs: run: docker version - name: Run E2E suite run: bash scripts/test-e2e.sh + - name: Verify SQLx OpenTelemetry span parenting + run: >- + cargo test --release --locked -p e2e-tests --test otel_db_test + --features otel-tests -- --test-threads=1 --nocapture - name: Show sccache stats if: always() run: ${SCCACHE_PATH} --show-stats From c0c026a74bb3aeda95a81add45262edcd3ef77a4 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:31:06 +0200 Subject: [PATCH 09/14] Document active SQLx OpenTelemetry integration --- README.md | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b00d71d..d73752a 100644 --- a/README.md +++ b/README.md @@ -162,10 +162,19 @@ The optional `otel` features export traces over OTLP/HTTP. If Both Axum routers install OpenTelemetry middleware that extracts incoming W3C `traceparent`/`tracestate`; setting a global propagator alone would not do that. -`sqlx-otel` is currently **not enabled** by the active `otel` feature. The lockfile -still contains 0.3 (SQLx 0.8 generation). Upstream 0.5 supports SQLx 0.9, but the -manifest and lockfile should be upgraded together in a dedicated dependency -refresh before enabling it. +`live-search` enables `sqlx-otel` 0.5 under its `otel` feature. Request-facing +application queries use an instrumented `sqlx_otel::Pool` that wraps a +clone of the same underlying SQLx pool; it does **not** create a second physical +connection pool. Migrations, LISTEN/NOTIFY, readiness probes, and graceful +shutdown retain the raw `PgPool` for SQLx-specific APIs and to avoid noisy probe +spans. + +This integration currently exports database **traces only**. live-search does +not install an OpenTelemetry `MeterProvider`, so `sqlx-otel`'s optional runtime +pool-metrics task is intentionally not enabled. The existing Axum Prometheus +layer is a separate metrics pipeline and does not receive native OTel metrics. +CI includes a focused real-Postgres test that verifies a SQLx `CLIENT` span is +emitted beneath an active tracing/OpenTelemetry parent span. ## i18n and WebSocket example From 96085088c82ac15a10b798d8b861a67fee7ffd4f Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:32:13 +0200 Subject: [PATCH 10/14] Align canonical guide with SQLx tracing --- SKILL.md | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/SKILL.md b/SKILL.md index 9559454..5f92269 100644 --- a/SKILL.md +++ b/SKILL.md @@ -160,7 +160,9 @@ close the database pool and flush telemetry. ### 10. Separate liveness from readiness `live-search` exposes a cheap `/health` liveness endpoint and `/readyz`, which -probes PostgreSQL. Compose uses readiness. +probes PostgreSQL. Compose uses readiness. The readiness query deliberately uses +the raw `PgPool` rather than the application instrumentation wrapper so routine +orchestrator probes do not create database spans. The gateway aggregate `/health` checks both registered service modules and the PostgreSQL dependency required for login/refresh-token operation. @@ -304,13 +306,28 @@ The `otel` features use OTLP/HTTP. `OTEL_EXPORTER_OTLP_ENDPOINT` defaults to incoming W3C `traceparent`/`tracestate`; configuring a global propagator alone does not parent request spans. -`sqlx-otel` is **not currently part of the active `otel` feature**. The lockfile -still contains 0.3, which targets SQLx 0.8. Upstream 0.5 supports this workspace's -SQLx/OpenTelemetry generation, but the dependency and lockfile must be upgraded -together in a dedicated dependency refresh before enabling it. - -Metrics and trace export are separate concerns even if the examples currently -gate some observability setup under the same feature. +`live-search` enables `sqlx-otel` 0.5 under the active `otel` feature. The +request-facing `AppPool` is an instrumented `sqlx_otel::Pool` wrapping +a clone of the same underlying `PgPool`, so enabling tracing does not create a +second physical connection pool or change the configured connection budget. +Migrations, LISTEN/NOTIFY, readiness, and graceful shutdown continue to use the +raw `PgPool` because those paths need concrete SQLx APIs or should avoid noisy +probe spans. + +The SQLx integration is deliberately **traces only** today. No OpenTelemetry +`MeterProvider` is installed, so `sqlx-otel`'s optional runtime pool-metrics +feature is not enabled. `axum-prometheus` is a separate metrics pipeline; it +does not automatically receive native OpenTelemetry metrics. Add a real OTel +meter provider before enabling SQLx OTel metrics. + +CI includes a focused integration test using a real Postgres testcontainer and +an in-memory OTel exporter. It executes a query through the production AppPool +wrapper beneath an active `tracing-opentelemetry` parent and asserts that the +emitted database `CLIENT` span shares the trace and has the request span as its +parent. + +Metrics and trace export remain separate concerns even when they are gated by +nearby observability features. ## WebSocket demo @@ -382,7 +399,8 @@ The maintained GitHub Actions workflow should cover: - dependency audit, - Leptos production builds, - all Dockerfile builds using a Docker daemon, -- browser E2E with an installed Chromium binary. +- browser E2E with an installed Chromium binary, +- a runtime SQLx OTel span-parenting check against real PostgreSQL. `--locked` protects the committed dependency graph. CI-installed helper tools must also pin their top-level versions so a crates.io release cannot silently From 5e20561384965d9d0e0d54049ceaf72d282e2fae Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:34:12 +0200 Subject: [PATCH 11/14] Apply canonical Rust formatting --- e2e-tests/tests/otel_db_test.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/e2e-tests/tests/otel_db_test.rs b/e2e-tests/tests/otel_db_test.rs index 09176c2..08d46f7 100644 --- a/e2e-tests/tests/otel_db_test.rs +++ b/e2e-tests/tests/otel_db_test.rs @@ -19,8 +19,8 @@ async fn sqlx_span_is_child_of_tracing_parent() -> Result<()> { opentelemetry::global::set_tracer_provider(provider.clone()); let tracer = provider.tracer("live-search-otel-test"); - let subscriber = tracing_subscriber::registry() - .with(tracing_opentelemetry::layer().with_tracer(tracer)); + let subscriber = + tracing_subscriber::registry().with(tracing_opentelemetry::layer().with_tracer(tracer)); let dispatch = tracing::Dispatch::new(subscriber); let _dispatch_guard = tracing::dispatcher::set_default(&dispatch); @@ -55,7 +55,10 @@ async fn sqlx_span_is_child_of_tracing_parent() -> Result<()> { .find(|span| span.span_kind == SpanKind::Client) .context("sqlx-otel did not emit a database client span")?; - assert_eq!(db_span.span_context.trace_id(), parent.span_context.trace_id()); + assert_eq!( + db_span.span_context.trace_id(), + parent.span_context.trace_id() + ); assert_eq!(db_span.parent_span_id, parent.span_context.span_id()); Ok(()) From 66deb573e7b3d2c991c09bc7af8887aeacd3c2d7 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:34:23 +0200 Subject: [PATCH 12/14] Temporarily refresh PR 4 lockfile in Actions --- .github/workflows/refresh-pr4-lock.yml | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/refresh-pr4-lock.yml diff --git a/.github/workflows/refresh-pr4-lock.yml b/.github/workflows/refresh-pr4-lock.yml new file mode 100644 index 0000000..2a20be7 --- /dev/null +++ b/.github/workflows/refresh-pr4-lock.yml @@ -0,0 +1,34 @@ +name: Refresh PR 4 lockfile + +on: + push: + branches: [deps/sqlx-otel-0.5] + paths: + - Cargo.toml + - e2e-tests/Cargo.toml + - .github/workflows/refresh-pr4-lock.yml + +permissions: + contents: write + +jobs: + refresh-lockfile: + runs-on: ubuntu-24.04 + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - name: Install pinned Rust toolchain + run: rustup toolchain install 1.94.0 --profile minimal + - name: Resolve current workspace + run: cargo +1.94.0 metadata --format-version 1 >/dev/null + - name: Commit generated lockfile + run: | + if git diff --quiet -- Cargo.lock; then + echo "Cargo.lock already current" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add Cargo.lock + git commit -m "Refresh OpenTelemetry test lockfile" + git push origin HEAD:deps/sqlx-otel-0.5 From 9bec7bdc9ee0fad8a2e924009998c4d9e8fbbe3a Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 28 Aug 2026 15:34:46 +0000 Subject: [PATCH 13/14] Refresh OpenTelemetry test lockfile --- Cargo.lock | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 624f136..3f9046e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1326,6 +1326,8 @@ dependencies = [ "leptos-utils", "leptos_axum", "live-search", + "opentelemetry", + "opentelemetry_sdk", "proptest", "reqwest", "rwf-domain", @@ -1338,6 +1340,8 @@ dependencies = [ "tokio-util", "tower-http 0.7.0", "tracing", + "tracing-opentelemetry", + "tracing-subscriber", "uuid", ] @@ -5607,7 +5611,6 @@ dependencies = [ "sqlx", "sqlx-core", "sqlx-postgres", - "tokio", ] [[package]] From a596be89b122903489a77103215ca9ef7f006b79 Mon Sep 17 00:00:00 2001 From: Code and Solder Date: Fri, 28 Aug 2026 17:35:21 +0200 Subject: [PATCH 14/14] Remove temporary PR 4 lockfile workflow --- .github/workflows/refresh-pr4-lock.yml | 34 -------------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/workflows/refresh-pr4-lock.yml diff --git a/.github/workflows/refresh-pr4-lock.yml b/.github/workflows/refresh-pr4-lock.yml deleted file mode 100644 index 2a20be7..0000000 --- a/.github/workflows/refresh-pr4-lock.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Refresh PR 4 lockfile - -on: - push: - branches: [deps/sqlx-otel-0.5] - paths: - - Cargo.toml - - e2e-tests/Cargo.toml - - .github/workflows/refresh-pr4-lock.yml - -permissions: - contents: write - -jobs: - refresh-lockfile: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - - name: Install pinned Rust toolchain - run: rustup toolchain install 1.94.0 --profile minimal - - name: Resolve current workspace - run: cargo +1.94.0 metadata --format-version 1 >/dev/null - - name: Commit generated lockfile - run: | - if git diff --quiet -- Cargo.lock; then - echo "Cargo.lock already current" - exit 0 - fi - git config user.name "github-actions[bot]" - git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add Cargo.lock - git commit -m "Refresh OpenTelemetry test lockfile" - git push origin HEAD:deps/sqlx-otel-0.5