From 2f97bc71dbabd363880ab08d69a6e96a95728624 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 22 Jul 2026 12:40:12 +0200 Subject: [PATCH 01/50] Instrument `create_subscription` --- src/handler.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/handler.rs b/src/handler.rs index a8a86f2..54e0440 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -17,7 +17,7 @@ use axum::{ }; use rovo::rovo; use serde::Deserialize; -use tracing::info; +use tracing::{info, instrument}; /// Maps a [`SubscriptionWithBranch`] to its HAL representation. fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { @@ -56,6 +56,15 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument( + skip_all, + fields( + %payload.source_repo_url, + %payload.source_branch_name, + %payload.target_repo, + %payload.event_type, + ) +)] pub async fn create_subscription( state: State, payload: Json, From 623be6e918e967aad7b415b340075a587c1af4c2 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 27 Jul 2026 09:53:59 +0200 Subject: [PATCH 02/50] Add `valuable` crate --- .cargo/config.toml | 2 ++ Cargo.lock | 15 +++++++++++++++ Cargo.toml | 3 ++- 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..a539230 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tracing_unstable"] diff --git a/Cargo.lock b/Cargo.lock index cb70ecc..33052ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -423,6 +423,7 @@ dependencies = [ "tracing-subscriber", "url", "validator", + "valuable", "wiremock", ] @@ -4665,6 +4666,20 @@ name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +dependencies = [ + "valuable-derive", +] + +[[package]] +name = "valuable-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3a32a9bcc0f6c6ccfd5b27bcf298c58e753bcc9eeff268157a303393183a6d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "vcpkg" diff --git a/Cargo.toml b/Cargo.toml index 4fa3d2c..e71a208 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,10 +38,11 @@ tokio = { version = "1.52.3", features = ["process", "rt-multi-thread", "signal" tokio-util = { version = "0.7.18", features = ["rt"] } tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6", features = ["timeout"] } -tracing = "0.1.44" +tracing = { version = "0.1.44", features = ["valuable"] } tracing-subscriber = "0.3" url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } +valuable = { version = "0.1.1", features = ["derive"] } [dev-dependencies] dudect-bencher = "0.7.0" From 19ecbba915004a06199c619ea7c7c9d3fc37fd7d Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 27 Jul 2026 10:48:26 +0200 Subject: [PATCH 03/50] Instrument `create_subscription` using `valuable` --- src/domain/branch_name.rs | 3 ++- src/domain/event_type.rs | 3 ++- src/domain/repo_url.rs | 3 ++- src/domain/target_repo.rs | 3 ++- src/handler.rs | 12 ++---------- src/model.rs | 6 ++++-- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/src/domain/branch_name.rs b/src/domain/branch_name.rs index 458b883..515ce9d 100644 --- a/src/domain/branch_name.rs +++ b/src/domain/branch_name.rs @@ -4,9 +4,10 @@ use crate::error::ValidationError; use rovo::schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; +use valuable::Valuable; /// The Git branch name. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] #[serde(try_from = "String", into = "String")] pub struct BranchName(String); diff --git a/src/domain/event_type.rs b/src/domain/event_type.rs index 813c43c..f24218c 100644 --- a/src/domain/event_type.rs +++ b/src/domain/event_type.rs @@ -4,9 +4,10 @@ use crate::error::ValidationError; use rovo::schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; +use valuable::Valuable; /// The GitHub's `repository_dispatch` `event_type`. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] #[serde(try_from = "String", into = "String")] pub struct EventType(String); diff --git a/src/domain/repo_url.rs b/src/domain/repo_url.rs index 45a4bfd..8e489e3 100644 --- a/src/domain/repo_url.rs +++ b/src/domain/repo_url.rs @@ -4,9 +4,10 @@ use crate::error::ValidationError; use rovo::schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; +use valuable::Valuable; /// The GitHub repository URL. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] #[serde(try_from = "String", into = "String")] pub struct RepoUrl(String); diff --git a/src/domain/target_repo.rs b/src/domain/target_repo.rs index ce81517..d717356 100644 --- a/src/domain/target_repo.rs +++ b/src/domain/target_repo.rs @@ -3,9 +3,10 @@ use crate::error::ValidationError; use rovo::schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use valuable::Valuable; /// The target GitHub repository in owner/repo format. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] #[serde(try_from = "String", into = "String")] pub struct TargetRepo(String); diff --git a/src/handler.rs b/src/handler.rs index 54e0440..1e9803c 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -17,7 +17,7 @@ use axum::{ }; use rovo::rovo; use serde::Deserialize; -use tracing::{info, instrument}; +use tracing::{field::valuable, info, instrument}; /// Maps a [`SubscriptionWithBranch`] to its HAL representation. fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { @@ -56,15 +56,7 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument( - skip_all, - fields( - %payload.source_repo_url, - %payload.source_branch_name, - %payload.target_repo, - %payload.event_type, - ) -)] +#[instrument(skip_all, fields(payload = valuable(&*payload)))] pub async fn create_subscription( state: State, payload: Json, diff --git a/src/model.rs b/src/model.rs index 1cdebf7..22b0b81 100644 --- a/src/model.rs +++ b/src/model.rs @@ -14,9 +14,10 @@ use crate::domain::{BranchName, CommitHash, EventType, RepoUrl, TargetRepo}; use chrono::{DateTime, Utc}; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sqlx::FromRow; +use valuable::Valuable; /// Represents a row in the `branches` table. #[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema)] @@ -146,7 +147,7 @@ pub struct SubscriptionHal { } /// Holds payload data for the creation of a [`Subscription`]. -#[derive(Debug, Clone, Deserialize, JsonSchema)] +#[derive(Valuable, Debug, Clone, Deserialize, JsonSchema)] pub struct CreateSubscription { /// Full HTTPS URL of the monitored git repository. pub source_repo_url: RepoUrl, @@ -169,6 +170,7 @@ pub struct CreateSubscription { /// /// /// [gh_app_auth]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + #[valuable(skip)] pub gh_app_installation_id: i64, } From ef6e1a7b7df0af1b18b76074f0cb76300e2b9ca1 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 27 Jul 2026 12:07:28 +0200 Subject: [PATCH 04/50] Enable `valuable` feature on `tracing-subscriber` --- Cargo.lock | 25 +++++++++++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 33052ac..14f5f7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4497,6 +4497,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", + "valuable", + "valuable-serde", +] + [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -4509,6 +4521,9 @@ dependencies = [ "thread_local", "tracing-core", "tracing-log", + "tracing-serde", + "valuable", + "valuable-serde", ] [[package]] @@ -4681,6 +4696,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "valuable-serde" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee0548edecd1b907be7e67789923b7d02275b9ba4a33ebc33300e2c947a8cb1" +dependencies = [ + "serde", + "valuable", +] + [[package]] name = "vcpkg" version = "0.2.15" diff --git a/Cargo.toml b/Cargo.toml index e71a208..45db390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,7 +39,7 @@ tokio-util = { version = "0.7.18", features = ["rt"] } tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6", features = ["timeout"] } tracing = { version = "0.1.44", features = ["valuable"] } -tracing-subscriber = "0.3" +tracing-subscriber = { version = "0.3", features = ["valuable"] } url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } valuable = { version = "0.1.1", features = ["derive"] } From 110181ea00525dbf0c22fc0e72b6b86a7c44c125 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 27 Jul 2026 12:39:50 +0200 Subject: [PATCH 05/50] Extend instrumentation to other API endpoints --- src/handler.rs | 7 ++++++- src/model.rs | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 1e9803c..d16551e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -18,6 +18,7 @@ use axum::{ use rovo::rovo; use serde::Deserialize; use tracing::{field::valuable, info, instrument}; +use valuable::Valuable; /// Maps a [`SubscriptionWithBranch`] to its HAL representation. fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { @@ -83,7 +84,7 @@ async fn create_subscription_inner( } /// Query parameters for listing subscriptions. -#[derive(Debug, Deserialize, rovo::schemars::JsonSchema)] +#[derive(Valuable, Debug, Deserialize, rovo::schemars::JsonSchema)] pub struct ListSubscriptionsQuery { /// Maximum number of subscriptions to return. pub limit: Option, @@ -112,6 +113,7 @@ pub struct ListSubscriptionsQuery { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(query = valuable(&*query)))] pub async fn list_subscriptions( state: State, query: Query, @@ -178,6 +180,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(id = valuable(&id)))] pub async fn get_subscription( state: State, Path(id): Path, @@ -220,6 +223,7 @@ async fn get_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(id = valuable(&id), payload = valuable(&*payload)))] pub async fn update_subscription( state: State, Path(id): Path, @@ -265,6 +269,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(id = valuable(&id)))] pub async fn delete_subscription( state: State, Path(id): Path, diff --git a/src/model.rs b/src/model.rs index 22b0b81..d9755f1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -175,7 +175,7 @@ pub struct CreateSubscription { } /// Holds payload data for the update of a [`Subscription`]. -#[derive(Debug, Deserialize, JsonSchema)] +#[derive(Valuable, Debug, Deserialize, JsonSchema)] pub struct UpdateSubscription { /// The repository whose workflow needs to be triggered. pub target_repo: Option, @@ -192,6 +192,7 @@ pub struct UpdateSubscription { /// /// /// [gh_app_auth]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation + #[valuable(skip)] pub gh_app_installation_id: Option, } From 4edc36e09f9fc2ee76b3b37e281be5648391be58 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 27 Jul 2026 14:03:09 +0200 Subject: [PATCH 06/50] Various fixes - don't skip GH app installation ID - don't wrap `id` values in valuable - use `#[valuable(transparent)]` to avoid exporting newtypes as lists of one element --- src/domain/branch_name.rs | 3 ++- src/domain/commit_hash.rs | 6 ++++-- src/domain/event_type.rs | 3 ++- src/domain/repo_url.rs | 3 ++- src/domain/target_repo.rs | 3 ++- src/handler.rs | 8 ++++---- src/model.rs | 2 -- 7 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/domain/branch_name.rs b/src/domain/branch_name.rs index 515ce9d..017454a 100644 --- a/src/domain/branch_name.rs +++ b/src/domain/branch_name.rs @@ -1,7 +1,7 @@ //! Domain type to represent a Git branch name. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; use valuable::Valuable; @@ -9,6 +9,7 @@ use valuable::Valuable; /// The Git branch name. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] #[serde(try_from = "String", into = "String")] +#[valuable(transparent)] pub struct BranchName(String); impl std::fmt::Display for BranchName { diff --git a/src/domain/commit_hash.rs b/src/domain/commit_hash.rs index 34dbb34..03fead0 100644 --- a/src/domain/commit_hash.rs +++ b/src/domain/commit_hash.rs @@ -1,12 +1,14 @@ //! Domain type to represent a git commit hash. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; +use valuable::Valuable; /// A git commit hash. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] #[serde(try_from = "String", into = "String")] +#[valuable(transparent)] pub struct CommitHash(String); impl CommitHash { diff --git a/src/domain/event_type.rs b/src/domain/event_type.rs index f24218c..71136d4 100644 --- a/src/domain/event_type.rs +++ b/src/domain/event_type.rs @@ -1,7 +1,7 @@ //! Domain type to represent a GitHub's `repository_dispatch` `event_type`. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; use valuable::Valuable; @@ -9,6 +9,7 @@ use valuable::Valuable; /// The GitHub's `repository_dispatch` `event_type`. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] #[serde(try_from = "String", into = "String")] +#[valuable(transparent)] pub struct EventType(String); impl EventType { diff --git a/src/domain/repo_url.rs b/src/domain/repo_url.rs index 8e489e3..c90a547 100644 --- a/src/domain/repo_url.rs +++ b/src/domain/repo_url.rs @@ -1,7 +1,7 @@ //! Domain type to represent a GitHub repository URL. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; use valuable::Valuable; @@ -9,6 +9,7 @@ use valuable::Valuable; /// The GitHub repository URL. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] #[serde(try_from = "String", into = "String")] +#[valuable(transparent)] pub struct RepoUrl(String); impl std::fmt::Display for RepoUrl { diff --git a/src/domain/target_repo.rs b/src/domain/target_repo.rs index d717356..6c4bacd 100644 --- a/src/domain/target_repo.rs +++ b/src/domain/target_repo.rs @@ -1,13 +1,14 @@ //! Domain type to represent a target repository hosted on GitHub. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use valuable::Valuable; /// The target GitHub repository in owner/repo format. #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] #[serde(try_from = "String", into = "String")] +#[valuable(transparent)] pub struct TargetRepo(String); impl TargetRepo { diff --git a/src/handler.rs b/src/handler.rs index d16551e..54635da 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -84,7 +84,7 @@ async fn create_subscription_inner( } /// Query parameters for listing subscriptions. -#[derive(Valuable, Debug, Deserialize, rovo::schemars::JsonSchema)] +#[derive(Valuable, Debug, Deserialize, schemars::JsonSchema)] pub struct ListSubscriptionsQuery { /// Maximum number of subscriptions to return. pub limit: Option, @@ -180,7 +180,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = valuable(&id)))] +#[instrument(skip_all, fields(id = %id))] pub async fn get_subscription( state: State, Path(id): Path, @@ -223,7 +223,7 @@ async fn get_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = valuable(&id), payload = valuable(&*payload)))] +#[instrument(skip_all, fields(id = %id, payload = valuable(&*payload)))] pub async fn update_subscription( state: State, Path(id): Path, @@ -269,7 +269,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = valuable(&id)))] +#[instrument(skip_all, fields(id = %id))] pub async fn delete_subscription( state: State, Path(id): Path, diff --git a/src/model.rs b/src/model.rs index d9755f1..e857908 100644 --- a/src/model.rs +++ b/src/model.rs @@ -170,7 +170,6 @@ pub struct CreateSubscription { /// /// /// [gh_app_auth]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation - #[valuable(skip)] pub gh_app_installation_id: i64, } @@ -192,7 +191,6 @@ pub struct UpdateSubscription { /// /// /// [gh_app_auth]: https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation - #[valuable(skip)] pub gh_app_installation_id: Option, } From d30ec33ffe2fdffbc0abb18539584744207e209e Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 28 Jul 2026 12:09:34 +0200 Subject: [PATCH 07/50] Instrument polling engine --- src/polling/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/polling/mod.rs b/src/polling/mod.rs index bbcc32b..34d6749 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -50,6 +50,7 @@ async fn polling_loop(ctx: SharedContext) { /// /// /// [`TriggerEngine`]: crate::trigger::TriggerEngine +#[tracing::instrument(skip_all)] async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { let updated_branches = gather_updated_branches(ctx).await?; if updated_branches.is_empty() { @@ -68,6 +69,7 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { } /// Gathers stored branches that need to be updated. +#[tracing::instrument(skip_all)] async fn gather_updated_branches(ctx: &SharedContext) -> Result, sqlx::Error> { let branches = BranchRepository::get_all(ctx.repository.as_ref()) .await @@ -105,6 +107,7 @@ fn execute_branch_updates<'a>( } /// Processes branch updates within a transaction. +#[tracing::instrument(skip_all)] async fn process_branches( repo: std::sync::Arc, shared_branches: std::sync::Arc>, From d577e024297298679db989a8b74b369c47fc07e5 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 29 Jul 2026 09:15:25 +0200 Subject: [PATCH 08/50] Instrument trigger engine --- src/model.rs | 2 +- src/trigger/mod.rs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/model.rs b/src/model.rs index e857908..87a9568 100644 --- a/src/model.rs +++ b/src/model.rs @@ -195,7 +195,7 @@ pub struct UpdateSubscription { } /// Represents a row in the `trigger_queue` table. -#[derive(Debug, FromRow)] +#[derive(Debug, FromRow, Valuable)] pub struct TriggerQueueItem { /// Unique database primary key. pub id: i64, diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 7d5504c..e2ec6d8 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use reqwest::Client; -use tracing::{info, warn}; +use tracing::{field::valuable, info, warn}; use crate::{ context::SharedContext, @@ -55,6 +55,12 @@ async fn trigger_loop(engine: &TriggerEngine) { } /// Processes a single queued event. +#[tracing::instrument( + skip_all, + fields( + trigger = tracing::field::Empty + ) +)] async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { let Some(trigger) = engine .ctx @@ -65,6 +71,8 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro return Ok(()); }; + tracing::Span::current().record("trigger", valuable(&trigger)); + let dispatch_result = dispatch_events(engine, &trigger).await; match dispatch_result { Ok(_) => { @@ -87,6 +95,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro } /// Schedules the next retry for a trigger in the `trigger_queue`. +#[tracing::instrument(skip_all)] async fn schedule_retry( engine: &TriggerEngine, trigger: TriggerQueueItem, @@ -124,6 +133,7 @@ async fn schedule_retry( } /// Recovers tasks that have been stuck in `PROCESSING` for too long. +#[tracing::instrument(skip_all)] pub async fn recover_stuck_tasks( repo: &crate::repository::SqliteRepository, config: &crate::config::Config, @@ -138,6 +148,7 @@ pub async fn recover_stuck_tasks( /// /// /// [`Subscription`]: crate::model::Subscription +#[tracing::instrument(skip_all)] pub async fn dispatch_events( engine: &TriggerEngine, trigger: &TriggerQueueItem, @@ -173,6 +184,7 @@ pub async fn dispatch_events( /// /// /// [`Subscription`]: crate::model::Subscription +#[tracing::instrument(skip_all)] async fn notify_subscription( engine: &TriggerEngine, iat: String, @@ -187,6 +199,7 @@ async fn notify_subscription( /// /// /// [`Subscription`]: crate::model::Subscription +#[tracing::instrument(skip_all)] async fn send_repository_dispatch( engine: &TriggerEngine, iat: &str, From 3693021b58975e7076b936253b3bb693c9dae7f2 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 29 Jul 2026 11:01:30 +0200 Subject: [PATCH 09/50] Implicitly correlate trigger jobs between polling and trigger engines --- src/model.rs | 4 +++- src/polling/branch.rs | 2 ++ src/polling/mod.rs | 36 ++++++++++++++++++++++++++---------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/model.rs b/src/model.rs index 87a9568..e892760 100644 --- a/src/model.rs +++ b/src/model.rs @@ -20,7 +20,7 @@ use sqlx::FromRow; use valuable::Valuable; /// Represents a row in the `branches` table. -#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema)] +#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Valuable)] pub struct Branch { /// Unique database primary key. pub id: i64, @@ -37,9 +37,11 @@ pub struct Branch { pub last_commit_hash: Option, /// Timestamp when the record was created. + #[valuable(skip)] pub created_at: DateTime, /// Timestamp when the record was updated. + #[valuable(skip)] pub updated_at: DateTime, } diff --git a/src/polling/branch.rs b/src/polling/branch.rs index cd11ff1..6cce2f8 100644 --- a/src/polling/branch.rs +++ b/src/polling/branch.rs @@ -1,8 +1,10 @@ //! Utilities for checking whether a branch has updated. use crate::{domain::CommitHash, error::CommitHashError, model::Branch, polling::git::GitFetcher}; +use valuable::Valuable; /// Enables comparison between a git branch row, and the newly fetched branch. +#[derive(Valuable)] pub(super) struct BranchInfo { /// The branch currently stored in the database. pub branch: Branch, diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 34d6749..b8568c9 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use futures::{StreamExt, future::BoxFuture, stream}; -use tracing::{info, warn}; +use tracing::{field::valuable, info, warn}; use crate::{ context::SharedContext, @@ -115,17 +115,33 @@ async fn process_branches( ) -> Result<(), RepositoryError> { let branches = std::sync::Arc::clone(&shared_branches); for branch_info in branches.iter() { - repo.update_last_commit_hash_in_tx(branch_info.branch.id, &branch_info.latest_hash, tx) - .await?; + process_single_branch(repo.clone(), branch_info, tx).await?; + } + Ok(()) +} - info!( - "New commit detected for branch {}. Hash: {}", - branch_info.branch.name, branch_info.latest_hash - ); +/// Processes a single branch update within a transaction. +#[tracing::instrument( + skip_all, + fields( + branch_info = valuable(branch_info), + ) +)] +async fn process_single_branch( + repo: std::sync::Arc, + branch_info: &branch::BranchInfo, + tx: &mut sqlx::SqliteConnection, +) -> Result<(), RepositoryError> { + repo.update_last_commit_hash_in_tx(branch_info.branch.id, &branch_info.latest_hash, tx) + .await?; - repo.queue_triggers_for_branch(branch_info.branch.id, &branch_info.latest_hash, tx) - .await?; - } + info!( + "New commit detected for branch {}. Hash: {}", + branch_info.branch.name, branch_info.latest_hash + ); + + repo.queue_triggers_for_branch(branch_info.branch.id, &branch_info.latest_hash, tx) + .await?; Ok(()) } From b3d37578ee810d2bf6c6eb556da3bf1d3e641dff Mon Sep 17 00:00:00 2001 From: Nilirad Date: Fri, 31 Jul 2026 11:16:24 +0200 Subject: [PATCH 10/50] Add OpenTelemetry crates --- Cargo.lock | 32 ++++++++++++++++++++++++++++++++ Cargo.toml | 2 ++ 2 files changed, 34 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 14f5f7e..7bdb696 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -405,6 +405,7 @@ dependencies = [ "http", "humantime-serde", "jsonwebtoken", + "opentelemetry", "rand 0.10.2", "reqwest", "rovo", @@ -420,6 +421,7 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-opentelemetry", "tracing-subscriber", "url", "validator", @@ -2851,6 +2853,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "opentelemetry" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0142c63252a9e054e68a4c61a5778f7b14f576274d593f8ce883d191a099682" +dependencies = [ + "futures-core", + "futures-sink", + "js-sys", + "pin-project-lite", + "thiserror", + "tracing", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -4497,6 +4513,22 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.33.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adbc64cba7137545b8044cb1fe9814f7aacf3c6b5f9b45be8bb5db538befdb26" +dependencies = [ + "js-sys", + "opentelemetry", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.2.0" diff --git a/Cargo.toml b/Cargo.toml index 45db390..74f45c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ gix = { version = "0.85.0", features = ["blocking-network-client", "blocking-htt http = "1.4.2" humantime-serde = "1.1.1" jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] } +opentelemetry = "0.32.0" reqwest = { version = "0.13.4", features = ["json"] } rovo = { version = "0.4.8", features = ["scalar"] } schemars = { version = "0.9", features = ["chrono04"] } @@ -39,6 +40,7 @@ tokio-util = { version = "0.7.18", features = ["rt"] } tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6", features = ["timeout"] } tracing = { version = "0.1.44", features = ["valuable"] } +tracing-opentelemetry = "0.33.0" tracing-subscriber = { version = "0.3", features = ["valuable"] } url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } From f170dcdbc171d7b24dfc24be05c2fa89df92e38a Mon Sep 17 00:00:00 2001 From: Nilirad Date: Fri, 31 Jul 2026 17:03:32 +0200 Subject: [PATCH 11/50] Add OpenTelemetry span links between polling and trigger engines --- ...13ac49077fb81e012c1d3172bcb24941835bb.json | 12 ----- ...aec9bbefc34795e8a828b6ee87f5c2701ca5e.json | 12 +++++ ...4821_add_span_context_to_trigger_queue.sql | 1 + src/lib.rs | 1 + src/model.rs | 3 ++ src/polling/mod.rs | 10 +++- src/repository/sqlite.rs | 15 +++--- src/repository/trigger.rs | 16 +++++- src/telemetry.rs | 52 +++++++++++++++++++ src/trigger/mod.rs | 6 +++ 10 files changed, 106 insertions(+), 22 deletions(-) delete mode 100644 .sqlx/query-30b46e820bb013286bf5a18db3513ac49077fb81e012c1d3172bcb24941835bb.json create mode 100644 .sqlx/query-d87a0e607c8036a121f10b31bdcaec9bbefc34795e8a828b6ee87f5c2701ca5e.json create mode 100644 migrations/20260731094821_add_span_context_to_trigger_queue.sql create mode 100644 src/telemetry.rs diff --git a/.sqlx/query-30b46e820bb013286bf5a18db3513ac49077fb81e012c1d3172bcb24941835bb.json b/.sqlx/query-30b46e820bb013286bf5a18db3513ac49077fb81e012c1d3172bcb24941835bb.json deleted file mode 100644 index 2217d50..0000000 --- a/.sqlx/query-30b46e820bb013286bf5a18db3513ac49077fb81e012c1d3172bcb24941835bb.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT INTO trigger_queue (branch_id, new_hash, target_repo, event_type, gh_app_installation_id)\n SELECT ?, ?, s.target_repo, s.event_type, s.gh_app_installation_id\n FROM subscriptions s\n WHERE s.branch_id = ?\n ON CONFLICT(target_repo, event_type) WHERE status = 'PENDING'\n DO UPDATE SET branch_id = excluded.branch_id, new_hash = excluded.new_hash, status_updated_at = CURRENT_TIMESTAMP", - "describe": { - "columns": [], - "parameters": { - "Right": 3 - }, - "nullable": [] - }, - "hash": "30b46e820bb013286bf5a18db3513ac49077fb81e012c1d3172bcb24941835bb" -} diff --git a/.sqlx/query-d87a0e607c8036a121f10b31bdcaec9bbefc34795e8a828b6ee87f5c2701ca5e.json b/.sqlx/query-d87a0e607c8036a121f10b31bdcaec9bbefc34795e8a828b6ee87f5c2701ca5e.json new file mode 100644 index 0000000..3b138bf --- /dev/null +++ b/.sqlx/query-d87a0e607c8036a121f10b31bdcaec9bbefc34795e8a828b6ee87f5c2701ca5e.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO trigger_queue (branch_id, new_hash, target_repo, event_type, gh_app_installation_id, span_context)\n SELECT ?, ?, s.target_repo, s.event_type, s.gh_app_installation_id, ?\n FROM subscriptions s\n WHERE s.branch_id = ?\n ON CONFLICT(target_repo, event_type) WHERE status = 'PENDING'\n DO UPDATE SET branch_id = excluded.branch_id, new_hash = excluded.new_hash, span_context = excluded.span_context, status_updated_at = CURRENT_TIMESTAMP", + "describe": { + "columns": [], + "parameters": { + "Right": 4 + }, + "nullable": [] + }, + "hash": "d87a0e607c8036a121f10b31bdcaec9bbefc34795e8a828b6ee87f5c2701ca5e" +} diff --git a/migrations/20260731094821_add_span_context_to_trigger_queue.sql b/migrations/20260731094821_add_span_context_to_trigger_queue.sql new file mode 100644 index 0000000..35443c3 --- /dev/null +++ b/migrations/20260731094821_add_span_context_to_trigger_queue.sql @@ -0,0 +1 @@ +ALTER TABLE trigger_queue ADD COLUMN span_context TEXT; diff --git a/src/lib.rs b/src/lib.rs index 3b33ceb..6b5f546 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,6 +58,7 @@ pub mod model; pub mod polling; pub mod repository; pub mod state; +pub mod telemetry; #[cfg(test)] mod test_utils; #[cfg(test)] diff --git a/src/model.rs b/src/model.rs index e892760..f4c2e27 100644 --- a/src/model.rs +++ b/src/model.rs @@ -225,4 +225,7 @@ pub struct TriggerQueueItem { /// Number of times the task has been attempted. pub retry_count: i64, + + /// Serialized OpenTelemetry span context. + pub span_context: Option, } diff --git a/src/polling/mod.rs b/src/polling/mod.rs index b8568c9..cd80002 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -140,8 +140,14 @@ async fn process_single_branch( branch_info.branch.name, branch_info.latest_hash ); - repo.queue_triggers_for_branch(branch_info.branch.id, &branch_info.latest_hash, tx) - .await?; + let span_context = crate::telemetry::serialize_current_span_context(); + + let trigger_params = crate::repository::trigger::QueueTriggersParams { + branch_id: branch_info.branch.id, + new_hash: &branch_info.latest_hash, + span_context: span_context.as_deref(), + }; + repo.queue_triggers_for_branch(trigger_params, tx).await?; Ok(()) } diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index 55f7d33..d257569 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -445,7 +445,7 @@ impl TriggerRepository for SqliteRepository { WHERE status IN ('PENDING') AND next_retry_at <= CURRENT_TIMESTAMP ORDER BY next_retry_at ASC LIMIT 1 ) - RETURNING id, branch_id, new_hash, retry_count, target_repo, event_type, gh_app_installation_id", + RETURNING id, branch_id, new_hash, retry_count, target_repo, event_type, gh_app_installation_id, span_context", ) .fetch_optional(&self.pool) .await @@ -499,19 +499,22 @@ impl TriggerRepository for SqliteRepository { async fn queue_triggers_for_branch( &self, - branch_id: i64, - new_hash: &crate::domain::CommitHash, + params: crate::repository::trigger::QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { + let branch_id = params.branch_id; + let new_hash = params.new_hash; + let span_context = params.span_context; sqlx::query!( - "INSERT INTO trigger_queue (branch_id, new_hash, target_repo, event_type, gh_app_installation_id) - SELECT ?, ?, s.target_repo, s.event_type, s.gh_app_installation_id + "INSERT INTO trigger_queue (branch_id, new_hash, target_repo, event_type, gh_app_installation_id, span_context) + SELECT ?, ?, s.target_repo, s.event_type, s.gh_app_installation_id, ? FROM subscriptions s WHERE s.branch_id = ? ON CONFLICT(target_repo, event_type) WHERE status = 'PENDING' - DO UPDATE SET branch_id = excluded.branch_id, new_hash = excluded.new_hash, status_updated_at = CURRENT_TIMESTAMP", + DO UPDATE SET branch_id = excluded.branch_id, new_hash = excluded.new_hash, span_context = excluded.span_context, status_updated_at = CURRENT_TIMESTAMP", branch_id, new_hash, + span_context, branch_id ) .execute(executor) diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index 8bbdb34..f6a9524 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -20,6 +20,19 @@ pub struct UpdateRetryStatus { pub backoff_base_secs: u64, } +/// Parameters for queueing triggers for a branch. +#[derive(Debug, Clone)] +pub struct QueueTriggersParams<'a> { + /// The unique identifier of the branch. + pub branch_id: i64, + + /// The new commit hash. + pub new_hash: &'a crate::domain::CommitHash, + + /// Optional serialized OpenTelemetry span context. + pub span_context: Option<&'a str>, +} + /// Interface for `trigger_queue` table operations. #[async_trait] pub trait TriggerRepository: Send + Sync { @@ -43,8 +56,7 @@ pub trait TriggerRepository: Send + Sync { /// Queues trigger events for all subscriptions of a branch. async fn queue_triggers_for_branch( &self, - branch_id: i64, - new_hash: &crate::domain::CommitHash, + params: QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError>; } diff --git a/src/telemetry.rs b/src/telemetry.rs new file mode 100644 index 0000000..97e3f5f --- /dev/null +++ b/src/telemetry.rs @@ -0,0 +1,52 @@ +//! OpenTelemetry telemetry helpers. + +use opentelemetry::global; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +/// Serializes the current tracing span's OpenTelemetry context into an optional JSON string, +/// logging a warning if serialization fails. +pub fn serialize_current_span_context() -> Option { + let context = tracing::Span::current().context(); + let mut map = std::collections::HashMap::new(); + global::get_text_map_propagator(|propagator| { + propagator.inject_context(&context, &mut map); + }); + + if map.is_empty() { + return None; + } + + serde_json::to_string(&map) + .map_err(|e| tracing::warn!("Failed to serialize span context: {e}")) + .ok() +} + +/// Adds a span link to the given tracing span +/// from a serialized OpenTelemetry span context string, if valid. +pub fn add_link_from_serialized_context(span: &tracing::Span, span_context: Option<&str>) { + let Some(span_ctx_str) = span_context else { + return; + }; + + match deserialize_span_context(span_ctx_str) { + Ok(parent_ctx) => { + let remote_span_ctx = opentelemetry::trace::TraceContextExt::span(&parent_ctx) + .span_context() + .clone(); + if remote_span_ctx.is_valid() { + tracing_opentelemetry::OpenTelemetrySpanExt::add_link(span, remote_span_ctx); + } + } + Err(e) => { + tracing::warn!("Failed to deserialize span context: {e}"); + } + } +} + +/// Deserializes a JSON string into an OpenTelemetry context. +fn deserialize_span_context(s: &str) -> Result { + let map: std::collections::HashMap = serde_json::from_str(s)?; + let context = global::get_text_map_propagator(|propagator| propagator.extract(&map)); + + Ok(context) +} diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index e2ec6d8..ff1d900 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -71,6 +71,11 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro return Ok(()); }; + crate::telemetry::add_link_from_serialized_context( + &tracing::Span::current(), + trigger.span_context.as_deref(), + ); + tracing::Span::current().record("trigger", valuable(&trigger)); let dispatch_result = dispatch_events(engine, &trigger).await; @@ -439,6 +444,7 @@ mod tests { target_repo: TargetRepo::new("org/repo".to_string()).unwrap(), event_type: EventType::new("event".to_string()).unwrap(), gh_app_installation_id: 1, + span_context: None, }; let engine = TriggerEngine { From cf8aa793e09b0c8c2e78e3aee5da3a0784479c35 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 3 Aug 2026 12:14:25 +0200 Subject: [PATCH 12/50] Set up OpenTelemetry subscriber --- Cargo.lock | 271 ++++++++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- src/lib.rs | 14 ++- src/main.rs | 22 ++-- src/telemetry.rs | 74 +++++++++++++ 5 files changed, 368 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7bdb696..519bb73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "arc-swap" version = "1.9.2" @@ -406,6 +412,8 @@ dependencies = [ "humantime-serde", "jsonwebtoken", "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand 0.10.2", "reqwest", "rovo", @@ -1065,6 +1073,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -1074,7 +1094,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -2239,6 +2259,19 @@ dependencies = [ "tower-service", ] +[[package]] +name = "hyper-timeout" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" +dependencies = [ + "hyper", + "hyper-util", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "hyper-util" version = "0.1.20" @@ -2425,6 +2458,15 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -2669,6 +2711,15 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "matchit" version = "0.8.4" @@ -2867,6 +2918,67 @@ dependencies = [ "tracing", ] +[[package]] +name = "opentelemetry-http" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", + "reqwest", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "reqwest", + "thiserror", + "tokio", + "tonic", + "tonic-types", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56d658ba1faf63f7b9c492cfbe6e0ec365440a16132d3270c1065f7b33f1b638" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", + "tonic-prost", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b59f80e1ac4d5ff7a2db8fb6c80badb7f0f3f858211fba08dd9aaec750894f9" +dependencies = [ + "futures-channel", + "futures-executor", + "futures-util", + "opentelemetry", + "percent-encoding", + "portable-atomic", + "rand 0.9.5", + "thiserror", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2979,6 +3091,26 @@ dependencies = [ "pest", ] +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3099,6 +3231,38 @@ dependencies = [ "parking_lot", ] +[[package]] +name = "prost" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528ac67416ff8646872a3c02cad9cc4ee5dc9f9540c9b10771855c95cb2e5ae1" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b570b25f7617e43d59005d0990ccb79e950a423952cea19671b7a876da390adf" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "prost-types" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +dependencies = [ + "prost", +] + [[package]] name = "quinn" version = "0.11.11" @@ -3165,6 +3329,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" @@ -3182,6 +3352,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ef1d0d795eb7d84685bca4f72f3649f064e6641543d3a8c415898726a57b41" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand" version = "0.10.2" @@ -3203,6 +3383,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.10.0" @@ -3222,6 +3412,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_core" version = "0.10.1" @@ -4422,6 +4621,54 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac2a5518c70fa84342385732db33fb3f44bc4cc748936eb5833d2df34d6445ef" +dependencies = [ + "async-trait", + "base64", + "bytes", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-timeout", + "hyper-util", + "percent-encoding", + "pin-project", + "sync_wrapper", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tonic-prost" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +dependencies = [ + "bytes", + "prost", + "tonic", +] + +[[package]] +name = "tonic-types" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab1b02061f83d519bba3caa167f88f261ef05720ab8ebc954ade70de3348e8" +dependencies = [ + "prost", + "prost-types", + "tonic", +] + [[package]] name = "tower" version = "0.5.3" @@ -4430,9 +4677,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4547,10 +4797,14 @@ version = "0.3.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" dependencies = [ + "matchers", "nu-ansi-term", + "once_cell", + "regex-automata", "sharded-slab", "smallvec", "thread_local", + "tracing", "tracing-core", "tracing-log", "tracing-serde", @@ -4781,6 +5035,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasite" version = "0.1.0" @@ -5180,6 +5443,12 @@ dependencies = [ "url", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 74f45c5..66adc21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ http = "1.4.2" humantime-serde = "1.1.1" jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] } opentelemetry = "0.32.0" +opentelemetry-otlp = { version = "0.32.0", features = ["trace", "grpc-tonic"] } +opentelemetry_sdk = "0.32.1" reqwest = { version = "0.13.4", features = ["json"] } rovo = { version = "0.4.8", features = ["scalar"] } schemars = { version = "0.9", features = ["chrono04"] } @@ -41,7 +43,7 @@ tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6", features = ["timeout"] } tracing = { version = "0.1.44", features = ["valuable"] } tracing-opentelemetry = "0.33.0" -tracing-subscriber = { version = "0.3", features = ["valuable"] } +tracing-subscriber = { version = "0.3", features = ["valuable", "env-filter"] } url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } valuable = { version = "0.1.1", features = ["derive"] } diff --git a/src/lib.rs b/src/lib.rs index 6b5f546..0a083a3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,15 @@ pub mod trigger; type EngineTask = (Box, &'static str); /// Runs the server, delegating errors to the caller. -pub async fn run_app(tracker: &TaskTracker, token: &CancellationToken) -> Result<(), FatalError> { +/// +/// Initializes telemetry on entry and returns a [`telemetry::TelemetryGuard`] +/// that must be held until all spawned tasks have terminated, +/// so that spans are flushed before the tracer provider is shut down. +pub async fn run_app( + tracker: &TaskTracker, + token: &CancellationToken, +) -> Result { + let tracer_guard = crate::telemetry::init(); let config = Config::load()?; let pool = init_database(&config).await?; let repository = std::sync::Arc::new(crate::repository::SqliteRepository::new(pool.clone())); @@ -93,7 +101,9 @@ pub async fn run_app(tracker: &TaskTracker, token: &CancellationToken) -> Result let app = build_router(repository, pool, &config); - run_server(app, &ctx.config, token.clone()).await + run_server(app, &ctx.config, token.clone()).await?; + + Ok(tracer_guard) } /// Initializes the database pool. diff --git a/src/main.rs b/src/main.rs index 8a3710b..360e5fc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,20 +14,16 @@ use tracing::{error, info}; #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); - - #[cfg(debug_assertions)] - tracing::warn!("APPLICATION IS RUNNING IN DEBUG MODE."); - let tracker = TaskTracker::new(); let token = CancellationToken::new(); - run_app(&tracker, &token) - .await - .unwrap_or_else(|e| error!("{e}")); - - token.cancel(); - tracker.close(); - tracker.wait().await; - info!("All systems terminated. Terminating process.") + match run_app(&tracker, &token).await { + Ok(_tracer_guard) => { + token.cancel(); + tracker.close(); + tracker.wait().await; + info!("All systems terminated. Terminating process."); + } + Err(e) => error!("{e}"), + } } diff --git a/src/telemetry.rs b/src/telemetry.rs index 97e3f5f..a9900ec 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,7 +1,81 @@ //! OpenTelemetry telemetry helpers. use opentelemetry::global; +use opentelemetry_sdk::trace::SdkTracerProvider; use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; + +/// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. +fn init_tracer_provider() -> Option { + if std::env::var_os("OTEL_SDK_DISABLED").is_some() { + eprintln!("OpenTelemetry disabled via OTEL_SDK_DISABLED."); + return None; + } + + let exporter = match opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build() + { + Ok(exporter) => exporter, + Err(e) => { + eprintln!("Failed to build OTLP span exporter, telemetry disabled: {e}"); + return None; + } + }; + + Some( + SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name("commit-bridge") + .build(), + ) + .build(), + ) +} + +/// Guard that gracefully shuts down the tracer provider on drop. +/// +/// Must be held alive while spans are still being emitted; +/// dropping it flushes and shuts down the underlying provider. +#[must_use] +pub struct TelemetryGuard(Option); + +impl Drop for TelemetryGuard { + fn drop(&mut self) { + if let Some(provider) = self.0.take() + && let Err(e) = provider.shutdown() + { + tracing::error!("Failed to gracefully shut down tracer provider: {e}"); + } + } +} + +/// Sets up the global OpenTelemetry propagator and tracing subscriber. +pub fn init() -> TelemetryGuard { + global::set_text_map_propagator(opentelemetry_sdk::propagation::TraceContextPropagator::new()); + + let tracer_provider = init_tracer_provider(); + + let otel_layer = tracer_provider.as_ref().map(|provider| { + let tracer = opentelemetry::trace::TracerProvider::tracer(provider, "commit-bridge"); + tracing_opentelemetry::layer().with_tracer(tracer) + }); + + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + tracing_subscriber::registry() + .with(env_filter) + .with(tracing_subscriber::fmt::layer()) + .with(otel_layer) + .init(); + + #[cfg(debug_assertions)] + tracing::warn!("APPLICATION IS RUNNING IN DEBUG MODE."); + + TelemetryGuard(tracer_provider) +} /// Serializes the current tracing span's OpenTelemetry context into an optional JSON string, /// logging a warning if serialization fails. From 266a5bafe71603883ff8240b7636fa09e077848f Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 4 Aug 2026 11:00:58 +0200 Subject: [PATCH 13/50] Make telemetry export optional --- .env.example | 6 ++++++ src/config.rs | 12 ------------ src/lib.rs | 23 +++++++++++++++++++++++ src/telemetry.rs | 29 ++++++++++++++++++++++++++++- 4 files changed, 57 insertions(+), 13 deletions(-) diff --git a/.env.example b/.env.example index 69f7e11..a2450ff 100644 --- a/.env.example +++ b/.env.example @@ -44,3 +44,9 @@ CBRIDGE__AUTH__PEM_PATH=/app/data/YOUR_PEM_FILE.pem # Git CBRIDGE__GIT__REPO_PATH=/app/data/git_repo + +# Telemetry +# OpenTelemetry is disabled by default +# and only enabled when an OTLP endpoint is explicitly configured, +# e.g. for a local Jaeger instance: +# OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 diff --git a/src/config.rs b/src/config.rs index 4ae0c1d..ba6a676 100644 --- a/src/config.rs +++ b/src/config.rs @@ -40,18 +40,6 @@ pub struct Config { impl Config { /// Bootstraps the application configuration from the environment. pub fn load() -> Result { - if dotenvy::dotenv().is_ok() { - #[cfg(debug_assertions)] - tracing::info!("Successfully loaded local `.env` file."); - - #[cfg(not(debug_assertions))] - tracing::warn!( - "Successfully loaded local `.env` file. \ - If this is a production build, \ - environment variables should be set prior to execution." - ); - } - let environment = Environment::with_prefix("CBRIDGE") .separator("__") .try_parsing(true); diff --git a/src/lib.rs b/src/lib.rs index 0a083a3..ded25ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,11 @@ pub async fn run_app( tracker: &TaskTracker, token: &CancellationToken, ) -> Result { + // Load `.env` before telemetry initialization, so that OTEL environment + // variables are visible when the tracer provider checks for them. + let dotenv_loaded = dotenvy::dotenv().is_ok(); let tracer_guard = crate::telemetry::init(); + log_dotenv_status(dotenv_loaded); let config = Config::load()?; let pool = init_database(&config).await?; let repository = std::sync::Arc::new(crate::repository::SqliteRepository::new(pool.clone())); @@ -106,6 +110,25 @@ pub async fn run_app( Ok(tracer_guard) } +/// Logs the outcome of the `.env` file load. +/// +/// Must only be called after the tracing subscriber is initialized. +fn log_dotenv_status(loaded: bool) { + if !loaded { + return; + } + + #[cfg(debug_assertions)] + tracing::info!("Successfully loaded local `.env` file."); + + #[cfg(not(debug_assertions))] + tracing::warn!( + "Successfully loaded local `.env` file. \ + If this is a production build, \ + environment variables should be set prior to execution." + ); +} + /// Initializes the database pool. async fn init_database(config: &Config) -> Result { let options = SqliteConnectOptions::from_str(config.database.url.as_str())? diff --git a/src/telemetry.rs b/src/telemetry.rs index a9900ec..589f13d 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -7,11 +7,26 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx /// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. fn init_tracer_provider() -> Option { - if std::env::var_os("OTEL_SDK_DISABLED").is_some() { + if std::env::var("OTEL_SDK_DISABLED") + .is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") + { eprintln!("OpenTelemetry disabled via OTEL_SDK_DISABLED."); return None; } + if std::env::var("OTEL_TRACES_EXPORTER").as_deref() == Ok("none") { + eprintln!("OpenTelemetry disabled via OTEL_TRACES_EXPORTER=none."); + return None; + } + + if !otlp_endpoint_is_configured() { + eprintln!( + "OTLP endpoint not configured, telemetry disabled. \ + Set OTEL_EXPORTER_OTLP_ENDPOINT to enable." + ); + return None; + } + let exporter = match opentelemetry_otlp::SpanExporter::builder() .with_tonic() .build() @@ -35,6 +50,18 @@ fn init_tracer_provider() -> Option { ) } +/// Returns `true` if an OTLP endpoint has been explicitly configured +/// through the standard OpenTelemetry environment variables. +fn otlp_endpoint_is_configured() -> bool { + is_non_empty_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + || is_non_empty_var("OTEL_EXPORTER_OTLP_ENDPOINT") +} + +/// Returns `true` if the environment variable is set to a non-empty value. +fn is_non_empty_var(name: &str) -> bool { + std::env::var_os(name).is_some_and(|value| !value.is_empty()) +} + /// Guard that gracefully shuts down the tracer provider on drop. /// /// Must be held alive while spans are still being emitted; From c0abf058d70c03664c224454d35dcc1266e22988 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 5 Aug 2026 11:07:51 +0200 Subject: [PATCH 14/50] Instrument repository methods --- src/repository/sqlite.rs | 60 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index d257569..e8048d5 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -34,6 +34,7 @@ impl SqliteRepository { } /// Runs a closure within a transaction. + #[tracing::instrument(skip_all, name = "transaction")] pub async fn run_in_transaction<'a, F, T, E>(&self, f: F) -> Result where F: for<'b> FnOnce(&'b mut SqliteConnection) -> BoxFuture<'b, Result> + Send + 'a, @@ -49,6 +50,7 @@ impl SqliteRepository { #[async_trait] impl BranchRepository for SqliteRepository { + #[tracing::instrument(skip_all, name = "branches.get_all")] async fn get_all(&self) -> Result, RepositoryError> { sqlx::query_as::<_, Branch>("SELECT * FROM branches") .fetch_all(&self.pool) @@ -56,6 +58,7 @@ impl BranchRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "branches.find_by_id", fields(id = %id))] async fn find_by_id(&self, id: i64) -> Result, RepositoryError> { sqlx::query_as::<_, Branch>("SELECT * FROM branches WHERE id = ?") .bind(id) @@ -64,6 +67,7 @@ impl BranchRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "branches.delete_by_id", fields(id = %id))] async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { let result = sqlx::query!("DELETE FROM branches WHERE id = ?", id) .execute(&self.pool) @@ -76,6 +80,7 @@ impl BranchRepository for SqliteRepository { Ok(()) } + #[tracing::instrument(skip_all, name = "branches.update_last_commit_hash", fields(id = %id))] async fn update_last_commit_hash( &self, id: i64, @@ -92,6 +97,11 @@ impl BranchRepository for SqliteRepository { Ok(()) } + #[tracing::instrument( + skip_all, + name = "branches.update_last_commit_hash_in_tx", + fields(id = %id) + )] async fn update_last_commit_hash_in_tx( &self, id: i64, @@ -112,6 +122,7 @@ impl BranchRepository for SqliteRepository { #[async_trait] impl SubscriptionRepository for SqliteRepository { + #[tracing::instrument(skip_all, name = "subscriptions.create")] async fn create( &self, subscription_payload: &CreateSubscription, @@ -154,6 +165,7 @@ impl SubscriptionRepository for SqliteRepository { }) } + #[tracing::instrument(skip_all, name = "subscriptions.get_by_id", fields(id = %id))] async fn get_by_id(&self, id: i64) -> Result, RepositoryError> { sqlx::query_as::<_, Subscription>("SELECT * FROM subscriptions WHERE id = ?") .bind(id) @@ -162,6 +174,7 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "subscriptions.get_by_id_with_branch", fields(id = %id))] async fn get_by_id_with_branch( &self, id: i64, @@ -201,6 +214,11 @@ impl SubscriptionRepository for SqliteRepository { } } + #[tracing::instrument( + skip_all, + name = "subscriptions.get_by_keys_with_branch", + fields(branch_id = %branch_id, target_repo = %target_repo, event_type = %event_type) + )] async fn get_by_keys_with_branch( &self, branch_id: i64, @@ -244,6 +262,11 @@ impl SubscriptionRepository for SqliteRepository { } } + #[tracing::instrument( + skip_all, + name = "subscriptions.list_paginated", + fields(last_id = %last_id, limit = %limit) + )] async fn list_paginated( &self, last_id: i64, @@ -259,6 +282,11 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument( + skip_all, + name = "subscriptions.list_paginated_with_branches", + fields(last_id = %last_id, limit = %limit) + )] async fn list_paginated_with_branches( &self, last_id: i64, @@ -303,6 +331,7 @@ impl SubscriptionRepository for SqliteRepository { subscriptions } + #[tracing::instrument(skip_all, name = "subscriptions.count_remaining", fields(last_id = %last_id))] async fn count_remaining(&self, last_id: i64) -> Result { sqlx::query_scalar!("SELECT COUNT(*) FROM subscriptions WHERE id > ?", last_id) .fetch_one(&self.pool) @@ -310,6 +339,7 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "subscriptions.update", fields(id = %id))] async fn update( &self, id: i64, @@ -348,6 +378,7 @@ impl SubscriptionRepository for SqliteRepository { .ok_or(RepositoryError::NotFound) } + #[tracing::instrument(skip_all, name = "subscriptions.delete", fields(id = %id))] async fn delete(&self, id: i64) -> Result<(), RepositoryError> { let result = sqlx::query!("DELETE FROM subscriptions WHERE id = ?", id) .execute(&self.pool) @@ -360,6 +391,11 @@ impl SubscriptionRepository for SqliteRepository { Ok(()) } + #[tracing::instrument( + skip_all, + name = "subscriptions.get_branch_id_by_subscription_id", + fields(id = %id) + )] async fn get_branch_id_by_subscription_id( &self, id: i64, @@ -370,6 +406,11 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument( + skip_all, + name = "subscriptions.count_subscriptions_by_branch_id", + fields(branch_id = %branch_id) + )] async fn count_subscriptions_by_branch_id( &self, branch_id: i64, @@ -383,6 +424,7 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "subscriptions.delete_and_cascade", fields(id = %id))] async fn delete_subscription_and_cascade(&self, id: i64) -> Result<(), RepositoryError> { self.run_in_transaction(|tx| { Box::pin(async move { @@ -419,6 +461,7 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { + #[tracing::instrument(skip_all, name = "trigger_queue.get_all")] async fn get_all(&self) -> Result, RepositoryError> { sqlx::query_as::<_, TriggerQueueItem>("SELECT * FROM trigger_queue") .fetch_all(&self.pool) @@ -426,6 +469,7 @@ impl TriggerRepository for SqliteRepository { .map_err(RepositoryError::Database) } + #[tracing::instrument(skip_all, name = "trigger_queue.delete_by_id", fields(id = %id))] async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { sqlx::query!("DELETE FROM trigger_queue WHERE id = ?", id) .execute(&self.pool) @@ -434,6 +478,7 @@ impl TriggerRepository for SqliteRepository { Ok(()) } + #[tracing::instrument(skip_all, name = "trigger_queue.mark_processing")] async fn find_oldest_pending_and_mark_processing( &self, ) -> Result, RepositoryError> { @@ -454,6 +499,11 @@ impl TriggerRepository for SqliteRepository { Ok(trigger) } + #[tracing::instrument( + skip_all, + name = "trigger_queue.update_retry_status", + fields(id = %params.id, retry_count = %params.retry_count) + )] async fn update_retry_status(&self, params: UpdateRetryStatus) -> Result<(), RepositoryError> { let next_retry_count = params.retry_count + 1; @@ -481,6 +531,11 @@ impl TriggerRepository for SqliteRepository { Ok(()) } + #[tracing::instrument( + skip_all, + name = "trigger_queue.recover_stuck_tasks", + fields(threshold_seconds = %threshold_seconds) + )] async fn recover_stuck_tasks(&self, threshold_seconds: u64) -> Result<(), RepositoryError> { let threshold_str = format!("-{} seconds", threshold_seconds); @@ -497,6 +552,11 @@ impl TriggerRepository for SqliteRepository { Ok(()) } + #[tracing::instrument( + skip_all, + name = "trigger_queue.queue_for_branch", + fields(branch_id = %params.branch_id) + )] async fn queue_triggers_for_branch( &self, params: crate::repository::trigger::QueueTriggersParams<'_>, From 9e4cda19f542dc4a65b46470e8c8dd16189557da Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 5 Aug 2026 11:40:42 +0200 Subject: [PATCH 15/50] Rename and cleanup repostory methods --- src/handler.rs | 20 +++-- src/polling/mod.rs | 15 +++- src/repository/branch.rs | 10 +-- src/repository/sqlite.rs | 141 ++++++++++++--------------------- src/repository/subscription.rs | 32 ++++---- src/repository/trigger.rs | 18 +++-- src/trigger/mod.rs | 27 +++++-- 7 files changed, 127 insertions(+), 136 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 54635da..3f95679 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -70,7 +70,7 @@ async fn create_subscription_inner( State(state): State, Json(payload): Json, ) -> Result, HandlerError> { - let sub_with_branch = state.repository.create(&payload).await?; + let sub_with_branch = state.repository.subscriptions_create(&payload).await?; info!( "Registered new subscription for branch ID {} (repo: {}, branch: {}): {:?}", @@ -134,13 +134,16 @@ async fn list_subscriptions_inner( let subscriptions = state .repository - .list_paginated_with_branches(last_id, limit as i64) + .subscriptions_list_paginated(last_id, limit as i64) .await?; let data: Vec = subscriptions.into_iter().map(map_to_hal).collect(); let next_id = data.last().map(|s| s.subscription.id).unwrap_or(last_id); - let remaining_count = state.repository.count_remaining(next_id).await?; + let remaining_count = state + .repository + .subscriptions_count_remaining(next_id) + .await?; let next_link = data .last() @@ -195,7 +198,7 @@ async fn get_subscription_inner( ) -> Result, HandlerError> { let sub_with_branch = state .repository - .get_by_id_with_branch(id) + .subscriptions_get_by_id_with_branch(id) .await? .ok_or(HandlerError::NotFound)?; Ok(Json(map_to_hal(sub_with_branch))) @@ -238,10 +241,10 @@ async fn update_subscription_inner( Path(id): Path, Json(payload): Json, ) -> Result, HandlerError> { - state.repository.update(id, &payload).await?; + state.repository.subscriptions_update(id, &payload).await?; let sub_with_branch = state .repository - .get_by_id_with_branch(id) + .subscriptions_get_by_id_with_branch(id) .await? .ok_or(HandlerError::NotFound)?; @@ -282,7 +285,10 @@ async fn delete_subscription_inner( State(state): State, Path(id): Path, ) -> Result<(), HandlerError> { - state.repository.delete_subscription_and_cascade(id).await?; + state + .repository + .subscriptions_delete_and_cascade(id) + .await?; Ok(()) } diff --git a/src/polling/mod.rs b/src/polling/mod.rs index cd80002..6b442c9 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -71,7 +71,9 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { /// Gathers stored branches that need to be updated. #[tracing::instrument(skip_all)] async fn gather_updated_branches(ctx: &SharedContext) -> Result, sqlx::Error> { - let branches = BranchRepository::get_all(ctx.repository.as_ref()) + let branches = ctx + .repository + .branches_get_all() .await .map_err(|e| match e { crate::repository::RepositoryError::Database(e) => e, @@ -132,8 +134,12 @@ async fn process_single_branch( branch_info: &branch::BranchInfo, tx: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { - repo.update_last_commit_hash_in_tx(branch_info.branch.id, &branch_info.latest_hash, tx) - .await?; + repo.branches_update_last_commit_hash_in_tx( + branch_info.branch.id, + &branch_info.latest_hash, + tx, + ) + .await?; info!( "New commit detected for branch {}. Hash: {}", @@ -147,7 +153,8 @@ async fn process_single_branch( new_hash: &branch_info.latest_hash, span_context: span_context.as_deref(), }; - repo.queue_triggers_for_branch(trigger_params, tx).await?; + repo.trigger_queue_queue_for_branch(trigger_params, tx) + .await?; Ok(()) } diff --git a/src/repository/branch.rs b/src/repository/branch.rs index 87edf99..870eea1 100644 --- a/src/repository/branch.rs +++ b/src/repository/branch.rs @@ -8,23 +8,23 @@ use async_trait::async_trait; #[async_trait] pub trait BranchRepository: Send + Sync { /// Returns all branches. - async fn get_all(&self) -> Result, RepositoryError>; + async fn branches_get_all(&self) -> Result, RepositoryError>; /// Returns the branch with the given `id`. - async fn find_by_id(&self, id: i64) -> Result, RepositoryError>; + async fn branches_find_by_id(&self, id: i64) -> Result, RepositoryError>; /// Deletes the branch with the given `id`. - async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; + async fn branches_delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; /// Updates the last commit hash of the branch. - async fn update_last_commit_hash( + async fn branches_update_last_commit_hash( &self, id: i64, hash: &crate::domain::CommitHash, ) -> Result<(), RepositoryError>; /// Updates the last commit hash of the branch within a transaction. - async fn update_last_commit_hash_in_tx( + async fn branches_update_last_commit_hash_in_tx( &self, id: i64, hash: &crate::domain::CommitHash, diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index e8048d5..b6d1fec 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -34,7 +34,7 @@ impl SqliteRepository { } /// Runs a closure within a transaction. - #[tracing::instrument(skip_all, name = "transaction")] + #[tracing::instrument(skip_all)] pub async fn run_in_transaction<'a, F, T, E>(&self, f: F) -> Result where F: for<'b> FnOnce(&'b mut SqliteConnection) -> BoxFuture<'b, Result> + Send + 'a, @@ -50,16 +50,16 @@ impl SqliteRepository { #[async_trait] impl BranchRepository for SqliteRepository { - #[tracing::instrument(skip_all, name = "branches.get_all")] - async fn get_all(&self) -> Result, RepositoryError> { + #[tracing::instrument(skip_all)] + async fn branches_get_all(&self) -> Result, RepositoryError> { sqlx::query_as::<_, Branch>("SELECT * FROM branches") .fetch_all(&self.pool) .await .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "branches.find_by_id", fields(id = %id))] - async fn find_by_id(&self, id: i64) -> Result, RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn branches_find_by_id(&self, id: i64) -> Result, RepositoryError> { sqlx::query_as::<_, Branch>("SELECT * FROM branches WHERE id = ?") .bind(id) .fetch_optional(&self.pool) @@ -67,8 +67,8 @@ impl BranchRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "branches.delete_by_id", fields(id = %id))] - async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn branches_delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { let result = sqlx::query!("DELETE FROM branches WHERE id = ?", id) .execute(&self.pool) .await @@ -80,8 +80,8 @@ impl BranchRepository for SqliteRepository { Ok(()) } - #[tracing::instrument(skip_all, name = "branches.update_last_commit_hash", fields(id = %id))] - async fn update_last_commit_hash( + #[tracing::instrument(skip_all, fields(id = %id))] + async fn branches_update_last_commit_hash( &self, id: i64, hash: &crate::domain::CommitHash, @@ -97,12 +97,8 @@ impl BranchRepository for SqliteRepository { Ok(()) } - #[tracing::instrument( - skip_all, - name = "branches.update_last_commit_hash_in_tx", - fields(id = %id) - )] - async fn update_last_commit_hash_in_tx( + #[tracing::instrument(skip_all, fields(id = %id))] + async fn branches_update_last_commit_hash_in_tx( &self, id: i64, hash: &crate::domain::CommitHash, @@ -122,8 +118,8 @@ impl BranchRepository for SqliteRepository { #[async_trait] impl SubscriptionRepository for SqliteRepository { - #[tracing::instrument(skip_all, name = "subscriptions.create")] - async fn create( + #[tracing::instrument(skip_all)] + async fn subscriptions_create( &self, subscription_payload: &CreateSubscription, ) -> Result { @@ -165,8 +161,11 @@ impl SubscriptionRepository for SqliteRepository { }) } - #[tracing::instrument(skip_all, name = "subscriptions.get_by_id", fields(id = %id))] - async fn get_by_id(&self, id: i64) -> Result, RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_get_by_id( + &self, + id: i64, + ) -> Result, RepositoryError> { sqlx::query_as::<_, Subscription>("SELECT * FROM subscriptions WHERE id = ?") .bind(id) .fetch_optional(&self.pool) @@ -174,8 +173,8 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "subscriptions.get_by_id_with_branch", fields(id = %id))] - async fn get_by_id_with_branch( + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_get_by_id_with_branch( &self, id: i64, ) -> Result, RepositoryError> { @@ -216,10 +215,9 @@ impl SubscriptionRepository for SqliteRepository { #[tracing::instrument( skip_all, - name = "subscriptions.get_by_keys_with_branch", fields(branch_id = %branch_id, target_repo = %target_repo, event_type = %event_type) )] - async fn get_by_keys_with_branch( + async fn subscriptions_get_by_keys_with_branch( &self, branch_id: i64, target_repo: &TargetRepo, @@ -262,32 +260,8 @@ impl SubscriptionRepository for SqliteRepository { } } - #[tracing::instrument( - skip_all, - name = "subscriptions.list_paginated", - fields(last_id = %last_id, limit = %limit) - )] - async fn list_paginated( - &self, - last_id: i64, - limit: i64, - ) -> Result, RepositoryError> { - sqlx::query_as::<_, Subscription>( - "SELECT * FROM subscriptions WHERE id > ? ORDER BY id ASC LIMIT ?", - ) - .bind(last_id) - .bind(limit) - .fetch_all(&self.pool) - .await - .map_err(RepositoryError::Database) - } - - #[tracing::instrument( - skip_all, - name = "subscriptions.list_paginated_with_branches", - fields(last_id = %last_id, limit = %limit) - )] - async fn list_paginated_with_branches( + #[tracing::instrument(skip_all, fields(last_id = %last_id, limit = %limit))] + async fn subscriptions_list_paginated( &self, last_id: i64, limit: i64, @@ -331,16 +305,16 @@ impl SubscriptionRepository for SqliteRepository { subscriptions } - #[tracing::instrument(skip_all, name = "subscriptions.count_remaining", fields(last_id = %last_id))] - async fn count_remaining(&self, last_id: i64) -> Result { + #[tracing::instrument(skip_all, fields(last_id = %last_id))] + async fn subscriptions_count_remaining(&self, last_id: i64) -> Result { sqlx::query_scalar!("SELECT COUNT(*) FROM subscriptions WHERE id > ?", last_id) .fetch_one(&self.pool) .await .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "subscriptions.update", fields(id = %id))] - async fn update( + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_update( &self, id: i64, subscription: &UpdateSubscription, @@ -378,8 +352,8 @@ impl SubscriptionRepository for SqliteRepository { .ok_or(RepositoryError::NotFound) } - #[tracing::instrument(skip_all, name = "subscriptions.delete", fields(id = %id))] - async fn delete(&self, id: i64) -> Result<(), RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError> { let result = sqlx::query!("DELETE FROM subscriptions WHERE id = ?", id) .execute(&self.pool) .await @@ -391,12 +365,8 @@ impl SubscriptionRepository for SqliteRepository { Ok(()) } - #[tracing::instrument( - skip_all, - name = "subscriptions.get_branch_id_by_subscription_id", - fields(id = %id) - )] - async fn get_branch_id_by_subscription_id( + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_get_branch_id_by_subscription_id( &self, id: i64, ) -> Result, RepositoryError> { @@ -406,12 +376,8 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument( - skip_all, - name = "subscriptions.count_subscriptions_by_branch_id", - fields(branch_id = %branch_id) - )] - async fn count_subscriptions_by_branch_id( + #[tracing::instrument(skip_all, fields(branch_id = %branch_id))] + async fn subscriptions_count_subscriptions_by_branch_id( &self, branch_id: i64, ) -> Result { @@ -424,8 +390,8 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "subscriptions.delete_and_cascade", fields(id = %id))] - async fn delete_subscription_and_cascade(&self, id: i64) -> Result<(), RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn subscriptions_delete_and_cascade(&self, id: i64) -> Result<(), RepositoryError> { self.run_in_transaction(|tx| { Box::pin(async move { let branch_id = sqlx::query_scalar!( @@ -461,16 +427,16 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { - #[tracing::instrument(skip_all, name = "trigger_queue.get_all")] - async fn get_all(&self) -> Result, RepositoryError> { + #[tracing::instrument(skip_all)] + async fn trigger_queue_get_all(&self) -> Result, RepositoryError> { sqlx::query_as::<_, TriggerQueueItem>("SELECT * FROM trigger_queue") .fetch_all(&self.pool) .await .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, name = "trigger_queue.delete_by_id", fields(id = %id))] - async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { + #[tracing::instrument(skip_all, fields(id = %id))] + async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { sqlx::query!("DELETE FROM trigger_queue WHERE id = ?", id) .execute(&self.pool) .await @@ -478,8 +444,8 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument(skip_all, name = "trigger_queue.mark_processing")] - async fn find_oldest_pending_and_mark_processing( + #[tracing::instrument(skip_all)] + async fn trigger_queue_process_oldest_pending( &self, ) -> Result, RepositoryError> { let trigger = sqlx::query_as::<_, TriggerQueueItem>( @@ -501,10 +467,12 @@ impl TriggerRepository for SqliteRepository { #[tracing::instrument( skip_all, - name = "trigger_queue.update_retry_status", fields(id = %params.id, retry_count = %params.retry_count) )] - async fn update_retry_status(&self, params: UpdateRetryStatus) -> Result<(), RepositoryError> { + async fn trigger_queue_update_retry_status( + &self, + params: UpdateRetryStatus, + ) -> Result<(), RepositoryError> { let next_retry_count = params.retry_count + 1; if next_retry_count as u32 >= params.max_attempts { @@ -531,12 +499,11 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument( - skip_all, - name = "trigger_queue.recover_stuck_tasks", - fields(threshold_seconds = %threshold_seconds) - )] - async fn recover_stuck_tasks(&self, threshold_seconds: u64) -> Result<(), RepositoryError> { + #[tracing::instrument(skip_all, fields(threshold_seconds = %threshold_seconds))] + async fn trigger_queue_recover_stuck_tasks( + &self, + threshold_seconds: u64, + ) -> Result<(), RepositoryError> { let threshold_str = format!("-{} seconds", threshold_seconds); sqlx::query!( @@ -552,12 +519,8 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument( - skip_all, - name = "trigger_queue.queue_for_branch", - fields(branch_id = %params.branch_id) - )] - async fn queue_triggers_for_branch( + #[tracing::instrument(skip_all, fields(branch_id = %params.branch_id))] + async fn trigger_queue_queue_for_branch( &self, params: crate::repository::trigger::QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, diff --git a/src/repository/subscription.rs b/src/repository/subscription.rs index 453edd4..bc2c16e 100644 --- a/src/repository/subscription.rs +++ b/src/repository/subscription.rs @@ -9,22 +9,25 @@ use async_trait::async_trait; #[async_trait] pub trait SubscriptionRepository: Send + Sync { /// Creates a new subscription and returns it. - async fn create( + async fn subscriptions_create( &self, subscription: &CreateSubscription, ) -> Result; /// Returns the subscription with the given id. - async fn get_by_id(&self, id: i64) -> Result, RepositoryError>; + async fn subscriptions_get_by_id( + &self, + id: i64, + ) -> Result, RepositoryError>; /// Returns the subscription with the given id with its branch information. - async fn get_by_id_with_branch( + async fn subscriptions_get_by_id_with_branch( &self, id: i64, ) -> Result, RepositoryError>; /// Returns the subscription with the given keys with its branch information. - async fn get_by_keys_with_branch( + async fn subscriptions_get_by_keys_with_branch( &self, branch_id: i64, target_repo: &TargetRepo, @@ -35,44 +38,37 @@ pub trait SubscriptionRepository: Send + Sync { /// /// `last_id` is the last subscription ID that is going to be excluded, /// while `limit` is the number of subscriptions to show. - async fn list_paginated( - &self, - last_id: i64, - limit: i64, - ) -> Result, RepositoryError>; - - /// Lists some subscriptions with their branch information. - async fn list_paginated_with_branches( + async fn subscriptions_list_paginated( &self, last_id: i64, limit: i64, ) -> Result, RepositoryError>; /// Counts the remaining subscriptions after `last_id`. - async fn count_remaining(&self, last_id: i64) -> Result; + async fn subscriptions_count_remaining(&self, last_id: i64) -> Result; /// Updates the subscription with the given id. - async fn update( + async fn subscriptions_update( &self, id: i64, subscription: &UpdateSubscription, ) -> Result; /// Deletes the subscription with the given id. - async fn delete(&self, id: i64) -> Result<(), RepositoryError>; + async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError>; /// Returns the branch ID associated to the given subscription's `id`. - async fn get_branch_id_by_subscription_id( + async fn subscriptions_get_branch_id_by_subscription_id( &self, id: i64, ) -> Result, RepositoryError>; /// Counts the number of subscriptions associated to the given `branch_id`. - async fn count_subscriptions_by_branch_id( + async fn subscriptions_count_subscriptions_by_branch_id( &self, branch_id: i64, ) -> Result; /// Deletes a subscription and cascades deletion to the associated branch if no other subscriptions exist. - async fn delete_subscription_and_cascade(&self, id: i64) -> Result<(), RepositoryError>; + async fn subscriptions_delete_and_cascade(&self, id: i64) -> Result<(), RepositoryError>; } diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index f6a9524..140073c 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -37,24 +37,30 @@ pub struct QueueTriggersParams<'a> { #[async_trait] pub trait TriggerRepository: Send + Sync { /// Returns all the trigger queue items. - async fn get_all(&self) -> Result, RepositoryError>; + async fn trigger_queue_get_all(&self) -> Result, RepositoryError>; /// Finds the oldest pending trigger queue item and marks it as processing in a transaction. - async fn find_oldest_pending_and_mark_processing( + async fn trigger_queue_process_oldest_pending( &self, ) -> Result, RepositoryError>; /// Schedules a retry or marks the trigger as failed if max attempts is reached. - async fn update_retry_status(&self, params: UpdateRetryStatus) -> Result<(), RepositoryError>; + async fn trigger_queue_update_retry_status( + &self, + params: UpdateRetryStatus, + ) -> Result<(), RepositoryError>; /// Recovers tasks that have been stuck in `PROCESSING` for too long. - async fn recover_stuck_tasks(&self, threshold_seconds: u64) -> Result<(), RepositoryError>; + async fn trigger_queue_recover_stuck_tasks( + &self, + threshold_seconds: u64, + ) -> Result<(), RepositoryError>; /// Deletes the trigger queue item with the given `id`. - async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; + async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; /// Queues trigger events for all subscriptions of a branch. - async fn queue_triggers_for_branch( + async fn trigger_queue_queue_for_branch( &self, params: QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index ff1d900..97dbf1c 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -65,7 +65,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro let Some(trigger) = engine .ctx .repository - .find_oldest_pending_and_mark_processing() + .trigger_queue_process_oldest_pending() .await? else { return Ok(()); @@ -81,14 +81,22 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro let dispatch_result = dispatch_events(engine, &trigger).await; match dispatch_result { Ok(_) => { - TriggerRepository::delete_by_id(&*engine.ctx.repository, trigger.id).await?; + engine + .ctx + .repository + .trigger_queue_delete_by_id(trigger.id) + .await?; } Err(WorkflowTriggerError::Repository(crate::repository::RepositoryError::NotFound)) => { warn!( "Subscription for branch ID {} and target repo {} was not found (likely deleted). Deleting trigger task {} from queue.", trigger.branch_id, trigger.target_repo, trigger.id ); - TriggerRepository::delete_by_id(&*engine.ctx.repository, trigger.id).await?; + engine + .ctx + .repository + .trigger_queue_delete_by_id(trigger.id) + .await?; } Err(e) => { warn!("Dispatch failed: {e}"); @@ -126,7 +134,7 @@ async fn schedule_retry( engine .ctx .repository - .update_retry_status(UpdateRetryStatus { + .trigger_queue_update_retry_status(UpdateRetryStatus { id: trigger.id, retry_count: trigger.retry_count, max_attempts, @@ -145,7 +153,8 @@ pub async fn recover_stuck_tasks( ) -> Result<(), crate::repository::RepositoryError> { let threshold_seconds = config.engine.stuck_task_threshold.as_secs(); - repo.recover_stuck_tasks(threshold_seconds).await?; + repo.trigger_queue_recover_stuck_tasks(threshold_seconds) + .await?; Ok(()) } @@ -161,7 +170,11 @@ pub async fn dispatch_events( let sub_with_branch = engine .ctx .repository - .get_by_keys_with_branch(trigger.branch_id, &trigger.target_repo, &trigger.event_type) + .subscriptions_get_by_keys_with_branch( + trigger.branch_id, + &trigger.target_repo, + &trigger.event_type, + ) .await? .ok_or_else(|| { WorkflowTriggerError::Repository(crate::repository::RepositoryError::NotFound) @@ -404,7 +417,7 @@ mod tests { let repo = crate::repository::SqliteRepository::new(pool.clone()); let trigger = repo - .find_oldest_pending_and_mark_processing() + .trigger_queue_process_oldest_pending() .await .unwrap() .unwrap(); From 5c2819e6be86ada371354c8b0d96eb63e6e378f6 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 5 Aug 2026 11:47:33 +0200 Subject: [PATCH 16/50] Remove unused repository methods --- src/handler.rs | 5 +- src/polling/mod.rs | 8 +-- src/repository/branch.rs | 13 ----- src/repository/sqlite.rs | 102 --------------------------------- src/repository/subscription.rs | 23 +------- src/repository/trigger.rs | 3 - 6 files changed, 4 insertions(+), 150 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 3f95679..712a299 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -285,10 +285,7 @@ async fn delete_subscription_inner( State(state): State, Path(id): Path, ) -> Result<(), HandlerError> { - state - .repository - .subscriptions_delete_and_cascade(id) - .await?; + state.repository.subscriptions_delete(id).await?; Ok(()) } diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 6b442c9..8578a5b 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -134,12 +134,8 @@ async fn process_single_branch( branch_info: &branch::BranchInfo, tx: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { - repo.branches_update_last_commit_hash_in_tx( - branch_info.branch.id, - &branch_info.latest_hash, - tx, - ) - .await?; + repo.branches_update_last_commit_hash(branch_info.branch.id, &branch_info.latest_hash, tx) + .await?; info!( "New commit detected for branch {}. Hash: {}", diff --git a/src/repository/branch.rs b/src/repository/branch.rs index 870eea1..92f8527 100644 --- a/src/repository/branch.rs +++ b/src/repository/branch.rs @@ -10,24 +10,11 @@ pub trait BranchRepository: Send + Sync { /// Returns all branches. async fn branches_get_all(&self) -> Result, RepositoryError>; - /// Returns the branch with the given `id`. - async fn branches_find_by_id(&self, id: i64) -> Result, RepositoryError>; - - /// Deletes the branch with the given `id`. - async fn branches_delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; - /// Updates the last commit hash of the branch. async fn branches_update_last_commit_hash( &self, id: i64, hash: &crate::domain::CommitHash, - ) -> Result<(), RepositoryError>; - - /// Updates the last commit hash of the branch within a transaction. - async fn branches_update_last_commit_hash_in_tx( - &self, - id: i64, - hash: &crate::domain::CommitHash, tx: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError>; } diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index b6d1fec..24929ac 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -28,11 +28,6 @@ impl SqliteRepository { Self { pool } } - /// Returns the stored [`SqlitePool`]. - pub fn get_pool(&self) -> &SqlitePool { - &self.pool - } - /// Runs a closure within a transaction. #[tracing::instrument(skip_all)] pub async fn run_in_transaction<'a, F, T, E>(&self, f: F) -> Result @@ -58,50 +53,11 @@ impl BranchRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, fields(id = %id))] - async fn branches_find_by_id(&self, id: i64) -> Result, RepositoryError> { - sqlx::query_as::<_, Branch>("SELECT * FROM branches WHERE id = ?") - .bind(id) - .fetch_optional(&self.pool) - .await - .map_err(RepositoryError::Database) - } - - #[tracing::instrument(skip_all, fields(id = %id))] - async fn branches_delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { - let result = sqlx::query!("DELETE FROM branches WHERE id = ?", id) - .execute(&self.pool) - .await - .map_err(RepositoryError::Database)?; - - if result.rows_affected() == 0 { - return Err(RepositoryError::NotFound); - } - Ok(()) - } - #[tracing::instrument(skip_all, fields(id = %id))] async fn branches_update_last_commit_hash( &self, id: i64, hash: &crate::domain::CommitHash, - ) -> Result<(), RepositoryError> { - sqlx::query!( - "UPDATE branches SET last_commit_hash = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", - hash, - id - ) - .execute(&self.pool) - .await - .map_err(RepositoryError::Database)?; - Ok(()) - } - - #[tracing::instrument(skip_all, fields(id = %id))] - async fn branches_update_last_commit_hash_in_tx( - &self, - id: i64, - hash: &crate::domain::CommitHash, tx: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { sqlx::query!( @@ -161,18 +117,6 @@ impl SubscriptionRepository for SqliteRepository { }) } - #[tracing::instrument(skip_all, fields(id = %id))] - async fn subscriptions_get_by_id( - &self, - id: i64, - ) -> Result, RepositoryError> { - sqlx::query_as::<_, Subscription>("SELECT * FROM subscriptions WHERE id = ?") - .bind(id) - .fetch_optional(&self.pool) - .await - .map_err(RepositoryError::Database) - } - #[tracing::instrument(skip_all, fields(id = %id))] async fn subscriptions_get_by_id_with_branch( &self, @@ -354,44 +298,6 @@ impl SubscriptionRepository for SqliteRepository { #[tracing::instrument(skip_all, fields(id = %id))] async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError> { - let result = sqlx::query!("DELETE FROM subscriptions WHERE id = ?", id) - .execute(&self.pool) - .await - .map_err(RepositoryError::Database)?; - - if result.rows_affected() == 0 { - return Err(RepositoryError::NotFound); - } - Ok(()) - } - - #[tracing::instrument(skip_all, fields(id = %id))] - async fn subscriptions_get_branch_id_by_subscription_id( - &self, - id: i64, - ) -> Result, RepositoryError> { - sqlx::query_scalar!("SELECT branch_id FROM subscriptions WHERE id = ?", id) - .fetch_optional(&self.pool) - .await - .map_err(RepositoryError::Database) - } - - #[tracing::instrument(skip_all, fields(branch_id = %branch_id))] - async fn subscriptions_count_subscriptions_by_branch_id( - &self, - branch_id: i64, - ) -> Result { - sqlx::query_scalar!( - "SELECT COUNT(*) FROM subscriptions WHERE branch_id = ?", - branch_id - ) - .fetch_one(&self.pool) - .await - .map_err(RepositoryError::Database) - } - - #[tracing::instrument(skip_all, fields(id = %id))] - async fn subscriptions_delete_and_cascade(&self, id: i64) -> Result<(), RepositoryError> { self.run_in_transaction(|tx| { Box::pin(async move { let branch_id = sqlx::query_scalar!( @@ -427,14 +333,6 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { - #[tracing::instrument(skip_all)] - async fn trigger_queue_get_all(&self) -> Result, RepositoryError> { - sqlx::query_as::<_, TriggerQueueItem>("SELECT * FROM trigger_queue") - .fetch_all(&self.pool) - .await - .map_err(RepositoryError::Database) - } - #[tracing::instrument(skip_all, fields(id = %id))] async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { sqlx::query!("DELETE FROM trigger_queue WHERE id = ?", id) diff --git a/src/repository/subscription.rs b/src/repository/subscription.rs index bc2c16e..37c90ef 100644 --- a/src/repository/subscription.rs +++ b/src/repository/subscription.rs @@ -14,12 +14,6 @@ pub trait SubscriptionRepository: Send + Sync { subscription: &CreateSubscription, ) -> Result; - /// Returns the subscription with the given id. - async fn subscriptions_get_by_id( - &self, - id: i64, - ) -> Result, RepositoryError>; - /// Returns the subscription with the given id with its branch information. async fn subscriptions_get_by_id_with_branch( &self, @@ -54,21 +48,6 @@ pub trait SubscriptionRepository: Send + Sync { subscription: &UpdateSubscription, ) -> Result; - /// Deletes the subscription with the given id. - async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError>; - - /// Returns the branch ID associated to the given subscription's `id`. - async fn subscriptions_get_branch_id_by_subscription_id( - &self, - id: i64, - ) -> Result, RepositoryError>; - - /// Counts the number of subscriptions associated to the given `branch_id`. - async fn subscriptions_count_subscriptions_by_branch_id( - &self, - branch_id: i64, - ) -> Result; - /// Deletes a subscription and cascades deletion to the associated branch if no other subscriptions exist. - async fn subscriptions_delete_and_cascade(&self, id: i64) -> Result<(), RepositoryError>; + async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError>; } diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index 140073c..9535a6b 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -36,9 +36,6 @@ pub struct QueueTriggersParams<'a> { /// Interface for `trigger_queue` table operations. #[async_trait] pub trait TriggerRepository: Send + Sync { - /// Returns all the trigger queue items. - async fn trigger_queue_get_all(&self) -> Result, RepositoryError>; - /// Finds the oldest pending trigger queue item and marks it as processing in a transaction. async fn trigger_queue_process_oldest_pending( &self, From f59545b908dceb4615c5a9576a6c902684fe5107 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 5 Aug 2026 11:57:15 +0200 Subject: [PATCH 17/50] Rename `trigger_queue_queue_for_branch` to `trigger_queue_upsert` --- src/polling/mod.rs | 3 +-- src/repository/sqlite.rs | 2 +- src/repository/trigger.rs | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 8578a5b..bf1c92d 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -149,8 +149,7 @@ async fn process_single_branch( new_hash: &branch_info.latest_hash, span_context: span_context.as_deref(), }; - repo.trigger_queue_queue_for_branch(trigger_params, tx) - .await?; + repo.trigger_queue_upsert(trigger_params, tx).await?; Ok(()) } diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index 24929ac..49dfee0 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -418,7 +418,7 @@ impl TriggerRepository for SqliteRepository { } #[tracing::instrument(skip_all, fields(branch_id = %params.branch_id))] - async fn trigger_queue_queue_for_branch( + async fn trigger_queue_upsert( &self, params: crate::repository::trigger::QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index 9535a6b..39f8428 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -57,7 +57,7 @@ pub trait TriggerRepository: Send + Sync { async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; /// Queues trigger events for all subscriptions of a branch. - async fn trigger_queue_queue_for_branch( + async fn trigger_queue_upsert( &self, params: QueueTriggersParams<'_>, executor: &mut sqlx::SqliteConnection, From 1af40e0c14193eb1de31069288f4b1b1adec1c9f Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 5 Aug 2026 14:33:44 +0200 Subject: [PATCH 18/50] Rename other objects - `trigger_queue_delete_by_id` -> `trigger_queue_delete` - `QueueTriggersParams` -> `TriggerQueueUpsertParams` --- src/polling/mod.rs | 2 +- src/repository/sqlite.rs | 4 ++-- src/repository/trigger.rs | 8 ++++---- src/trigger/mod.rs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/polling/mod.rs b/src/polling/mod.rs index bf1c92d..3e21653 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -144,7 +144,7 @@ async fn process_single_branch( let span_context = crate::telemetry::serialize_current_span_context(); - let trigger_params = crate::repository::trigger::QueueTriggersParams { + let trigger_params = crate::repository::trigger::TriggerQueueUpsertParams { branch_id: branch_info.branch.id, new_hash: &branch_info.latest_hash, span_context: span_context.as_deref(), diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index 49dfee0..249de8b 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -334,7 +334,7 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { #[tracing::instrument(skip_all, fields(id = %id))] - async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { + async fn trigger_queue_delete(&self, id: i64) -> Result<(), RepositoryError> { sqlx::query!("DELETE FROM trigger_queue WHERE id = ?", id) .execute(&self.pool) .await @@ -420,7 +420,7 @@ impl TriggerRepository for SqliteRepository { #[tracing::instrument(skip_all, fields(branch_id = %params.branch_id))] async fn trigger_queue_upsert( &self, - params: crate::repository::trigger::QueueTriggersParams<'_>, + params: crate::repository::trigger::TriggerQueueUpsertParams<'_>, executor: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { let branch_id = params.branch_id; diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index 39f8428..8f8be20 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -20,9 +20,9 @@ pub struct UpdateRetryStatus { pub backoff_base_secs: u64, } -/// Parameters for queueing triggers for a branch. +/// Parameters for upserting trigger events for a branch. #[derive(Debug, Clone)] -pub struct QueueTriggersParams<'a> { +pub struct TriggerQueueUpsertParams<'a> { /// The unique identifier of the branch. pub branch_id: i64, @@ -54,12 +54,12 @@ pub trait TriggerRepository: Send + Sync { ) -> Result<(), RepositoryError>; /// Deletes the trigger queue item with the given `id`. - async fn trigger_queue_delete_by_id(&self, id: i64) -> Result<(), RepositoryError>; + async fn trigger_queue_delete(&self, id: i64) -> Result<(), RepositoryError>; /// Queues trigger events for all subscriptions of a branch. async fn trigger_queue_upsert( &self, - params: QueueTriggersParams<'_>, + params: TriggerQueueUpsertParams<'_>, executor: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError>; } diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 97dbf1c..343a95c 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -84,7 +84,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro engine .ctx .repository - .trigger_queue_delete_by_id(trigger.id) + .trigger_queue_delete(trigger.id) .await?; } Err(WorkflowTriggerError::Repository(crate::repository::RepositoryError::NotFound)) => { @@ -95,7 +95,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro engine .ctx .repository - .trigger_queue_delete_by_id(trigger.id) + .trigger_queue_delete(trigger.id) .await?; } Err(e) => { From 0a58e9fb0bf832fa406613e02a0c715c8b332b94 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 6 Aug 2026 10:14:14 +0200 Subject: [PATCH 19/50] Instrument `request_installation_token` --- src/model.rs | 4 +++- src/trigger/auth.rs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/model.rs b/src/model.rs index f4c2e27..b15bae2 100644 --- a/src/model.rs +++ b/src/model.rs @@ -46,7 +46,7 @@ pub struct Branch { } /// Represents a row in the `subscriptions` table. -#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Clone)] +#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Clone, Valuable)] pub struct Subscription { /// Unique database primary key. pub id: i64, @@ -72,9 +72,11 @@ pub struct Subscription { pub gh_app_installation_id: i64, /// Timestamp when the record was created. + #[valuable(skip)] pub created_at: DateTime, /// Timestamp when the record was updated. + #[valuable(skip)] pub updated_at: DateTime, } diff --git a/src/trigger/auth.rs b/src/trigger/auth.rs index 5b60151..fa9ccbd 100644 --- a/src/trigger/auth.rs +++ b/src/trigger/auth.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use tracing::info; +use tracing::{field::valuable, info}; use crate::{ config::Config, @@ -33,6 +33,7 @@ pub struct GitHubAuthenticator { #[async_trait] impl Authenticator for GitHubAuthenticator { + #[tracing::instrument(skip_all, fields(subscription = valuable(subscription)))] async fn request_installation_token( &self, subscription: &Subscription, From 581cededa1248e2030c9bb9d215a9de10140b6af Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 6 Aug 2026 10:49:23 +0200 Subject: [PATCH 20/50] Add instrumentation to other functions - `auth_middleware` - `health_check` - `get_latest_hash` --- src/lib.rs | 12 ++++++++++++ src/polling/git.rs | 1 + 2 files changed, 13 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index ded25ea..cea6d01 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,6 +196,14 @@ fn init_engines(ctx: &SharedContext, http_client: Client) -> Result, req: Request, @@ -208,8 +216,11 @@ async fn auth_middleware( let auth_header = req.headers().get("X-API-KEY").and_then(|v| v.to_str().ok()); if !verify_api_key(state.config.auth.api_key.as_ref(), auth_header) { + tracing::Span::current().record("authenticated", false); return StatusCode::UNAUTHORIZED.into_response(); } + + tracing::Span::current().record("authenticated", true); } next.run(req).await @@ -252,6 +263,7 @@ async fn set_no_cache_header(req: Request, next: Next) -> Response { mod health_handler { use super::*; #[rovo] + #[tracing::instrument(skip_all)] pub async fn health_check(State(_state): State) -> &'static str { "CommitBridge is alive" } diff --git a/src/polling/git.rs b/src/polling/git.rs index 06cbed7..5f7bef6 100644 --- a/src/polling/git.rs +++ b/src/polling/git.rs @@ -44,6 +44,7 @@ impl MainGitFetcher { #[async_trait] impl GitFetcher for MainGitFetcher { + #[tracing::instrument(skip_all, fields(repo_url = %repo_url, branch = %branch))] async fn get_latest_hash( &self, repo_url: &str, From 5f2ea5207e80446466f088def6f747f07514442a Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 6 Aug 2026 12:15:07 +0200 Subject: [PATCH 21/50] Fix various issues - `TelemetryGuard` preserved in independently of `run_app` outcome - Disabled unused features from OpenTelemetry dependencies - Skipped `span_context` from telemetry spans --- Cargo.lock | 17 ++--------------- Cargo.toml | 4 ++-- src/lib.rs | 18 +++--------------- src/main.rs | 24 ++++++++++++++++-------- src/model.rs | 1 + 5 files changed, 24 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 519bb73..a92bd7e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2918,19 +2918,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "opentelemetry-http" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5683015d09e2df236ef005b17f6f196f0d5f6313c4fa43a7b6a53b52776e4331" -dependencies = [ - "async-trait", - "bytes", - "http", - "opentelemetry", - "reqwest", -] - [[package]] name = "opentelemetry-otlp" version = "0.32.0" @@ -2939,11 +2926,9 @@ checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" dependencies = [ "http", "opentelemetry", - "opentelemetry-http", "opentelemetry-proto", "opentelemetry_sdk", "prost", - "reqwest", "thiserror", "tokio", "tonic", @@ -2977,6 +2962,8 @@ dependencies = [ "portable-atomic", "rand 0.9.5", "thiserror", + "tokio", + "tokio-stream", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 66adc21..870dc36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,8 +27,8 @@ http = "1.4.2" humantime-serde = "1.1.1" jsonwebtoken = { version = "10.4.0", features = ["aws_lc_rs"] } opentelemetry = "0.32.0" -opentelemetry-otlp = { version = "0.32.0", features = ["trace", "grpc-tonic"] } -opentelemetry_sdk = "0.32.1" +opentelemetry-otlp = { version = "0.32.0", default-features = false, features = ["trace", "grpc-tonic"] } +opentelemetry_sdk = { version = "0.32.1", features = ["rt-tokio"] } reqwest = { version = "0.13.4", features = ["json"] } rovo = { version = "0.4.8", features = ["scalar"] } schemars = { version = "0.9", features = ["chrono04"] } diff --git a/src/lib.rs b/src/lib.rs index cea6d01..fc7dd7e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,19 +69,7 @@ pub mod trigger; type EngineTask = (Box, &'static str); /// Runs the server, delegating errors to the caller. -/// -/// Initializes telemetry on entry and returns a [`telemetry::TelemetryGuard`] -/// that must be held until all spawned tasks have terminated, -/// so that spans are flushed before the tracer provider is shut down. -pub async fn run_app( - tracker: &TaskTracker, - token: &CancellationToken, -) -> Result { - // Load `.env` before telemetry initialization, so that OTEL environment - // variables are visible when the tracer provider checks for them. - let dotenv_loaded = dotenvy::dotenv().is_ok(); - let tracer_guard = crate::telemetry::init(); - log_dotenv_status(dotenv_loaded); +pub async fn run_app(tracker: &TaskTracker, token: &CancellationToken) -> Result<(), FatalError> { let config = Config::load()?; let pool = init_database(&config).await?; let repository = std::sync::Arc::new(crate::repository::SqliteRepository::new(pool.clone())); @@ -107,13 +95,13 @@ pub async fn run_app( run_server(app, &ctx.config, token.clone()).await?; - Ok(tracer_guard) + Ok(()) } /// Logs the outcome of the `.env` file load. /// /// Must only be called after the tracing subscriber is initialized. -fn log_dotenv_status(loaded: bool) { +pub fn log_dotenv_status(loaded: bool) { if !loaded { return; } diff --git a/src/main.rs b/src/main.rs index 360e5fc..94fc3e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,8 @@ clippy::indexing_slicing )] -use commit_bridge::run_app; +use commit_bridge::{log_dotenv_status, run_app, telemetry}; +use dotenvy::dotenv; use tokio_util::{sync::CancellationToken, task::TaskTracker}; use tracing::{error, info}; @@ -17,13 +18,20 @@ async fn main() { let tracker = TaskTracker::new(); let token = CancellationToken::new(); - match run_app(&tracker, &token).await { - Ok(_tracer_guard) => { - token.cancel(); - tracker.close(); - tracker.wait().await; - info!("All systems terminated. Terminating process."); - } + let dotenv_loaded = dotenv().is_ok(); + let tracer_guard = telemetry::init(); + log_dotenv_status(dotenv_loaded); + + let result = run_app(&tracker, &token).await; + + token.cancel(); + tracker.close(); + tracker.wait().await; + + match result { + Ok(()) => info!("All systems terminated. Terminating process."), Err(e) => error!("{e}"), } + + drop(tracer_guard); } diff --git a/src/model.rs b/src/model.rs index b15bae2..7e4662f 100644 --- a/src/model.rs +++ b/src/model.rs @@ -229,5 +229,6 @@ pub struct TriggerQueueItem { pub retry_count: i64, /// Serialized OpenTelemetry span context. + #[valuable(skip)] pub span_context: Option, } From 3e481a9232a7de0bbe5b329c686f405199c136c7 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Fri, 7 Aug 2026 11:32:54 +0200 Subject: [PATCH 22/50] Reduce span noise --- .env.example | 1 + src/telemetry.rs | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index a2450ff..b24d75e 100644 --- a/.env.example +++ b/.env.example @@ -50,3 +50,4 @@ CBRIDGE__GIT__REPO_PATH=/app/data/git_repo # and only enabled when an OTLP endpoint is explicitly configured, # e.g. for a local Jaeger instance: # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 +# RUST_LOG=commit_bridge=info diff --git a/src/telemetry.rs b/src/telemetry.rs index 589f13d..9bef39b 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,6 +1,6 @@ //! OpenTelemetry telemetry helpers. -use opentelemetry::global; +use opentelemetry::{global, trace::TracerProvider}; use opentelemetry_sdk::trace::SdkTracerProvider; use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; @@ -81,16 +81,19 @@ impl Drop for TelemetryGuard { /// Sets up the global OpenTelemetry propagator and tracing subscriber. pub fn init() -> TelemetryGuard { + const TRACER_NAME: &'static str = "commit-bridge"; + const DEFAULT_RUST_LOG: &'static str = "commit_bridge=info"; + global::set_text_map_propagator(opentelemetry_sdk::propagation::TraceContextPropagator::new()); let tracer_provider = init_tracer_provider(); let otel_layer = tracer_provider.as_ref().map(|provider| { - let tracer = opentelemetry::trace::TracerProvider::tracer(provider, "commit-bridge"); + let tracer = provider.tracer(TRACER_NAME); tracing_opentelemetry::layer().with_tracer(tracer) }); - let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(DEFAULT_RUST_LOG)); tracing_subscriber::registry() .with(env_filter) From d87eff1f1e1ba224826754160eb67d6521abb952 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Fri, 7 Aug 2026 11:33:42 +0200 Subject: [PATCH 23/50] Add Tower HTTP server spans --- Cargo.lock | 1 + Cargo.toml | 2 +- src/lib.rs | 25 ++++++++++++++++++++++++- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a92bd7e..b07f7e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4691,6 +4691,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", "url", ] diff --git a/Cargo.toml b/Cargo.toml index 870dc36..38c7b93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,7 +40,7 @@ thiserror = "2.0.18" tokio = { version = "1.52.3", features = ["process", "rt-multi-thread", "signal"] } tokio-util = { version = "0.7.18", features = ["rt"] } tower = { version = "0.5.3", features = ["util"] } -tower-http = { version = "0.6", features = ["timeout"] } +tower-http = { version = "0.6", features = ["timeout", "trace"] } tracing = { version = "0.1.44", features = ["valuable"] } tracing-opentelemetry = "0.33.0" tracing-subscriber = { version = "0.3", features = ["valuable", "env-filter"] } diff --git a/src/lib.rs b/src/lib.rs index fc7dd7e..2d4ac34 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -30,7 +30,8 @@ use tokio::signal; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tower_http::timeout::TimeoutLayer; -use tracing::info; +use tower_http::trace::{MakeSpan, TraceLayer}; +use tracing::{Span, info}; use crate::{ config::Config, @@ -257,6 +258,27 @@ mod health_handler { } } +/// Span factory for incoming HTTP requests, +/// following OpenTelemetry semantic conventions. +/// +/// The span is created within this crate +/// so that it is picked up by the telemetry filter +/// (which only exports spans whose target starts with `commit_bridge`), +/// unlike the default `tower_http` span factory. +#[derive(Clone, Copy)] +struct HttpRequestSpan; + +impl MakeSpan for HttpRequestSpan { + fn make_span(&mut self, request: &Request) -> Span { + tracing::info_span!( + "http.request", + otel.kind = "server", + http.request.method = %request.method(), + url.path = %request.uri().path(), + ) + } +} + /// Builds the application router. pub fn build_router( repository: std::sync::Arc, @@ -299,6 +321,7 @@ pub fn build_router( StatusCode::REQUEST_TIMEOUT, config.server.in_request_timeout, )) + .layer(TraceLayer::new_for_http().make_span_with(HttpRequestSpan)) } /// Runs the server. From 69ddca26b7a61559c1483672696cbfb62246df1b Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 10 Aug 2026 09:56:17 +0200 Subject: [PATCH 24/50] Record status code on HTTP spans --- src/lib.rs | 28 ++++++++++++++++++++++++++-- src/telemetry.rs | 4 ++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2d4ac34..ba2bbe7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,7 @@ use std::fs; use std::str::FromStr; +use std::time::Duration; use axum::{ Router, @@ -30,7 +31,7 @@ use tokio::signal; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tower_http::timeout::TimeoutLayer; -use tower_http::trace::{MakeSpan, TraceLayer}; +use tower_http::trace::{MakeSpan, OnResponse, TraceLayer}; use tracing::{Span, info}; use crate::{ @@ -275,10 +276,29 @@ impl MakeSpan for HttpRequestSpan { otel.kind = "server", http.request.method = %request.method(), url.path = %request.uri().path(), + http.response.status_code = tracing::field::Empty, + otel.status_code = tracing::field::Empty, ) } } +/// Records HTTP response metadata onto the request span, +/// following OpenTelemetry semantic conventions. +/// +/// Must be used with [`HttpRequestSpan`], +/// which declares the fields recorded here. +#[derive(Clone, Copy)] +struct HttpRequestOnResponse; + +impl OnResponse for HttpRequestOnResponse { + fn on_response(self, response: &Response, _latency: Duration, span: &Span) { + span.record("http.response.status_code", response.status().as_u16()); + if response.status().is_client_error() || response.status().is_server_error() { + span.record("otel.status_code", "ERROR"); + } + } +} + /// Builds the application router. pub fn build_router( repository: std::sync::Arc, @@ -321,7 +341,11 @@ pub fn build_router( StatusCode::REQUEST_TIMEOUT, config.server.in_request_timeout, )) - .layer(TraceLayer::new_for_http().make_span_with(HttpRequestSpan)) + .layer( + TraceLayer::new_for_http() + .make_span_with(HttpRequestSpan) + .on_response(HttpRequestOnResponse), + ) } /// Runs the server. diff --git a/src/telemetry.rs b/src/telemetry.rs index 9bef39b..e6e8ab3 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -81,8 +81,8 @@ impl Drop for TelemetryGuard { /// Sets up the global OpenTelemetry propagator and tracing subscriber. pub fn init() -> TelemetryGuard { - const TRACER_NAME: &'static str = "commit-bridge"; - const DEFAULT_RUST_LOG: &'static str = "commit_bridge=info"; + const TRACER_NAME: &str = "commit-bridge"; + const DEFAULT_RUST_LOG: &str = "commit_bridge=info"; global::set_text_map_propagator(opentelemetry_sdk::propagation::TraceContextPropagator::new()); From 4ac78b508af2ee60be3296eda1a1ecee9a8d06ba Mon Sep 17 00:00:00 2001 From: Nilirad Date: Mon, 10 Aug 2026 11:43:34 +0200 Subject: [PATCH 25/50] Opt-in to marking client errors as such on OpenTelemetry exports --- .env.example | 1 + src/config.rs | 16 ++++++++++++++++ src/lib.rs | 28 +++++++++++++++++++++++++--- src/test_utils.rs | 3 +++ src/tests/mark_error_tests.rs | 21 +++++++++++++++++++++ src/tests/mod.rs | 1 + 6 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 src/tests/mark_error_tests.rs diff --git a/.env.example b/.env.example index b24d75e..df05b6f 100644 --- a/.env.example +++ b/.env.example @@ -51,3 +51,4 @@ CBRIDGE__GIT__REPO_PATH=/app/data/git_repo # e.g. for a local Jaeger instance: # OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 # RUST_LOG=commit_bridge=info +# CBRIDGE__TELEMETRY__MARK_CLIENT_ERRORS_AS_ERROR=true diff --git a/src/config.rs b/src/config.rs index ba6a676..39f48b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,6 +35,11 @@ pub struct Config { /// Git-related configuration settings. #[validate(nested)] pub git: GitConfig, + + /// Telemetry-related configuration settings. + #[serde(default)] + #[validate(nested)] + pub telemetry: TelemetryConfig, } impl Config { @@ -238,3 +243,14 @@ pub struct GitConfig { #[validate(custom(function = "validate_gix_repo_path"))] pub repo_path: PathBuf, } + +/// Configuration for telemetry and observability. +#[derive(Clone, Debug, Default, Deserialize, Validate)] +pub struct TelemetryConfig { + /// Mark client error responses (4xx) as errors in exported traces. + /// + /// `false` by default. + /// Server errors (5xx) are marked as errors anyways. + #[serde(default)] + pub mark_client_errors_as_error: bool, +} diff --git a/src/lib.rs b/src/lib.rs index ba2bbe7..ac3a464 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -288,12 +288,32 @@ impl MakeSpan for HttpRequestSpan { /// Must be used with [`HttpRequestSpan`], /// which declares the fields recorded here. #[derive(Clone, Copy)] -struct HttpRequestOnResponse; +pub(crate) struct HttpRequestOnResponse { + /// Whether client error responses (4xx) + /// should be marked as errors in exported traces. + mark_client_errors: bool, +} + +impl HttpRequestOnResponse { + /// Creates a new [`HttpRequestOnResponse`]. + pub(crate) const fn new(mark_client_errors: bool) -> Self { + Self { mark_client_errors } + } + + /// Returns `true` if the response status should be marked as an error + /// in exported traces. + /// + /// Server errors (5xx) are always marked; + /// client errors (4xx) are only marked when `mark_client_errors` is set. + pub(crate) fn should_mark_error(&self, status: StatusCode) -> bool { + status.is_server_error() || (self.mark_client_errors && status.is_client_error()) + } +} impl OnResponse for HttpRequestOnResponse { fn on_response(self, response: &Response, _latency: Duration, span: &Span) { span.record("http.response.status_code", response.status().as_u16()); - if response.status().is_client_error() || response.status().is_server_error() { + if self.should_mark_error(response.status()) { span.record("otel.status_code", "ERROR"); } } @@ -344,7 +364,9 @@ pub fn build_router( .layer( TraceLayer::new_for_http() .make_span_with(HttpRequestSpan) - .on_response(HttpRequestOnResponse), + .on_response(HttpRequestOnResponse::new( + config.telemetry.mark_client_errors_as_error, + )), ) } diff --git a/src/test_utils.rs b/src/test_utils.rs index 0a64bf6..6718f4f 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -98,5 +98,8 @@ pub fn create_test_config() -> crate::config::Config { git: crate::config::GitConfig { repo_path: PathBuf::from("test-git-repo"), }, + telemetry: crate::config::TelemetryConfig { + mark_client_errors_as_error: false, + }, } } diff --git a/src/tests/mark_error_tests.rs b/src/tests/mark_error_tests.rs new file mode 100644 index 0000000..d56309b --- /dev/null +++ b/src/tests/mark_error_tests.rs @@ -0,0 +1,21 @@ +use crate::HttpRequestOnResponse; +use axum::http::StatusCode; + +#[test] +fn test_should_mark_error_server_errors_always_marked() { + assert!(HttpRequestOnResponse::new(false).should_mark_error(StatusCode::INTERNAL_SERVER_ERROR)); + assert!(HttpRequestOnResponse::new(true).should_mark_error(StatusCode::BAD_GATEWAY)); +} + +#[test] +fn test_should_mark_error_client_errors_opt_in() { + assert!(!HttpRequestOnResponse::new(false).should_mark_error(StatusCode::NOT_FOUND)); + assert!(HttpRequestOnResponse::new(true).should_mark_error(StatusCode::NOT_FOUND)); +} + +#[test] +fn test_should_mark_error_success_and_redirects_never_marked() { + let on_response = HttpRequestOnResponse::new(true); + assert!(!on_response.should_mark_error(StatusCode::OK)); + assert!(!on_response.should_mark_error(StatusCode::MOVED_PERMANENTLY)); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b1d4ae5..53ab131 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,4 +9,5 @@ pub mod api_routes; pub mod auth_tests; pub mod config_tests; +pub mod mark_error_tests; pub mod polling_tests; From 0c35d65a84983d58633ac7ff21a20724e9333b16 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 11 Aug 2026 13:03:53 +0200 Subject: [PATCH 26/50] Assing a Span Kind to each span --- src/handler.rs | 10 ++++----- src/lib.rs | 3 ++- src/polling/git.rs | 5 ++++- src/polling/mod.rs | 7 ++++--- src/repository/sqlite.rs | 44 ++++++++++++++++++++++++++-------------- src/trigger/auth.rs | 5 ++++- src/trigger/mod.rs | 11 +++++----- 7 files changed, 54 insertions(+), 31 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 712a299..27f2fa6 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -57,7 +57,7 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(payload = valuable(&*payload)))] +#[instrument(skip_all, fields(otel.kind = "internal", payload = valuable(&*payload)))] pub async fn create_subscription( state: State, payload: Json, @@ -113,7 +113,7 @@ pub struct ListSubscriptionsQuery { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(query = valuable(&*query)))] +#[instrument(skip_all, fields(otel.kind = "internal", query = valuable(&*query)))] pub async fn list_subscriptions( state: State, query: Query, @@ -183,7 +183,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn get_subscription( state: State, Path(id): Path, @@ -226,7 +226,7 @@ async fn get_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = %id, payload = valuable(&*payload)))] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id, payload = valuable(&*payload)))] pub async fn update_subscription( state: State, Path(id): Path, @@ -272,7 +272,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn delete_subscription( state: State, Path(id): Path, diff --git a/src/lib.rs b/src/lib.rs index ac3a464..d0798ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -189,6 +189,7 @@ fn init_engines(ctx: &SharedContext, http_client: Client) -> Result, next: Next) -> Response { mod health_handler { use super::*; #[rovo] - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn health_check(State(_state): State) -> &'static str { "CommitBridge is alive" } diff --git a/src/polling/git.rs b/src/polling/git.rs index 5f7bef6..4536b5b 100644 --- a/src/polling/git.rs +++ b/src/polling/git.rs @@ -44,7 +44,10 @@ impl MainGitFetcher { #[async_trait] impl GitFetcher for MainGitFetcher { - #[tracing::instrument(skip_all, fields(repo_url = %repo_url, branch = %branch))] + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", repo_url = %repo_url, branch = %branch) + )] async fn get_latest_hash( &self, repo_url: &str, diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 3e21653..27da1ac 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -50,7 +50,7 @@ async fn polling_loop(ctx: SharedContext) { /// /// /// [`TriggerEngine`]: crate::trigger::TriggerEngine -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { let updated_branches = gather_updated_branches(ctx).await?; if updated_branches.is_empty() { @@ -69,7 +69,7 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { } /// Gathers stored branches that need to be updated. -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn gather_updated_branches(ctx: &SharedContext) -> Result, sqlx::Error> { let branches = ctx .repository @@ -109,7 +109,7 @@ fn execute_branch_updates<'a>( } /// Processes branch updates within a transaction. -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn process_branches( repo: std::sync::Arc, shared_branches: std::sync::Arc>, @@ -126,6 +126,7 @@ async fn process_branches( #[tracing::instrument( skip_all, fields( + otel.kind = "producer", branch_info = valuable(branch_info), ) )] diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index 249de8b..965892b 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -29,7 +29,7 @@ impl SqliteRepository { } /// Runs a closure within a transaction. - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn run_in_transaction<'a, F, T, E>(&self, f: F) -> Result where F: for<'b> FnOnce(&'b mut SqliteConnection) -> BoxFuture<'b, Result> + Send + 'a, @@ -45,7 +45,7 @@ impl SqliteRepository { #[async_trait] impl BranchRepository for SqliteRepository { - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, fields(otel.kind = "client"))] async fn branches_get_all(&self) -> Result, RepositoryError> { sqlx::query_as::<_, Branch>("SELECT * FROM branches") .fetch_all(&self.pool) @@ -53,7 +53,7 @@ impl BranchRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, fields(id = %id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] async fn branches_update_last_commit_hash( &self, id: i64, @@ -74,7 +74,7 @@ impl BranchRepository for SqliteRepository { #[async_trait] impl SubscriptionRepository for SqliteRepository { - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, fields(otel.kind = "client"))] async fn subscriptions_create( &self, subscription_payload: &CreateSubscription, @@ -117,7 +117,7 @@ impl SubscriptionRepository for SqliteRepository { }) } - #[tracing::instrument(skip_all, fields(id = %id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] async fn subscriptions_get_by_id_with_branch( &self, id: i64, @@ -159,7 +159,12 @@ impl SubscriptionRepository for SqliteRepository { #[tracing::instrument( skip_all, - fields(branch_id = %branch_id, target_repo = %target_repo, event_type = %event_type) + fields( + otel.kind = "client", + branch_id = %branch_id, + target_repo = %target_repo, + event_type = %event_type + ) )] async fn subscriptions_get_by_keys_with_branch( &self, @@ -204,7 +209,10 @@ impl SubscriptionRepository for SqliteRepository { } } - #[tracing::instrument(skip_all, fields(last_id = %last_id, limit = %limit))] + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", last_id = %last_id, limit = %limit) + )] async fn subscriptions_list_paginated( &self, last_id: i64, @@ -249,7 +257,7 @@ impl SubscriptionRepository for SqliteRepository { subscriptions } - #[tracing::instrument(skip_all, fields(last_id = %last_id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", last_id = %last_id))] async fn subscriptions_count_remaining(&self, last_id: i64) -> Result { sqlx::query_scalar!("SELECT COUNT(*) FROM subscriptions WHERE id > ?", last_id) .fetch_one(&self.pool) @@ -257,7 +265,7 @@ impl SubscriptionRepository for SqliteRepository { .map_err(RepositoryError::Database) } - #[tracing::instrument(skip_all, fields(id = %id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] async fn subscriptions_update( &self, id: i64, @@ -296,7 +304,7 @@ impl SubscriptionRepository for SqliteRepository { .ok_or(RepositoryError::NotFound) } - #[tracing::instrument(skip_all, fields(id = %id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] async fn subscriptions_delete(&self, id: i64) -> Result<(), RepositoryError> { self.run_in_transaction(|tx| { Box::pin(async move { @@ -333,7 +341,7 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { - #[tracing::instrument(skip_all, fields(id = %id))] + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] async fn trigger_queue_delete(&self, id: i64) -> Result<(), RepositoryError> { sqlx::query!("DELETE FROM trigger_queue WHERE id = ?", id) .execute(&self.pool) @@ -342,7 +350,7 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument(skip_all)] + #[tracing::instrument(skip_all, fields(otel.kind = "client"))] async fn trigger_queue_process_oldest_pending( &self, ) -> Result, RepositoryError> { @@ -365,7 +373,7 @@ impl TriggerRepository for SqliteRepository { #[tracing::instrument( skip_all, - fields(id = %params.id, retry_count = %params.retry_count) + fields(otel.kind = "client", id = %params.id, retry_count = %params.retry_count) )] async fn trigger_queue_update_retry_status( &self, @@ -397,7 +405,10 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument(skip_all, fields(threshold_seconds = %threshold_seconds))] + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", threshold_seconds = %threshold_seconds) + )] async fn trigger_queue_recover_stuck_tasks( &self, threshold_seconds: u64, @@ -417,7 +428,10 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - #[tracing::instrument(skip_all, fields(branch_id = %params.branch_id))] + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", branch_id = %params.branch_id) + )] async fn trigger_queue_upsert( &self, params: crate::repository::trigger::TriggerQueueUpsertParams<'_>, diff --git a/src/trigger/auth.rs b/src/trigger/auth.rs index fa9ccbd..c09530e 100644 --- a/src/trigger/auth.rs +++ b/src/trigger/auth.rs @@ -33,7 +33,10 @@ pub struct GitHubAuthenticator { #[async_trait] impl Authenticator for GitHubAuthenticator { - #[tracing::instrument(skip_all, fields(subscription = valuable(subscription)))] + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", subscription = valuable(subscription)) + )] async fn request_installation_token( &self, subscription: &Subscription, diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 343a95c..1adf2cf 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -58,6 +58,7 @@ async fn trigger_loop(engine: &TriggerEngine) { #[tracing::instrument( skip_all, fields( + otel.kind = "consumer", trigger = tracing::field::Empty ) )] @@ -108,7 +109,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro } /// Schedules the next retry for a trigger in the `trigger_queue`. -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "producer"))] async fn schedule_retry( engine: &TriggerEngine, trigger: TriggerQueueItem, @@ -146,7 +147,7 @@ async fn schedule_retry( } /// Recovers tasks that have been stuck in `PROCESSING` for too long. -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "producer"))] pub async fn recover_stuck_tasks( repo: &crate::repository::SqliteRepository, config: &crate::config::Config, @@ -162,7 +163,7 @@ pub async fn recover_stuck_tasks( /// /// /// [`Subscription`]: crate::model::Subscription -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn dispatch_events( engine: &TriggerEngine, trigger: &TriggerQueueItem, @@ -202,7 +203,7 @@ pub async fn dispatch_events( /// /// /// [`Subscription`]: crate::model::Subscription -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn notify_subscription( engine: &TriggerEngine, iat: String, @@ -217,7 +218,7 @@ async fn notify_subscription( /// /// /// [`Subscription`]: crate::model::Subscription -#[tracing::instrument(skip_all)] +#[tracing::instrument(skip_all, fields(otel.kind = "client"))] async fn send_repository_dispatch( engine: &TriggerEngine, iat: &str, From d30360676776fa8685bb87fa7b3f7194b82e602a Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 12 Aug 2026 11:09:00 +0200 Subject: [PATCH 27/50] Remove `valuable` `tracing-opentelemetry` does not currently support `valuable` --- .cargo/config.toml | 2 -- Cargo.lock | 40 --------------------------------------- Cargo.toml | 5 ++--- src/domain/branch_name.rs | 4 +--- src/domain/commit_hash.rs | 4 +--- src/domain/event_type.rs | 4 +--- src/domain/repo_url.rs | 4 +--- src/domain/target_repo.rs | 4 +--- src/handler.rs | 28 +++++++++++++++++++++------ src/model.rs | 16 +++++----------- src/polling/branch.rs | 2 -- src/polling/mod.rs | 8 ++++++-- src/trigger/auth.rs | 11 +++++++++-- src/trigger/mod.rs | 22 ++++++++++++++++++--- 14 files changed, 68 insertions(+), 86 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index a539230..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -rustflags = ["--cfg", "tracing_unstable"] diff --git a/Cargo.lock b/Cargo.lock index b07f7e8..3ef04d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -433,7 +433,6 @@ dependencies = [ "tracing-subscriber", "url", "validator", - "valuable", "wiremock", ] @@ -4767,18 +4766,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "tracing-serde" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" -dependencies = [ - "serde", - "tracing-core", - "valuable", - "valuable-serde", -] - [[package]] name = "tracing-subscriber" version = "0.3.23" @@ -4795,9 +4782,6 @@ dependencies = [ "tracing", "tracing-core", "tracing-log", - "tracing-serde", - "valuable", - "valuable-serde", ] [[package]] @@ -4955,30 +4939,6 @@ name = "valuable" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" -dependencies = [ - "valuable-derive", -] - -[[package]] -name = "valuable-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3a32a9bcc0f6c6ccfd5b27bcf298c58e753bcc9eeff268157a303393183a6d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "valuable-serde" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee0548edecd1b907be7e67789923b7d02275b9ba4a33ebc33300e2c947a8cb1" -dependencies = [ - "serde", - "valuable", -] [[package]] name = "vcpkg" diff --git a/Cargo.toml b/Cargo.toml index 38c7b93..21fdc66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,12 +41,11 @@ tokio = { version = "1.52.3", features = ["process", "rt-multi-thread", "signal" tokio-util = { version = "0.7.18", features = ["rt"] } tower = { version = "0.5.3", features = ["util"] } tower-http = { version = "0.6", features = ["timeout", "trace"] } -tracing = { version = "0.1.44", features = ["valuable"] } +tracing = "0.1.44" tracing-opentelemetry = "0.33.0" -tracing-subscriber = { version = "0.3", features = ["valuable", "env-filter"] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } -valuable = { version = "0.1.1", features = ["derive"] } [dev-dependencies] dudect-bencher = "0.7.0" diff --git a/src/domain/branch_name.rs b/src/domain/branch_name.rs index 017454a..e8770cc 100644 --- a/src/domain/branch_name.rs +++ b/src/domain/branch_name.rs @@ -4,12 +4,10 @@ use crate::error::ValidationError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; -use valuable::Valuable; /// The Git branch name. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(try_from = "String", into = "String")] -#[valuable(transparent)] pub struct BranchName(String); impl std::fmt::Display for BranchName { diff --git a/src/domain/commit_hash.rs b/src/domain/commit_hash.rs index 03fead0..115baa9 100644 --- a/src/domain/commit_hash.rs +++ b/src/domain/commit_hash.rs @@ -3,12 +3,10 @@ use crate::error::ValidationError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use valuable::Valuable; /// A git commit hash. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(try_from = "String", into = "String")] -#[valuable(transparent)] pub struct CommitHash(String); impl CommitHash { diff --git a/src/domain/event_type.rs b/src/domain/event_type.rs index 71136d4..5e9b6a5 100644 --- a/src/domain/event_type.rs +++ b/src/domain/event_type.rs @@ -4,12 +4,10 @@ use crate::error::ValidationError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; -use valuable::Valuable; /// The GitHub's `repository_dispatch` `event_type`. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Valuable)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(try_from = "String", into = "String")] -#[valuable(transparent)] pub struct EventType(String); impl EventType { diff --git a/src/domain/repo_url.rs b/src/domain/repo_url.rs index c90a547..408b60c 100644 --- a/src/domain/repo_url.rs +++ b/src/domain/repo_url.rs @@ -4,12 +4,10 @@ use crate::error::ValidationError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use validator::Validate; -use valuable::Valuable; /// The GitHub repository URL. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(try_from = "String", into = "String")] -#[valuable(transparent)] pub struct RepoUrl(String); impl std::fmt::Display for RepoUrl { diff --git a/src/domain/target_repo.rs b/src/domain/target_repo.rs index 6c4bacd..771a648 100644 --- a/src/domain/target_repo.rs +++ b/src/domain/target_repo.rs @@ -3,12 +3,10 @@ use crate::error::ValidationError; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use valuable::Valuable; /// The target GitHub repository in owner/repo format. -#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Valuable)] +#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] #[serde(try_from = "String", into = "String")] -#[valuable(transparent)] pub struct TargetRepo(String); impl TargetRepo { diff --git a/src/handler.rs b/src/handler.rs index 27f2fa6..cb621b8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -17,8 +17,7 @@ use axum::{ }; use rovo::rovo; use serde::Deserialize; -use tracing::{field::valuable, info, instrument}; -use valuable::Valuable; +use tracing::{info, instrument}; /// Maps a [`SubscriptionWithBranch`] to its HAL representation. fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { @@ -57,7 +56,14 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", payload = valuable(&*payload)))] +#[instrument(skip_all, fields( + otel.kind = "internal", + payload.source_repo_url = %payload.source_repo_url.as_str(), + payload.source_branch_name = %payload.source_branch_name.as_str(), + payload.target_repo = %payload.target_repo.as_str(), + payload.event_type = %payload.event_type.as_str(), + payload.gh_app_installation_id = %payload.gh_app_installation_id, +))] pub async fn create_subscription( state: State, payload: Json, @@ -84,7 +90,7 @@ async fn create_subscription_inner( } /// Query parameters for listing subscriptions. -#[derive(Valuable, Debug, Deserialize, schemars::JsonSchema)] +#[derive(Debug, Deserialize, schemars::JsonSchema)] pub struct ListSubscriptionsQuery { /// Maximum number of subscriptions to return. pub limit: Option, @@ -113,7 +119,11 @@ pub struct ListSubscriptionsQuery { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", query = valuable(&*query)))] +#[instrument(skip_all, fields( + otel.kind = "internal", + query.limit = ?query.limit, + query.last_id = ?query.last_id, +))] pub async fn list_subscriptions( state: State, query: Query, @@ -226,7 +236,13 @@ async fn get_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", id = %id, payload = valuable(&*payload)))] +#[instrument(skip_all, fields( + otel.kind = "internal", + id = %id, + payload.target_repo = ?payload.target_repo.as_ref().map(|v| v.as_str()), + payload.event_type = ?payload.event_type.as_ref().map(|v| v.as_str()), + payload.gh_app_installation_id = ?payload.gh_app_installation_id, +))] pub async fn update_subscription( state: State, Path(id): Path, diff --git a/src/model.rs b/src/model.rs index 7e4662f..3cffc4d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -17,10 +17,9 @@ use chrono::{DateTime, Utc}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use sqlx::FromRow; -use valuable::Valuable; /// Represents a row in the `branches` table. -#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Valuable)] +#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema)] pub struct Branch { /// Unique database primary key. pub id: i64, @@ -37,16 +36,14 @@ pub struct Branch { pub last_commit_hash: Option, /// Timestamp when the record was created. - #[valuable(skip)] pub created_at: DateTime, /// Timestamp when the record was updated. - #[valuable(skip)] pub updated_at: DateTime, } /// Represents a row in the `subscriptions` table. -#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Clone, Valuable)] +#[derive(Debug, Serialize, Deserialize, FromRow, JsonSchema, Clone)] pub struct Subscription { /// Unique database primary key. pub id: i64, @@ -72,11 +69,9 @@ pub struct Subscription { pub gh_app_installation_id: i64, /// Timestamp when the record was created. - #[valuable(skip)] pub created_at: DateTime, /// Timestamp when the record was updated. - #[valuable(skip)] pub updated_at: DateTime, } @@ -151,7 +146,7 @@ pub struct SubscriptionHal { } /// Holds payload data for the creation of a [`Subscription`]. -#[derive(Valuable, Debug, Clone, Deserialize, JsonSchema)] +#[derive(Debug, Clone, Deserialize, JsonSchema)] pub struct CreateSubscription { /// Full HTTPS URL of the monitored git repository. pub source_repo_url: RepoUrl, @@ -178,7 +173,7 @@ pub struct CreateSubscription { } /// Holds payload data for the update of a [`Subscription`]. -#[derive(Valuable, Debug, Deserialize, JsonSchema)] +#[derive(Debug, Deserialize, JsonSchema)] pub struct UpdateSubscription { /// The repository whose workflow needs to be triggered. pub target_repo: Option, @@ -199,7 +194,7 @@ pub struct UpdateSubscription { } /// Represents a row in the `trigger_queue` table. -#[derive(Debug, FromRow, Valuable)] +#[derive(Debug, FromRow)] pub struct TriggerQueueItem { /// Unique database primary key. pub id: i64, @@ -229,6 +224,5 @@ pub struct TriggerQueueItem { pub retry_count: i64, /// Serialized OpenTelemetry span context. - #[valuable(skip)] pub span_context: Option, } diff --git a/src/polling/branch.rs b/src/polling/branch.rs index 6cce2f8..cd11ff1 100644 --- a/src/polling/branch.rs +++ b/src/polling/branch.rs @@ -1,10 +1,8 @@ //! Utilities for checking whether a branch has updated. use crate::{domain::CommitHash, error::CommitHashError, model::Branch, polling::git::GitFetcher}; -use valuable::Valuable; /// Enables comparison between a git branch row, and the newly fetched branch. -#[derive(Valuable)] pub(super) struct BranchInfo { /// The branch currently stored in the database. pub branch: Branch, diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 27da1ac..9314f73 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use futures::{StreamExt, future::BoxFuture, stream}; -use tracing::{field::valuable, info, warn}; +use tracing::{info, warn}; use crate::{ context::SharedContext, @@ -127,7 +127,11 @@ async fn process_branches( skip_all, fields( otel.kind = "producer", - branch_info = valuable(branch_info), + branch.id = %branch_info.branch.id, + branch.repo_url = %branch_info.branch.repo_url.as_str(), + branch.name = %branch_info.branch.name.as_str(), + branch.last_commit_hash = ?branch_info.branch.last_commit_hash.as_deref(), + latest_hash = %branch_info.latest_hash.as_str(), ) )] async fn process_single_branch( diff --git a/src/trigger/auth.rs b/src/trigger/auth.rs index c09530e..a35ca75 100644 --- a/src/trigger/auth.rs +++ b/src/trigger/auth.rs @@ -4,7 +4,7 @@ use async_trait::async_trait; use jsonwebtoken::{Algorithm, EncodingKey, Header, encode}; use reqwest::Client; use serde::{Deserialize, Serialize}; -use tracing::{field::valuable, info}; +use tracing::info; use crate::{ config::Config, @@ -35,7 +35,14 @@ pub struct GitHubAuthenticator { impl Authenticator for GitHubAuthenticator { #[tracing::instrument( skip_all, - fields(otel.kind = "client", subscription = valuable(subscription)) + fields( + otel.kind = "client", + subscription.id = %subscription.id, + subscription.branch_id = %subscription.branch_id, + subscription.target_repo = %subscription.target_repo.as_str(), + subscription.event_type = %subscription.event_type.as_str(), + subscription.gh_app_installation_id = %subscription.gh_app_installation_id, + ) )] async fn request_installation_token( &self, diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 1adf2cf..d9b7386 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -2,7 +2,7 @@ use async_trait::async_trait; use reqwest::Client; -use tracing::{field::valuable, info, warn}; +use tracing::{info, warn}; use crate::{ context::SharedContext, @@ -59,7 +59,13 @@ async fn trigger_loop(engine: &TriggerEngine) { skip_all, fields( otel.kind = "consumer", - trigger = tracing::field::Empty + trigger.id = tracing::field::Empty, + trigger.branch_id = tracing::field::Empty, + trigger.new_hash = tracing::field::Empty, + trigger.target_repo = tracing::field::Empty, + trigger.event_type = tracing::field::Empty, + trigger.gh_app_installation_id = tracing::field::Empty, + trigger.retry_count = tracing::field::Empty, ) )] async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { @@ -77,7 +83,17 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro trigger.span_context.as_deref(), ); - tracing::Span::current().record("trigger", valuable(&trigger)); + let span = tracing::Span::current(); + span.record("trigger.id", trigger.id); + span.record("trigger.branch_id", trigger.branch_id); + span.record("trigger.new_hash", trigger.new_hash.as_str()); + span.record("trigger.target_repo", trigger.target_repo.as_str()); + span.record("trigger.event_type", trigger.event_type.as_str()); + span.record( + "trigger.gh_app_installation_id", + trigger.gh_app_installation_id, + ); + span.record("trigger.retry_count", trigger.retry_count); let dispatch_result = dispatch_events(engine, &trigger).await; match dispatch_result { From fa85ff963706671759d369216e2ce94ca762f891 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 09:27:08 +0200 Subject: [PATCH 28/50] Set job retry span kind as `internal` instead of `producer` --- src/trigger/mod.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index d9b7386..756303b 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -125,7 +125,7 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro } /// Schedules the next retry for a trigger in the `trigger_queue`. -#[tracing::instrument(skip_all, fields(otel.kind = "producer"))] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn schedule_retry( engine: &TriggerEngine, trigger: TriggerQueueItem, @@ -163,7 +163,7 @@ async fn schedule_retry( } /// Recovers tasks that have been stuck in `PROCESSING` for too long. -#[tracing::instrument(skip_all, fields(otel.kind = "producer"))] +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn recover_stuck_tasks( repo: &crate::repository::SqliteRepository, config: &crate::config::Config, From 193c19688247903f7b207e78c355a27bdf88250c Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 10:22:49 +0200 Subject: [PATCH 29/50] Log telemetry initialization using `tracing` --- src/telemetry.rs | 77 ++++++++++++++++++++++++++++++------------------ 1 file changed, 48 insertions(+), 29 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index e6e8ab3..cf2a917 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -2,29 +2,46 @@ use opentelemetry::{global, trace::TracerProvider}; use opentelemetry_sdk::trace::SdkTracerProvider; +use thiserror::Error; use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +/// Reason why OpenTelemetry initialization was skipped or failed. +#[derive(Debug, Error)] +enum TelemetryDisabledReason { + /// The SDK was disabled via `OTEL_SDK_DISABLED`. + #[error("OpenTelemetry disabled via OTEL_SDK_DISABLED.")] + SdkDisabled, + + /// The exporter was disabled via `OTEL_TRACES_EXPORTER=none`. + #[error("OpenTelemetry disabled via OTEL_TRACES_EXPORTER=none.")] + ExporterNone, + + /// No OTLP endpoint was configured. + #[error( + "OTLP endpoint not configured, telemetry disabled. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable." + )] + EndpointNotConfigured, + + /// The OTLP span exporter failed to build. + #[error("Failed to build OTLP span exporter, telemetry disabled: {0}")] + ExporterBuildFailed(String), +} + /// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. -fn init_tracer_provider() -> Option { +fn init_tracer_provider() -> Result { if std::env::var("OTEL_SDK_DISABLED") .is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") { - eprintln!("OpenTelemetry disabled via OTEL_SDK_DISABLED."); - return None; + return Err(TelemetryDisabledReason::SdkDisabled); } if std::env::var("OTEL_TRACES_EXPORTER").as_deref() == Ok("none") { - eprintln!("OpenTelemetry disabled via OTEL_TRACES_EXPORTER=none."); - return None; + return Err(TelemetryDisabledReason::ExporterNone); } if !otlp_endpoint_is_configured() { - eprintln!( - "OTLP endpoint not configured, telemetry disabled. \ - Set OTEL_EXPORTER_OTLP_ENDPOINT to enable." - ); - return None; + return Err(TelemetryDisabledReason::EndpointNotConfigured); } let exporter = match opentelemetry_otlp::SpanExporter::builder() @@ -32,22 +49,17 @@ fn init_tracer_provider() -> Option { .build() { Ok(exporter) => exporter, - Err(e) => { - eprintln!("Failed to build OTLP span exporter, telemetry disabled: {e}"); - return None; - } + Err(e) => return Err(TelemetryDisabledReason::ExporterBuildFailed(e.to_string())), }; - Some( - SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource( - opentelemetry_sdk::Resource::builder() - .with_service_name("commit-bridge") - .build(), - ) - .build(), - ) + Ok(SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name("commit-bridge") + .build(), + ) + .build()) } /// Returns `true` if an OTLP endpoint has been explicitly configured @@ -88,10 +100,13 @@ pub fn init() -> TelemetryGuard { let tracer_provider = init_tracer_provider(); - let otel_layer = tracer_provider.as_ref().map(|provider| { - let tracer = provider.tracer(TRACER_NAME); - tracing_opentelemetry::layer().with_tracer(tracer) - }); + let otel_layer = tracer_provider + .as_ref() + .map(|provider| { + let tracer = provider.tracer(TRACER_NAME); + tracing_opentelemetry::layer().with_tracer(tracer) + }) + .ok(); let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(DEFAULT_RUST_LOG)); @@ -101,10 +116,14 @@ pub fn init() -> TelemetryGuard { .with(otel_layer) .init(); + if let Err(reason) = &tracer_provider { + tracing::warn!("{reason}"); + } + #[cfg(debug_assertions)] tracing::warn!("APPLICATION IS RUNNING IN DEBUG MODE."); - TelemetryGuard(tracer_provider) + TelemetryGuard(tracer_provider.ok()) } /// Serializes the current tracing span's OpenTelemetry context into an optional JSON string, From 635959b99464fa8f2bbbfe0b388dfc07c5e6f1b5 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 10:51:31 +0200 Subject: [PATCH 30/50] Clean up `telemetry.rs` --- src/telemetry.rs | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index cf2a917..e2b31a6 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -6,6 +6,12 @@ use thiserror::Error; use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +/// Name used for the OpenTelemetry tracer. +const TRACER_NAME: &str = "commit-bridge"; + +/// Fallback log filter used when `RUST_LOG` is not set. +const DEFAULT_RUST_LOG: &str = "commit_bridge=info"; + /// Reason why OpenTelemetry initialization was skipped or failed. #[derive(Debug, Error)] enum TelemetryDisabledReason { @@ -44,13 +50,10 @@ fn init_tracer_provider() -> Result return Err(TelemetryDisabledReason::EndpointNotConfigured); } - let exporter = match opentelemetry_otlp::SpanExporter::builder() + let exporter = opentelemetry_otlp::SpanExporter::builder() .with_tonic() .build() - { - Ok(exporter) => exporter, - Err(e) => return Err(TelemetryDisabledReason::ExporterBuildFailed(e.to_string())), - }; + .map_err(|e| TelemetryDisabledReason::ExporterBuildFailed(e.to_string()))?; Ok(SdkTracerProvider::builder() .with_batch_exporter(exporter) @@ -93,20 +96,14 @@ impl Drop for TelemetryGuard { /// Sets up the global OpenTelemetry propagator and tracing subscriber. pub fn init() -> TelemetryGuard { - const TRACER_NAME: &str = "commit-bridge"; - const DEFAULT_RUST_LOG: &str = "commit_bridge=info"; - global::set_text_map_propagator(opentelemetry_sdk::propagation::TraceContextPropagator::new()); let tracer_provider = init_tracer_provider(); let otel_layer = tracer_provider .as_ref() - .map(|provider| { - let tracer = provider.tracer(TRACER_NAME); - tracing_opentelemetry::layer().with_tracer(tracer) - }) - .ok(); + .ok() + .map(|provider| tracing_opentelemetry::layer().with_tracer(provider.tracer(TRACER_NAME))); let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(DEFAULT_RUST_LOG)); @@ -116,7 +113,7 @@ pub fn init() -> TelemetryGuard { .with(otel_layer) .init(); - if let Err(reason) = &tracer_provider { + if let Err(reason) = tracer_provider.as_ref() { tracing::warn!("{reason}"); } From 92ccfdb1f0ecdf5154b0caae99d7d4937dfd24da Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 11:14:37 +0200 Subject: [PATCH 31/50] Constrain AI agent reviews --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 2d86087..63b892a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,3 +26,7 @@ This file contains crucial context for AI agents working in this repository. 1. `polling/`: Periodically checks remote git repositories for updates. 2. `trigger/`: Receives update events from the polling engine via `mpsc` channels and triggers GitHub Action workflows on target repositories. - **Error Handling**: Use domain-specific error enums (`HandlerError`, `FatalError`) defined in `src/error.rs` using the `thiserror` crate. Ensure `IntoResponse` is implemented for any errors that bubble up to Axum handlers. + +## Reviews + +When reviewing the correctness of code, always make sure that a potential issue can actually arise _in the context_ of the affected piece of code. From 7a2adb082d0bf19c32de80ff945cc5b94e82e112 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 11:15:44 +0200 Subject: [PATCH 32/50] Move `pub` items up in `telemetry.rs` --- src/telemetry.rs | 130 +++++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index e2b31a6..cbff55f 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -12,71 +12,6 @@ const TRACER_NAME: &str = "commit-bridge"; /// Fallback log filter used when `RUST_LOG` is not set. const DEFAULT_RUST_LOG: &str = "commit_bridge=info"; -/// Reason why OpenTelemetry initialization was skipped or failed. -#[derive(Debug, Error)] -enum TelemetryDisabledReason { - /// The SDK was disabled via `OTEL_SDK_DISABLED`. - #[error("OpenTelemetry disabled via OTEL_SDK_DISABLED.")] - SdkDisabled, - - /// The exporter was disabled via `OTEL_TRACES_EXPORTER=none`. - #[error("OpenTelemetry disabled via OTEL_TRACES_EXPORTER=none.")] - ExporterNone, - - /// No OTLP endpoint was configured. - #[error( - "OTLP endpoint not configured, telemetry disabled. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable." - )] - EndpointNotConfigured, - - /// The OTLP span exporter failed to build. - #[error("Failed to build OTLP span exporter, telemetry disabled: {0}")] - ExporterBuildFailed(String), -} - -/// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. -fn init_tracer_provider() -> Result { - if std::env::var("OTEL_SDK_DISABLED") - .is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") - { - return Err(TelemetryDisabledReason::SdkDisabled); - } - - if std::env::var("OTEL_TRACES_EXPORTER").as_deref() == Ok("none") { - return Err(TelemetryDisabledReason::ExporterNone); - } - - if !otlp_endpoint_is_configured() { - return Err(TelemetryDisabledReason::EndpointNotConfigured); - } - - let exporter = opentelemetry_otlp::SpanExporter::builder() - .with_tonic() - .build() - .map_err(|e| TelemetryDisabledReason::ExporterBuildFailed(e.to_string()))?; - - Ok(SdkTracerProvider::builder() - .with_batch_exporter(exporter) - .with_resource( - opentelemetry_sdk::Resource::builder() - .with_service_name("commit-bridge") - .build(), - ) - .build()) -} - -/// Returns `true` if an OTLP endpoint has been explicitly configured -/// through the standard OpenTelemetry environment variables. -fn otlp_endpoint_is_configured() -> bool { - is_non_empty_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") - || is_non_empty_var("OTEL_EXPORTER_OTLP_ENDPOINT") -} - -/// Returns `true` if the environment variable is set to a non-empty value. -fn is_non_empty_var(name: &str) -> bool { - std::env::var_os(name).is_some_and(|value| !value.is_empty()) -} - /// Guard that gracefully shuts down the tracer provider on drop. /// /// Must be held alive while spans are still being emitted; @@ -163,6 +98,71 @@ pub fn add_link_from_serialized_context(span: &tracing::Span, span_context: Opti } } +/// Reason why OpenTelemetry initialization was skipped or failed. +#[derive(Debug, Error)] +enum TelemetryDisabledReason { + /// The SDK was disabled via `OTEL_SDK_DISABLED`. + #[error("OpenTelemetry disabled via OTEL_SDK_DISABLED.")] + SdkDisabled, + + /// The exporter was disabled via `OTEL_TRACES_EXPORTER=none`. + #[error("OpenTelemetry disabled via OTEL_TRACES_EXPORTER=none.")] + ExporterNone, + + /// No OTLP endpoint was configured. + #[error( + "OTLP endpoint not configured, telemetry disabled. Set OTEL_EXPORTER_OTLP_ENDPOINT to enable." + )] + EndpointNotConfigured, + + /// The OTLP span exporter failed to build. + #[error("Failed to build OTLP span exporter, telemetry disabled: {0}")] + ExporterBuildFailed(String), +} + +/// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. +fn init_tracer_provider() -> Result { + if std::env::var("OTEL_SDK_DISABLED") + .is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") + { + return Err(TelemetryDisabledReason::SdkDisabled); + } + + if std::env::var("OTEL_TRACES_EXPORTER").as_deref() == Ok("none") { + return Err(TelemetryDisabledReason::ExporterNone); + } + + if !otlp_endpoint_is_configured() { + return Err(TelemetryDisabledReason::EndpointNotConfigured); + } + + let exporter = opentelemetry_otlp::SpanExporter::builder() + .with_tonic() + .build() + .map_err(|e| TelemetryDisabledReason::ExporterBuildFailed(e.to_string()))?; + + Ok(SdkTracerProvider::builder() + .with_batch_exporter(exporter) + .with_resource( + opentelemetry_sdk::Resource::builder() + .with_service_name(TRACER_NAME) + .build(), + ) + .build()) +} + +/// Returns `true` if an OTLP endpoint has been explicitly configured +/// through the standard OpenTelemetry environment variables. +fn otlp_endpoint_is_configured() -> bool { + is_non_empty_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") + || is_non_empty_var("OTEL_EXPORTER_OTLP_ENDPOINT") +} + +/// Returns `true` if the environment variable is set to a non-empty value. +fn is_non_empty_var(name: &str) -> bool { + std::env::var_os(name).is_some_and(|value| !value.is_empty()) +} + /// Deserializes a JSON string into an OpenTelemetry context. fn deserialize_span_context(s: &str) -> Result { let map: std::collections::HashMap = serde_json::from_str(s)?; From 758eaaee000c634bbc3528555a215ef675b824da Mon Sep 17 00:00:00 2001 From: Nilirad Date: Thu, 13 Aug 2026 11:57:50 +0200 Subject: [PATCH 33/50] Clean up code in `telemetry.rs` --- src/telemetry.rs | 57 +++++++++++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index cbff55f..65806f5 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -1,8 +1,12 @@ //! OpenTelemetry telemetry helpers. -use opentelemetry::{global, trace::TracerProvider}; +use opentelemetry::{ + global, + trace::{TraceContextExt, TracerProvider}, +}; use opentelemetry_sdk::trace::SdkTracerProvider; use thiserror::Error; +use tracing::{error, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; @@ -24,7 +28,7 @@ impl Drop for TelemetryGuard { if let Some(provider) = self.0.take() && let Err(e) = provider.shutdown() { - tracing::error!("Failed to gracefully shut down tracer provider: {e}"); + error!("Failed to gracefully shut down tracer provider: {e}"); } } } @@ -35,10 +39,12 @@ pub fn init() -> TelemetryGuard { let tracer_provider = init_tracer_provider(); - let otel_layer = tracer_provider - .as_ref() - .ok() - .map(|provider| tracing_opentelemetry::layer().with_tracer(provider.tracer(TRACER_NAME))); + let otel_layer = match &tracer_provider { + Ok(provider) => { + Some(tracing_opentelemetry::layer().with_tracer(provider.tracer(TRACER_NAME))) + } + Err(_) => None, + }; let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(DEFAULT_RUST_LOG)); @@ -48,12 +54,12 @@ pub fn init() -> TelemetryGuard { .with(otel_layer) .init(); - if let Err(reason) = tracer_provider.as_ref() { - tracing::warn!("{reason}"); + if let Err(reason) = &tracer_provider { + warn!("{reason}"); } #[cfg(debug_assertions)] - tracing::warn!("APPLICATION IS RUNNING IN DEBUG MODE."); + warn!("APPLICATION IS RUNNING IN DEBUG MODE."); TelemetryGuard(tracer_provider.ok()) } @@ -72,7 +78,7 @@ pub fn serialize_current_span_context() -> Option { } serde_json::to_string(&map) - .map_err(|e| tracing::warn!("Failed to serialize span context: {e}")) + .map_err(|e| warn!("Failed to serialize span context: {e}")) .ok() } @@ -83,18 +89,16 @@ pub fn add_link_from_serialized_context(span: &tracing::Span, span_context: Opti return; }; - match deserialize_span_context(span_ctx_str) { - Ok(parent_ctx) => { - let remote_span_ctx = opentelemetry::trace::TraceContextExt::span(&parent_ctx) - .span_context() - .clone(); - if remote_span_ctx.is_valid() { - tracing_opentelemetry::OpenTelemetrySpanExt::add_link(span, remote_span_ctx); - } - } - Err(e) => { - tracing::warn!("Failed to deserialize span context: {e}"); - } + let Some(parent_ctx) = deserialize_span_context(span_ctx_str) + .map_err(|e| warn!("Failed to deserialize span context: {e}")) + .ok() + else { + return; + }; + + let remote_span_ctx = parent_ctx.span().span_context().clone(); + if remote_span_ctx.is_valid() { + span.add_link(remote_span_ctx); } } @@ -122,9 +126,7 @@ enum TelemetryDisabledReason { /// Initializes the OpenTelemetry tracer provider if not disabled and configuration is valid. fn init_tracer_provider() -> Result { - if std::env::var("OTEL_SDK_DISABLED") - .is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") - { + if env_var_is_truthy("OTEL_SDK_DISABLED") { return Err(TelemetryDisabledReason::SdkDisabled); } @@ -163,6 +165,11 @@ fn is_non_empty_var(name: &str) -> bool { std::env::var_os(name).is_some_and(|value| !value.is_empty()) } +/// Returns `true` if the environment variable is set to a truthy value (`true` or `1`). +fn env_var_is_truthy(name: &str) -> bool { + std::env::var(name).is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") +} + /// Deserializes a JSON string into an OpenTelemetry context. fn deserialize_span_context(s: &str) -> Result { let map: std::collections::HashMap = serde_json::from_str(s)?; From 2100ae792b988d90b9970f5896ea6899ee4f3538 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:03:53 +0200 Subject: [PATCH 34/50] Add `http.route` trace attributes to Axum handlers --- src/handler.rs | 7 +++++-- src/lib.rs | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index cb621b8..09334c9 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -58,6 +58,7 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", + http.route = "/subscriptions", payload.source_repo_url = %payload.source_repo_url.as_str(), payload.source_branch_name = %payload.source_branch_name.as_str(), payload.target_repo = %payload.target_repo.as_str(), @@ -121,6 +122,7 @@ pub struct ListSubscriptionsQuery { #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", + http.route = "/subscriptions", query.limit = ?query.limit, query.last_id = ?query.last_id, ))] @@ -193,7 +195,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", http.route = "/subscriptions/{id}", id = %id))] pub async fn get_subscription( state: State, Path(id): Path, @@ -238,6 +240,7 @@ async fn get_subscription_inner( #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", + http.route = "/subscriptions/{id}", id = %id, payload.target_repo = ?payload.target_repo.as_ref().map(|v| v.as_str()), payload.event_type = ?payload.event_type.as_ref().map(|v| v.as_str()), @@ -288,7 +291,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", http.route = "/subscriptions/{id}", id = %id))] pub async fn delete_subscription( state: State, Path(id): Path, diff --git a/src/lib.rs b/src/lib.rs index d0798ef..2eaf87c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -254,7 +254,7 @@ async fn set_no_cache_header(req: Request, next: Next) -> Response { mod health_handler { use super::*; #[rovo] - #[tracing::instrument(skip_all, fields(otel.kind = "internal"))] + #[tracing::instrument(skip_all, fields(otel.kind = "internal", http.route = "/health"))] pub async fn health_check(State(_state): State) -> &'static str { "CommitBridge is alive" } From a2867a1615a4fd92d1f1ecd26ac5510a705ce032 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:05:06 +0200 Subject: [PATCH 35/50] Correct documentation for `HttpRequestSpan` --- src/lib.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2eaf87c..206f10c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -264,9 +264,10 @@ mod health_handler { /// following OpenTelemetry semantic conventions. /// /// The span is created within this crate -/// so that it is picked up by the telemetry filter -/// (which only exports spans whose target starts with `commit_bridge`), -/// unlike the default `tower_http` span factory. +/// (instead of using the default `tower_http` span factory) +/// so that it is not filtered out by the default log filter +/// (`RUST_LOG=commit_bridge=info`), +/// which only enables targets within this crate. #[derive(Clone, Copy)] struct HttpRequestSpan; From 2ff89a97791d4b71f09f374fc485bbd3c6951e20 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:08:25 +0200 Subject: [PATCH 36/50] Include SQLx warnings in default `RUST_LOG` --- src/telemetry.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index 65806f5..b728044 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -14,7 +14,13 @@ use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitEx const TRACER_NAME: &str = "commit-bridge"; /// Fallback log filter used when `RUST_LOG` is not set. -const DEFAULT_RUST_LOG: &str = "commit_bridge=info"; +/// +/// Only targets within this crate are enabled at `info` level by default, +/// plus slow SQL statements (`sqlx::query` at `warn` level, which +/// includes the `db.statement` attribute in exported spans). +/// Set `RUST_LOG=debug` (or narrower targets such as `sqlx::query=debug`) +/// to enrich spans with per-query details. +const DEFAULT_RUST_LOG: &str = "commit_bridge=info,sqlx::query=warn"; /// Guard that gracefully shuts down the tracer provider on drop. /// From 6be9cd6d7ab8e421405f0886263b12f4255115e2 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:11:56 +0200 Subject: [PATCH 37/50] Mark spans of failed triggers as error --- src/lib.rs | 2 ++ src/trigger/mod.rs | 29 +++++++++++++++++++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 206f10c..a5a645c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -280,6 +280,7 @@ impl MakeSpan for HttpRequestSpan { url.path = %request.uri().path(), http.response.status_code = tracing::field::Empty, otel.status_code = tracing::field::Empty, + error.type = tracing::field::Empty, ) } } @@ -317,6 +318,7 @@ impl OnResponse for HttpRequestOnResponse { span.record("http.response.status_code", response.status().as_u16()); if self.should_mark_error(response.status()) { span.record("otel.status_code", "ERROR"); + span.record("error.type", response.status().as_u16().to_string()); } } } diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 756303b..12f80e3 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -59,6 +59,8 @@ async fn trigger_loop(engine: &TriggerEngine) { skip_all, fields( otel.kind = "consumer", + otel.status_code = tracing::field::Empty, + error.type = tracing::field::Empty, trigger.id = tracing::field::Empty, trigger.branch_id = tracing::field::Empty, trigger.new_hash = tracing::field::Empty, @@ -116,6 +118,9 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro .await?; } Err(e) => { + let span = tracing::Span::current(); + span.record("otel.status_code", "ERROR"); + span.record("error.type", "dispatch_failed"); warn!("Dispatch failed: {e}"); schedule_retry(engine, trigger, e).await?; } @@ -125,7 +130,14 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro } /// Schedules the next retry for a trigger in the `trigger_queue`. -#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] +#[tracing::instrument( + skip_all, + fields( + otel.kind = "internal", + otel.status_code = tracing::field::Empty, + error.type = tracing::field::Empty, + ) +)] async fn schedule_retry( engine: &TriggerEngine, trigger: TriggerQueueItem, @@ -141,6 +153,9 @@ async fn schedule_retry( .as_secs(); if next_retry_count as u32 >= max_attempts { + let span = tracing::Span::current(); + span.record("otel.status_code", "ERROR"); + span.record("error.type", "retries_exhausted"); tracing::warn!( "Task {} failed after {} attempts: {e}", trigger.id, @@ -234,7 +249,14 @@ async fn notify_subscription( /// /// /// [`Subscription`]: crate::model::Subscription -#[tracing::instrument(skip_all, fields(otel.kind = "client"))] +#[tracing::instrument( + skip_all, + fields( + otel.kind = "client", + otel.status_code = tracing::field::Empty, + error.type = tracing::field::Empty, + ) +)] async fn send_repository_dispatch( engine: &TriggerEngine, iat: &str, @@ -297,6 +319,9 @@ async fn send_repository_dispatch( ); Ok(()) } else { + let span = tracing::Span::current(); + span.record("otel.status_code", "ERROR"); + span.record("error.type", response.status().as_u16().to_string()); Err(WorkflowTriggerError::Api(RequestError::Response { status: response.status(), text: response.text().await?, From c124a3205be2e2f6badcb0470c3becb9da1f7e80 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:13:19 +0200 Subject: [PATCH 38/50] Update SQLx query cache --- ...f5d8d426e63e6eee4b721b46c8b54b9c682e8.json | 20 ------------------- ...baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97.json | 12 ----------- 2 files changed, 32 deletions(-) delete mode 100644 .sqlx/query-adb4f4dfab995b1f06e7491b3cbf5d8d426e63e6eee4b721b46c8b54b9c682e8.json delete mode 100644 .sqlx/query-d8891acd4474ac91f3d2cf12093baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97.json diff --git a/.sqlx/query-adb4f4dfab995b1f06e7491b3cbf5d8d426e63e6eee4b721b46c8b54b9c682e8.json b/.sqlx/query-adb4f4dfab995b1f06e7491b3cbf5d8d426e63e6eee4b721b46c8b54b9c682e8.json deleted file mode 100644 index 8bcbd2a..0000000 --- a/.sqlx/query-adb4f4dfab995b1f06e7491b3cbf5d8d426e63e6eee4b721b46c8b54b9c682e8.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "db_name": "SQLite", - "query": "SELECT branch_id FROM subscriptions WHERE id = ?", - "describe": { - "columns": [ - { - "name": "branch_id", - "ordinal": 0, - "type_info": "Integer" - } - ], - "parameters": { - "Right": 1 - }, - "nullable": [ - false - ] - }, - "hash": "adb4f4dfab995b1f06e7491b3cbf5d8d426e63e6eee4b721b46c8b54b9c682e8" -} diff --git a/.sqlx/query-d8891acd4474ac91f3d2cf12093baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97.json b/.sqlx/query-d8891acd4474ac91f3d2cf12093baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97.json deleted file mode 100644 index 0e5569f..0000000 --- a/.sqlx/query-d8891acd4474ac91f3d2cf12093baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "DELETE FROM subscriptions WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 1 - }, - "nullable": [] - }, - "hash": "d8891acd4474ac91f3d2cf12093baf1c03a4b5b5f4e05e79fc76fefaeb1b9b97" -} From 5b25640466f63784dd13e9cc8ad188c2545ac3cd Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 12:29:55 +0200 Subject: [PATCH 39/50] Honor `OTEL_SERVICE_NAME`/`OTEL_RESOURCE_ATTRIBUTES` envirnoment variables --- src/telemetry.rs | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index b728044..8b4979f 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -4,7 +4,7 @@ use opentelemetry::{ global, trace::{TraceContextExt, TracerProvider}, }; -use opentelemetry_sdk::trace::SdkTracerProvider; +use opentelemetry_sdk::{Resource, trace::SdkTracerProvider}; use thiserror::Error; use tracing::{error, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; @@ -151,14 +151,36 @@ fn init_tracer_provider() -> Result Ok(SdkTracerProvider::builder() .with_batch_exporter(exporter) - .with_resource( - opentelemetry_sdk::Resource::builder() - .with_service_name(TRACER_NAME) - .build(), - ) + .with_resource(build_resource()) .build()) } +/// Builds the tracer provider resource. +/// +/// The SDK-provided resource attributes are kept +/// (`telemetry.sdk.*` and `OTEL_RESOURCE_ATTRIBUTES`), +/// and a fallback service name is applied only when +/// no service name is configured through the environment. +pub(crate) fn build_resource() -> Resource { + let mut builder = Resource::builder(); + if !service_name_is_configured() { + builder = builder.with_service_name(TRACER_NAME); + } + builder.build() +} + +/// Returns `true` if a `service.name` resource attribute +/// is configured through the standard OpenTelemetry environment variables. +pub(crate) fn service_name_is_configured() -> bool { + std::env::var("OTEL_SERVICE_NAME").is_ok_and(|value| !value.is_empty()) + || std::env::var("OTEL_RESOURCE_ATTRIBUTES").is_ok_and(|value| { + value + .split(',') + .filter_map(|entry| entry.split_once('=')) + .any(|(key, value)| key.trim() == "service.name" && !value.trim().is_empty()) + }) +} + /// Returns `true` if an OTLP endpoint has been explicitly configured /// through the standard OpenTelemetry environment variables. fn otlp_endpoint_is_configured() -> bool { From 0c38a79b2e49b01f8e19d9fb7a11cac0eb69520a Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 14:06:58 +0200 Subject: [PATCH 40/50] Add telemetry tests --- src/lib.rs | 3 +- src/main.rs | 3 +- src/telemetry.rs | 8 +- src/tests/mod.rs | 1 + src/tests/telemetry_tests.rs | 192 +++++++++++++++++++++++++++++++++++ 5 files changed, 202 insertions(+), 5 deletions(-) create mode 100644 src/tests/telemetry_tests.rs diff --git a/src/lib.rs b/src/lib.rs index a5a645c..e35f6de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,7 +5,8 @@ clippy::expect_used, clippy::todo, clippy::unimplemented, - clippy::indexing_slicing + clippy::indexing_slicing, + clippy::undocumented_unsafe_blocks )] use std::fs; diff --git a/src/main.rs b/src/main.rs index 94fc3e7..a4fbaf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,7 +5,8 @@ clippy::expect_used, clippy::todo, clippy::unimplemented, - clippy::indexing_slicing + clippy::indexing_slicing, + clippy::undocumented_unsafe_blocks )] use commit_bridge::{log_dotenv_status, run_app, telemetry}; diff --git a/src/telemetry.rs b/src/telemetry.rs index 8b4979f..b399c55 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -183,7 +183,7 @@ pub(crate) fn service_name_is_configured() -> bool { /// Returns `true` if an OTLP endpoint has been explicitly configured /// through the standard OpenTelemetry environment variables. -fn otlp_endpoint_is_configured() -> bool { +pub(crate) fn otlp_endpoint_is_configured() -> bool { is_non_empty_var("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") || is_non_empty_var("OTEL_EXPORTER_OTLP_ENDPOINT") } @@ -194,12 +194,14 @@ fn is_non_empty_var(name: &str) -> bool { } /// Returns `true` if the environment variable is set to a truthy value (`true` or `1`). -fn env_var_is_truthy(name: &str) -> bool { +pub(crate) fn env_var_is_truthy(name: &str) -> bool { std::env::var(name).is_ok_and(|value| value.eq_ignore_ascii_case("true") || value == "1") } /// Deserializes a JSON string into an OpenTelemetry context. -fn deserialize_span_context(s: &str) -> Result { +pub(crate) fn deserialize_span_context( + s: &str, +) -> Result { let map: std::collections::HashMap = serde_json::from_str(s)?; let context = global::get_text_map_propagator(|propagator| propagator.extract(&map)); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 53ab131..0b2947e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -11,3 +11,4 @@ pub mod auth_tests; pub mod config_tests; pub mod mark_error_tests; pub mod polling_tests; +pub mod telemetry_tests; diff --git a/src/tests/telemetry_tests.rs b/src/tests/telemetry_tests.rs new file mode 100644 index 0000000..de3dd70 --- /dev/null +++ b/src/tests/telemetry_tests.rs @@ -0,0 +1,192 @@ +use crate::telemetry::{ + add_link_from_serialized_context, build_resource, deserialize_span_context, env_var_is_truthy, + otlp_endpoint_is_configured, service_name_is_configured, +}; +use opentelemetry::{ + Context, + trace::{SpanContext, SpanId, TraceContextExt, TraceFlags, TraceId, TraceState}, +}; +use opentelemetry_sdk::propagation::TraceContextPropagator; +use std::sync::{Mutex, MutexGuard}; + +const SERVICE_NAME_KEY: opentelemetry::Key = opentelemetry::Key::from_static_str("service.name"); + +/// Serializes all environment access in the test suite. +/// +/// The environment-mutating tests hold this lock for their entire body, +/// covering both the mutation +/// and the reads performed by the functions under test. +/// All environment access in the test binary is confined to these tests, +/// so holding this lock guarantees that +/// no other thread reads or mutates the environment concurrently. +static ENV_LOCK: Mutex<()> = Mutex::new(()); + +/// Sets an environment variable for the duration of the current test. +/// +/// Must only be called from a test that holds [`ENV_LOCK`]. +fn set_env(name: &str, value: &str) { + // SAFETY: Every test that calls this helper + // (directly or through another helper) + // holds `ENV_LOCK` for its whole body, + // and all environment access in the test binary is confined to those tests. + // No other thread can read or mutate the environment while this call runs. + unsafe { + std::env::set_var(name, value); + } +} + +/// Removes an environment variable for the duration of the current test. +/// +/// Must only be called from a test that holds [`ENV_LOCK`]. +fn remove_env(name: &str) { + // SAFETY: Every test that calls this helper + // (directly or through another helper) + // holds `ENV_LOCK` for its whole body, + // and all environment access in the test binary is confined to those tests. + // No other thread can read or mutate the environment while this call runs. + unsafe { + std::env::remove_var(name); + } +} + +/// Acquires [`ENV_LOCK`], tolerating poisoning. +fn lock_env() -> MutexGuard<'static, ()> { + ENV_LOCK + .lock() + .unwrap_or_else(|poisoned| poisoned.into_inner()) +} + +fn sample_span_context() -> SpanContext { + SpanContext::new( + TraceId::from_hex("4bf92f3577b34da6a3ce929d0e0e4736").unwrap(), + SpanId::from_hex("00f067aa0ba902b7").unwrap(), + TraceFlags::SAMPLED, + true, + TraceState::default(), + ) +} + +#[test] +fn test_span_context_serialization_round_trip() { + opentelemetry::global::set_text_map_propagator(TraceContextPropagator::new()); + + let remote_context = Context::new().with_remote_span_context(sample_span_context()); + let mut map = std::collections::HashMap::new(); + opentelemetry::global::get_text_map_propagator(|propagator| { + propagator.inject_context(&remote_context, &mut map); + }); + let serialized = serde_json::to_string(&map).unwrap(); + + let deserialized = deserialize_span_context(&serialized).unwrap(); + let deserialized_span = deserialized.span(); + let extracted = deserialized_span.span_context(); + + let expected = sample_span_context(); + assert_eq!(extracted.trace_id(), expected.trace_id()); + assert_eq!(extracted.span_id(), expected.span_id()); + assert_eq!(extracted.trace_flags(), expected.trace_flags()); + assert!(extracted.is_valid()); +} + +#[test] +fn test_add_link_from_serialized_context_noop_without_parent() { + let span = tracing::info_span!("test_span"); + add_link_from_serialized_context(&span, None); +} + +#[test] +fn test_add_link_from_serialized_context_noop_on_invalid_json() { + let span = tracing::info_span!("test_span"); + add_link_from_serialized_context(&span, Some("not valid json {")); +} + +#[test] +fn test_service_name_is_configured_respects_env() { + let _guard = lock_env(); + set_env("OTEL_SERVICE_NAME", "my-service"); + assert!(service_name_is_configured()); + remove_env("OTEL_SERVICE_NAME"); + + set_env( + "OTEL_RESOURCE_ATTRIBUTES", + "service.name=from-attrs,deployment.environment=dev", + ); + assert!(service_name_is_configured()); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + + assert!(!service_name_is_configured()); +} + +#[test] +fn test_service_name_is_configured_ignores_empty_values() { + let _guard = lock_env(); + set_env("OTEL_SERVICE_NAME", ""); + assert!(!service_name_is_configured()); + remove_env("OTEL_SERVICE_NAME"); + + set_env("OTEL_RESOURCE_ATTRIBUTES", "service.name="); + assert!(!service_name_is_configured()); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); +} + +#[test] +fn test_build_resource_prefers_env_service_name() { + let _guard = lock_env(); + set_env("OTEL_SERVICE_NAME", "my-service"); + let resource = build_resource(); + assert_eq!( + resource + .get(&SERVICE_NAME_KEY) + .map(|v| v.to_string()) + .as_deref(), + Some("my-service") + ); + remove_env("OTEL_SERVICE_NAME"); +} + +#[test] +fn test_build_resource_falls_back_to_tracer_name() { + let _guard = lock_env(); + remove_env("OTEL_SERVICE_NAME"); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + let resource = build_resource(); + assert_eq!( + resource + .get(&SERVICE_NAME_KEY) + .map(|v| v.to_string()) + .as_deref(), + Some("commit-bridge") + ); +} + +#[test] +fn test_env_var_truthiness() { + let _guard = lock_env(); + set_env("CBRIDGE_TEST_TRUTHY", "1"); + assert!(env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); + set_env("CBRIDGE_TEST_TRUTHY", "true"); + assert!(env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); + set_env("CBRIDGE_TEST_TRUTHY", "TRUE"); + assert!(env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); + set_env("CBRIDGE_TEST_TRUTHY", "yes"); + assert!(!env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); + remove_env("CBRIDGE_TEST_TRUTHY"); + assert!(!env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); +} + +#[test] +fn test_otlp_endpoint_detection() { + let _guard = lock_env(); + set_env("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); + assert!(otlp_endpoint_is_configured()); + remove_env("OTEL_EXPORTER_OTLP_ENDPOINT"); + + set_env( + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + "http://localhost:4317/v1/traces", + ); + assert!(otlp_endpoint_is_configured()); + remove_env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); + + assert!(!otlp_endpoint_is_configured()); +} From 1efcca1941c47809ef050c7a36716e5ff248fcff Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 16:13:27 +0200 Subject: [PATCH 41/50] Apply env hygiene on telemetry tests --- src/tests/telemetry_tests.rs | 78 ++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/src/tests/telemetry_tests.rs b/src/tests/telemetry_tests.rs index de3dd70..1e6d359 100644 --- a/src/tests/telemetry_tests.rs +++ b/src/tests/telemetry_tests.rs @@ -49,6 +49,35 @@ fn remove_env(name: &str) { } } +/// Restores environment variables to their original state on drop. +/// +/// Must only be created from a test that holds [`ENV_LOCK`]; +/// the restoration happens while the lock is still held. +struct EnvBackup(Vec<(&'static str, Option)>); + +impl EnvBackup { + /// Captures the current values of the given variables. + fn capture(names: &[&'static str]) -> Self { + Self( + names + .iter() + .map(|&name| (name, std::env::var(name).ok())) + .collect(), + ) + } +} + +impl Drop for EnvBackup { + fn drop(&mut self) { + for (name, value) in &self.0 { + match value { + Some(value) => set_env(name, value), + None => remove_env(name), + } + } + } +} + /// Acquires [`ENV_LOCK`], tolerating poisoning. fn lock_env() -> MutexGuard<'static, ()> { ENV_LOCK @@ -103,6 +132,10 @@ fn test_add_link_from_serialized_context_noop_on_invalid_json() { #[test] fn test_service_name_is_configured_respects_env() { let _guard = lock_env(); + let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); + remove_env("OTEL_SERVICE_NAME"); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + set_env("OTEL_SERVICE_NAME", "my-service"); assert!(service_name_is_configured()); remove_env("OTEL_SERVICE_NAME"); @@ -120,6 +153,10 @@ fn test_service_name_is_configured_respects_env() { #[test] fn test_service_name_is_configured_ignores_empty_values() { let _guard = lock_env(); + let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); + remove_env("OTEL_SERVICE_NAME"); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + set_env("OTEL_SERVICE_NAME", ""); assert!(!service_name_is_configured()); remove_env("OTEL_SERVICE_NAME"); @@ -132,6 +169,9 @@ fn test_service_name_is_configured_ignores_empty_values() { #[test] fn test_build_resource_prefers_env_service_name() { let _guard = lock_env(); + let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); + remove_env("OTEL_SERVICE_NAME"); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); set_env("OTEL_SERVICE_NAME", "my-service"); let resource = build_resource(); assert_eq!( @@ -141,12 +181,42 @@ fn test_build_resource_prefers_env_service_name() { .as_deref(), Some("my-service") ); +} + +#[test] +fn test_build_resource_prefers_env_service_name_over_resource_attributes() { + let _guard = lock_env(); + let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); remove_env("OTEL_SERVICE_NAME"); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + set_env("OTEL_SERVICE_NAME", "my-service"); + set_env( + "OTEL_RESOURCE_ATTRIBUTES", + "service.name=from-attrs,deployment.environment=dev", + ); + let resource = build_resource(); + assert_eq!( + resource + .get(&SERVICE_NAME_KEY) + .map(|v| v.to_string()) + .as_deref(), + Some("my-service") + ); + assert_eq!( + resource + .get(&opentelemetry::Key::from_static_str( + "deployment.environment" + )) + .map(|v| v.to_string()) + .as_deref(), + Some("dev") + ); } #[test] fn test_build_resource_falls_back_to_tracer_name() { let _guard = lock_env(); + let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); remove_env("OTEL_SERVICE_NAME"); remove_env("OTEL_RESOURCE_ATTRIBUTES"); let resource = build_resource(); @@ -162,6 +232,8 @@ fn test_build_resource_falls_back_to_tracer_name() { #[test] fn test_env_var_truthiness() { let _guard = lock_env(); + let _env = EnvBackup::capture(&["CBRIDGE_TEST_TRUTHY"]); + remove_env("CBRIDGE_TEST_TRUTHY"); set_env("CBRIDGE_TEST_TRUTHY", "1"); assert!(env_var_is_truthy("CBRIDGE_TEST_TRUTHY")); set_env("CBRIDGE_TEST_TRUTHY", "true"); @@ -177,6 +249,12 @@ fn test_env_var_truthiness() { #[test] fn test_otlp_endpoint_detection() { let _guard = lock_env(); + let _env = EnvBackup::capture(&[ + "OTEL_EXPORTER_OTLP_ENDPOINT", + "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", + ]); + remove_env("OTEL_EXPORTER_OTLP_ENDPOINT"); + remove_env("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT"); set_env("OTEL_EXPORTER_OTLP_ENDPOINT", "http://localhost:4317"); assert!(otlp_endpoint_is_configured()); remove_env("OTEL_EXPORTER_OTLP_ENDPOINT"); From c83e1c94c5b9b4d69a1860887fd5f1701051102c Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 16:15:42 +0200 Subject: [PATCH 42/50] Store `http.route` on server span rather than handlers spans --- src/handler.rs | 7 ++----- src/lib.rs | 17 +++++++++++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/handler.rs b/src/handler.rs index 09334c9..cb621b8 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -58,7 +58,6 @@ fn map_to_hal(sub_with_branch: SubscriptionWithBranch) -> SubscriptionHal { #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", - http.route = "/subscriptions", payload.source_repo_url = %payload.source_repo_url.as_str(), payload.source_branch_name = %payload.source_branch_name.as_str(), payload.target_repo = %payload.target_repo.as_str(), @@ -122,7 +121,6 @@ pub struct ListSubscriptionsQuery { #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", - http.route = "/subscriptions", query.limit = ?query.limit, query.last_id = ?query.last_id, ))] @@ -195,7 +193,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", http.route = "/subscriptions/{id}", id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn get_subscription( state: State, Path(id): Path, @@ -240,7 +238,6 @@ async fn get_subscription_inner( #[rovo] #[instrument(skip_all, fields( otel.kind = "internal", - http.route = "/subscriptions/{id}", id = %id, payload.target_repo = ?payload.target_repo.as_ref().map(|v| v.as_str()), payload.event_type = ?payload.event_type.as_ref().map(|v| v.as_str()), @@ -291,7 +288,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] -#[instrument(skip_all, fields(otel.kind = "internal", http.route = "/subscriptions/{id}", id = %id))] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn delete_subscription( state: State, Path(id): Path, diff --git a/src/lib.rs b/src/lib.rs index e35f6de..7285fe6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ use std::time::Duration; use axum::{ Router, body::Body, - extract::State, + extract::{MatchedPath, State}, http::{HeaderValue, Request, Response, StatusCode, header}, middleware::{self, Next}, response::IntoResponse, @@ -251,11 +251,22 @@ async fn set_no_cache_header(req: Request, next: Next) -> Response { response } +/// Records the matched route path (`http.route`) +/// onto the current HTTP request span. +/// +/// Runs after routing, so the [`MatchedPath`] extension is available. +async fn record_http_route(req: Request, next: Next) -> Response { + if let Some(matched) = req.extensions().get::() { + tracing::Span::current().record("http.route", matched.as_str()); + } + next.run(req).await +} + #[allow(missing_docs, clippy::missing_docs_in_private_items)] mod health_handler { use super::*; #[rovo] - #[tracing::instrument(skip_all, fields(otel.kind = "internal", http.route = "/health"))] + #[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn health_check(State(_state): State) -> &'static str { "CommitBridge is alive" } @@ -279,6 +290,7 @@ impl MakeSpan for HttpRequestSpan { otel.kind = "server", http.request.method = %request.method(), url.path = %request.uri().path(), + http.route = tracing::field::Empty, http.response.status_code = tracing::field::Empty, otel.status_code = tracing::field::Empty, error.type = tracing::field::Empty, @@ -362,6 +374,7 @@ pub fn build_router( .finish() .layer(middleware::from_fn_with_state(state, auth_middleware)) .layer(middleware::from_fn(set_no_cache_header)) + .layer(middleware::from_fn(record_http_route)) .layer(TimeoutLayer::with_status_code( StatusCode::REQUEST_TIMEOUT, config.server.in_request_timeout, From 88eb2ad5312f47996d15d1fa072e0ae7765c92cb Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 16:20:53 +0200 Subject: [PATCH 43/50] Cover DB errors in `process_queue` spans --- src/trigger/mod.rs | 40 +++++++++++++++++++++++++++------------- 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 12f80e3..1363695 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -71,6 +71,15 @@ async fn trigger_loop(engine: &TriggerEngine) { ) )] async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { + let result = process_queue_inner(engine).await; + if result.is_err() { + tracing::Span::current().record("otel.status_code", "ERROR"); + } + result +} + +/// Internal implementation of [`process_queue`]. +async fn process_queue_inner(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { let Some(trigger) = engine .ctx .repository @@ -99,33 +108,38 @@ async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerErro let dispatch_result = dispatch_events(engine, &trigger).await; match dispatch_result { - Ok(_) => { - engine - .ctx - .repository - .trigger_queue_delete(trigger.id) - .await?; - } + Ok(_) => delete_processed_trigger(engine, trigger.id).await, Err(WorkflowTriggerError::Repository(crate::repository::RepositoryError::NotFound)) => { warn!( "Subscription for branch ID {} and target repo {} was not found (likely deleted). Deleting trigger task {} from queue.", trigger.branch_id, trigger.target_repo, trigger.id ); - engine - .ctx - .repository - .trigger_queue_delete(trigger.id) - .await?; + delete_processed_trigger(engine, trigger.id).await } Err(e) => { let span = tracing::Span::current(); span.record("otel.status_code", "ERROR"); span.record("error.type", "dispatch_failed"); warn!("Dispatch failed: {e}"); - schedule_retry(engine, trigger, e).await?; + if let Err(retry_err) = schedule_retry(engine, trigger, e).await { + tracing::Span::current().record("error.type", "retry_scheduling_failed"); + return Err(retry_err); + } + Ok(()) } } +} +/// Deletes a processed trigger from the queue, +/// marking the current span as failed if the deletion errors. +async fn delete_processed_trigger( + engine: &TriggerEngine, + trigger_id: i64, +) -> Result<(), WorkflowTriggerError> { + if let Err(e) = engine.ctx.repository.trigger_queue_delete(trigger_id).await { + tracing::Span::current().record("error.type", "queue_delete_failed"); + return Err(e.into()); + } Ok(()) } From 17666ac0fd213e3c188d2a7a13b77c9dbe4c197f Mon Sep 17 00:00:00 2001 From: Nilirad Date: Tue, 18 Aug 2026 16:31:31 +0200 Subject: [PATCH 44/50] Make `OTEL_SERVICE_NAME` take precedence over `OTEL_RESOURCE_ATTRIBUTES` `service_name_is_configured` has been removed, so `telemetry_tests.rs` needed to be changed too to use `configured_service_name` instead. --- src/telemetry.rs | 46 ++++++++++++++++++++++++------------ src/tests/telemetry_tests.rs | 18 +++++++------- 2 files changed, 40 insertions(+), 24 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index b399c55..4ea5e14 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -159,25 +159,41 @@ fn init_tracer_provider() -> Result /// /// The SDK-provided resource attributes are kept /// (`telemetry.sdk.*` and `OTEL_RESOURCE_ATTRIBUTES`), -/// and a fallback service name is applied only when -/// no service name is configured through the environment. +/// and the `service.name` attribute is set explicitly from the environment +/// (or the [`TRACER_NAME`] fallback), +/// so that `OTEL_SERVICE_NAME` takes precedence +/// over `service.name` in `OTEL_RESOURCE_ATTRIBUTES`, +/// as required by the OpenTelemetry specification. pub(crate) fn build_resource() -> Resource { - let mut builder = Resource::builder(); - if !service_name_is_configured() { - builder = builder.with_service_name(TRACER_NAME); + Resource::builder() + .with_service_name(configured_service_name().unwrap_or(TRACER_NAME.to_string())) + .build() +} + +/// Returns the `service.name` resource attribute value +/// configured through the standard OpenTelemetry environment variables, +/// if any. +/// +/// `OTEL_SERVICE_NAME` takes precedence over +/// the `service.name` entry of `OTEL_RESOURCE_ATTRIBUTES`. +pub(crate) fn configured_service_name() -> Option { + match std::env::var("OTEL_SERVICE_NAME") { + Ok(name) if !name.is_empty() => Some(name), + _ => resource_attributes_service_name(), } - builder.build() } -/// Returns `true` if a `service.name` resource attribute -/// is configured through the standard OpenTelemetry environment variables. -pub(crate) fn service_name_is_configured() -> bool { - std::env::var("OTEL_SERVICE_NAME").is_ok_and(|value| !value.is_empty()) - || std::env::var("OTEL_RESOURCE_ATTRIBUTES").is_ok_and(|value| { - value - .split(',') - .filter_map(|entry| entry.split_once('=')) - .any(|(key, value)| key.trim() == "service.name" && !value.trim().is_empty()) +/// Returns the `service.name` value +/// from the `OTEL_RESOURCE_ATTRIBUTES` environment variable, if present. +fn resource_attributes_service_name() -> Option { + std::env::var("OTEL_RESOURCE_ATTRIBUTES") + .ok() + .and_then(|value| { + value.split(',').find_map(|entry| { + let (key, value) = entry.split_once('=')?; + (key.trim() == "service.name" && !value.trim().is_empty()) + .then(|| value.trim().to_string()) + }) }) } diff --git a/src/tests/telemetry_tests.rs b/src/tests/telemetry_tests.rs index 1e6d359..dcccb0a 100644 --- a/src/tests/telemetry_tests.rs +++ b/src/tests/telemetry_tests.rs @@ -1,6 +1,6 @@ use crate::telemetry::{ - add_link_from_serialized_context, build_resource, deserialize_span_context, env_var_is_truthy, - otlp_endpoint_is_configured, service_name_is_configured, + add_link_from_serialized_context, build_resource, configured_service_name, + deserialize_span_context, env_var_is_truthy, otlp_endpoint_is_configured, }; use opentelemetry::{ Context, @@ -130,39 +130,39 @@ fn test_add_link_from_serialized_context_noop_on_invalid_json() { } #[test] -fn test_service_name_is_configured_respects_env() { +fn test_configured_service_name_respects_env() { let _guard = lock_env(); let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); remove_env("OTEL_SERVICE_NAME"); remove_env("OTEL_RESOURCE_ATTRIBUTES"); set_env("OTEL_SERVICE_NAME", "my-service"); - assert!(service_name_is_configured()); + assert_eq!(configured_service_name().as_deref(), Some("my-service")); remove_env("OTEL_SERVICE_NAME"); set_env( "OTEL_RESOURCE_ATTRIBUTES", "service.name=from-attrs,deployment.environment=dev", ); - assert!(service_name_is_configured()); + assert_eq!(configured_service_name().as_deref(), Some("from-attrs")); remove_env("OTEL_RESOURCE_ATTRIBUTES"); - assert!(!service_name_is_configured()); + assert_eq!(configured_service_name(), None); } #[test] -fn test_service_name_is_configured_ignores_empty_values() { +fn test_configured_service_name_ignores_empty_values() { let _guard = lock_env(); let _env = EnvBackup::capture(&["OTEL_SERVICE_NAME", "OTEL_RESOURCE_ATTRIBUTES"]); remove_env("OTEL_SERVICE_NAME"); remove_env("OTEL_RESOURCE_ATTRIBUTES"); set_env("OTEL_SERVICE_NAME", ""); - assert!(!service_name_is_configured()); + assert_eq!(configured_service_name(), None); remove_env("OTEL_SERVICE_NAME"); set_env("OTEL_RESOURCE_ATTRIBUTES", "service.name="); - assert!(!service_name_is_configured()); + assert_eq!(configured_service_name(), None); remove_env("OTEL_RESOURCE_ATTRIBUTES"); } From b3106608d0b2a2926a5585c78abc3416e91d4b3b Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 19 Aug 2026 12:04:19 +0200 Subject: [PATCH 45/50] Make polling engine report span error --- src/polling/git.rs | 101 ++++++++++++++++++++++++++------------------- src/polling/mod.rs | 50 +++++++++++++++++++++- 2 files changed, 107 insertions(+), 44 deletions(-) diff --git a/src/polling/git.rs b/src/polling/git.rs index 4536b5b..2185397 100644 --- a/src/polling/git.rs +++ b/src/polling/git.rs @@ -46,57 +46,74 @@ impl MainGitFetcher { impl GitFetcher for MainGitFetcher { #[tracing::instrument( skip_all, - fields(otel.kind = "client", repo_url = %repo_url, branch = %branch) + fields( + otel.kind = "client", + otel.status_code = tracing::field::Empty, + repo_url = %repo_url, + branch = %branch, + ) )] async fn get_latest_hash( &self, repo_url: &str, branch: &str, ) -> Result { - let repo_url = repo_url.to_string(); - let branch = branch.to_string(); - let timeout = self.timeout; - let thread_safe_repo = self.repo.clone(); + let result = get_latest_hash_inner(&self.repo, self.timeout, repo_url, branch).await; + if result.is_err() { + tracing::Span::current().record("otel.status_code", "ERROR"); + } + result + } +} - let fetch_task = tokio::task::spawn_blocking(move || { - let repo = thread_safe_repo.to_thread_local(); - let mut remote = repo.remote_at(repo_url)?; - remote = remote.with_fetch_tags(gix::remote::fetch::Tags::None); - remote.replace_refspecs(Some(branch.as_str()), Direction::Fetch)?; - let connection = remote.connect(Direction::Fetch)?; - let (ref_map, _) = - connection.ref_map(Discard, gix::remote::ref_map::Options::default())?; +/// Internal implementation of [`GitFetcher::get_latest_hash`]. +async fn get_latest_hash_inner( + repo: &gix::ThreadSafeRepository, + timeout: std::time::Duration, + repo_url: &str, + branch: &str, +) -> Result { + let repo_url = repo_url.to_string(); + let branch = branch.to_string(); + let thread_safe_repo = repo.clone(); - let target_head = format!("refs/heads/{}", branch); - let target_tag = format!("refs/tags/{}", branch); - let target_head_bytes = target_head.as_bytes(); - let target_tag_bytes = target_tag.as_bytes(); - let branch_bytes = branch.as_bytes(); + let fetch_task = tokio::task::spawn_blocking(move || { + let repo = thread_safe_repo.to_thread_local(); + let mut remote = repo.remote_at(repo_url)?; + remote = remote.with_fetch_tags(gix::remote::fetch::Tags::None); + remote.replace_refspecs(Some(branch.as_str()), Direction::Fetch)?; + let connection = remote.connect(Direction::Fetch)?; + let (ref_map, _) = connection.ref_map(Discard, gix::remote::ref_map::Options::default())?; - ref_map - .remote_refs - .iter() - .find(|r| { - let (name, _, _) = r.unpack(); - name == branch_bytes || name == target_head_bytes || name == target_tag_bytes - }) - .ok_or_else(|| CommitHashError::Git("Branch not found".to_string()))? - .unpack() - .1 - .map(|id| id.to_string()) - .ok_or_else(|| CommitHashError::Git("Hash not found for branch".to_string())) - }); + let target_head = format!("refs/heads/{}", branch); + let target_tag = format!("refs/tags/{}", branch); + let target_head_bytes = target_head.as_bytes(); + let target_tag_bytes = target_tag.as_bytes(); + let branch_bytes = branch.as_bytes(); - match tokio::time::timeout(timeout, fetch_task).await { - Ok(Ok(Ok(hash))) => Ok(CommitHash::new(hash)?), - Ok(Ok(Err(e))) => Err(e), - Ok(Err(e)) => Err(CommitHashError::UnexpectedStatus(format!( - "Spawn blocking failed: {}", - e - ))), - Err(_) => Err(CommitHashError::UnexpectedStatus( - "Gix operation timed out".to_string(), - )), - } + ref_map + .remote_refs + .iter() + .find(|r| { + let (name, _, _) = r.unpack(); + name == branch_bytes || name == target_head_bytes || name == target_tag_bytes + }) + .ok_or_else(|| CommitHashError::Git("Branch not found".to_string()))? + .unpack() + .1 + .map(|id| id.to_string()) + .ok_or_else(|| CommitHashError::Git("Hash not found for branch".to_string())) + }); + + match tokio::time::timeout(timeout, fetch_task).await { + Ok(Ok(Ok(hash))) => Ok(CommitHash::new(hash)?), + Ok(Ok(Err(e))) => Err(e), + Ok(Err(e)) => Err(CommitHashError::UnexpectedStatus(format!( + "Spawn blocking failed: {}", + e + ))), + Err(_) => Err(CommitHashError::UnexpectedStatus( + "Gix operation timed out".to_string(), + )), } } diff --git a/src/polling/mod.rs b/src/polling/mod.rs index 9314f73..d39fe3d 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -50,8 +50,23 @@ async fn polling_loop(ctx: SharedContext) { /// /// /// [`TriggerEngine`]: crate::trigger::TriggerEngine -#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] +#[tracing::instrument( + skip_all, + fields( + otel.kind = "internal", + otel.status_code = tracing::field::Empty, + ) +)] async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { + let result = poll_branches_inner(ctx).await; + if result.is_err() { + tracing::Span::current().record("otel.status_code", "ERROR"); + } + result +} + +/// Internal implementation of [`poll_branches`]. +async fn poll_branches_inner(ctx: &SharedContext) -> Result<(), PollingError> { let updated_branches = gather_updated_branches(ctx).await?; if updated_branches.is_empty() { return Ok(()); @@ -69,8 +84,25 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { } /// Gathers stored branches that need to be updated. -#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] +#[tracing::instrument( + skip_all, + fields( + otel.kind = "internal", + otel.status_code = tracing::field::Empty, + ) +)] async fn gather_updated_branches(ctx: &SharedContext) -> Result, sqlx::Error> { + let result = gather_updated_branches_inner(ctx).await; + if result.is_err() { + tracing::Span::current().record("otel.status_code", "ERROR"); + } + result +} + +/// Internal implementation of [`gather_updated_branches`]. +async fn gather_updated_branches_inner( + ctx: &SharedContext, +) -> Result, sqlx::Error> { let branches = ctx .repository .branches_get_all() @@ -127,6 +159,7 @@ async fn process_branches( skip_all, fields( otel.kind = "producer", + otel.status_code = tracing::field::Empty, branch.id = %branch_info.branch.id, branch.repo_url = %branch_info.branch.repo_url.as_str(), branch.name = %branch_info.branch.name.as_str(), @@ -138,6 +171,19 @@ async fn process_single_branch( repo: std::sync::Arc, branch_info: &branch::BranchInfo, tx: &mut sqlx::SqliteConnection, +) -> Result<(), RepositoryError> { + let result = process_single_branch_inner(repo, branch_info, tx).await; + if result.is_err() { + tracing::Span::current().record("otel.status_code", "ERROR"); + } + result +} + +/// Internal implementation of [`process_single_branch`]. +async fn process_single_branch_inner( + repo: std::sync::Arc, + branch_info: &branch::BranchInfo, + tx: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError> { repo.branches_update_last_commit_hash(branch_info.branch.id, &branch_info.latest_hash, tx) .await?; From ef55346c9034ab6653ece7db0b5a1740358b1267 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Fri, 21 Aug 2026 16:58:24 +0200 Subject: [PATCH 46/50] Async engines enter spans only if there is job --- src/polling/mod.rs | 51 ++++++++++++++++++++++------------------------ src/trigger/mod.rs | 40 ++++++++++++++++++++++++------------ 2 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/polling/mod.rs b/src/polling/mod.rs index d39fe3d..2234b84 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -48,8 +48,22 @@ async fn polling_loop(ctx: SharedContext) { /// updates them in the `branches` table, /// and queues the updates for the [`TriggerEngine`]. /// +/// Only enters an instrumented span when updates are found, +/// so that empty polling cycles do not produce exported spans. +/// /// /// [`TriggerEngine`]: crate::trigger::TriggerEngine +async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { + let updated_branches = gather_updated_branches(ctx).await?; + if updated_branches.is_empty() { + return Ok(()); + } + + process_branch_updates(ctx, updated_branches).await +} + +/// Applies branch updates in the `branches` table +/// and queues triggers for the updated branches. #[tracing::instrument( skip_all, fields( @@ -57,21 +71,22 @@ async fn polling_loop(ctx: SharedContext) { otel.status_code = tracing::field::Empty, ) )] -async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { - let result = poll_branches_inner(ctx).await; +async fn process_branch_updates( + ctx: &SharedContext, + updated_branches: Vec, +) -> Result<(), PollingError> { + let result = process_branch_updates_inner(ctx, updated_branches).await; if result.is_err() { tracing::Span::current().record("otel.status_code", "ERROR"); } result } -/// Internal implementation of [`poll_branches`]. -async fn poll_branches_inner(ctx: &SharedContext) -> Result<(), PollingError> { - let updated_branches = gather_updated_branches(ctx).await?; - if updated_branches.is_empty() { - return Ok(()); - } - +/// Internal implementation of [`process_branch_updates`]. +async fn process_branch_updates_inner( + ctx: &SharedContext, + updated_branches: Vec, +) -> Result<(), PollingError> { let repository = ctx.repository.clone(); let shared_branches = std::sync::Arc::new(updated_branches); ctx.repository @@ -84,25 +99,7 @@ async fn poll_branches_inner(ctx: &SharedContext) -> Result<(), PollingError> { } /// Gathers stored branches that need to be updated. -#[tracing::instrument( - skip_all, - fields( - otel.kind = "internal", - otel.status_code = tracing::field::Empty, - ) -)] async fn gather_updated_branches(ctx: &SharedContext) -> Result, sqlx::Error> { - let result = gather_updated_branches_inner(ctx).await; - if result.is_err() { - tracing::Span::current().record("otel.status_code", "ERROR"); - } - result -} - -/// Internal implementation of [`gather_updated_branches`]. -async fn gather_updated_branches_inner( - ctx: &SharedContext, -) -> Result, sqlx::Error> { let branches = ctx .repository .branches_get_all() diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 1363695..7ed3762 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -55,6 +55,23 @@ async fn trigger_loop(engine: &TriggerEngine) { } /// Processes a single queued event. +/// +/// Only enters an instrumented span when a trigger is actually found, +/// so that empty polling cycles do not produce exported spans. +async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { + let Some(trigger) = engine + .ctx + .repository + .trigger_queue_process_oldest_pending() + .await? + else { + return Ok(()); + }; + + process_trigger(engine, trigger).await +} + +/// Processes a single queued trigger. #[tracing::instrument( skip_all, fields( @@ -70,25 +87,22 @@ async fn trigger_loop(engine: &TriggerEngine) { trigger.retry_count = tracing::field::Empty, ) )] -async fn process_queue(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { - let result = process_queue_inner(engine).await; +async fn process_trigger( + engine: &TriggerEngine, + trigger: TriggerQueueItem, +) -> Result<(), WorkflowTriggerError> { + let result = process_trigger_inner(engine, trigger).await; if result.is_err() { tracing::Span::current().record("otel.status_code", "ERROR"); } result } -/// Internal implementation of [`process_queue`]. -async fn process_queue_inner(engine: &TriggerEngine) -> Result<(), WorkflowTriggerError> { - let Some(trigger) = engine - .ctx - .repository - .trigger_queue_process_oldest_pending() - .await? - else { - return Ok(()); - }; - +/// Internal implementation of [`process_trigger`]. +async fn process_trigger_inner( + engine: &TriggerEngine, + trigger: TriggerQueueItem, +) -> Result<(), WorkflowTriggerError> { crate::telemetry::add_link_from_serialized_context( &tracing::Span::current(), trigger.span_context.as_deref(), From 2e80edc0a38f311ae879a5341a059721149898af Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 2 Sep 2026 10:02:49 +0200 Subject: [PATCH 47/50] Decouple telemetry from `RUST_LOG` Telemetry export is fixed to `info` level or above --- src/telemetry.rs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/telemetry.rs b/src/telemetry.rs index 4ea5e14..0ad7ea1 100644 --- a/src/telemetry.rs +++ b/src/telemetry.rs @@ -8,7 +8,9 @@ use opentelemetry_sdk::{Resource, trace::SdkTracerProvider}; use thiserror::Error; use tracing::{error, warn}; use tracing_opentelemetry::OpenTelemetrySpanExt; -use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{ + EnvFilter, Layer, filter::LevelFilter, layer::SubscriberExt, util::SubscriberInitExt, +}; /// Name used for the OpenTelemetry tracer. const TRACER_NAME: &str = "commit-bridge"; @@ -19,7 +21,11 @@ const TRACER_NAME: &str = "commit-bridge"; /// plus slow SQL statements (`sqlx::query` at `warn` level, which /// includes the `db.statement` attribute in exported spans). /// Set `RUST_LOG=debug` (or narrower targets such as `sqlx::query=debug`) -/// to enrich spans with per-query details. +/// to enrich console logs with per-query details. +/// +/// `RUST_LOG` only affects console logs: +/// exported spans are filtered independently +/// (see [`init`]). const DEFAULT_RUST_LOG: &str = "commit_bridge=info,sqlx::query=warn"; /// Guard that gracefully shuts down the tracer provider on drop. @@ -40,6 +46,8 @@ impl Drop for TelemetryGuard { } /// Sets up the global OpenTelemetry propagator and tracing subscriber. +/// +/// Exports spans and events at `info` level or above. pub fn init() -> TelemetryGuard { global::set_text_map_propagator(opentelemetry_sdk::propagation::TraceContextPropagator::new()); @@ -55,9 +63,8 @@ pub fn init() -> TelemetryGuard { let env_filter = EnvFilter::try_from_default_env().unwrap_or(EnvFilter::new(DEFAULT_RUST_LOG)); tracing_subscriber::registry() - .with(env_filter) - .with(tracing_subscriber::fmt::layer()) - .with(otel_layer) + .with(tracing_subscriber::fmt::layer().with_filter(env_filter)) + .with(otel_layer.with_filter(LevelFilter::INFO)) .init(); if let Err(reason) = &tracer_provider { From 4dd4bac4bfc646309887d5057715ab0ddc53c5a4 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 2 Sep 2026 10:08:16 +0200 Subject: [PATCH 48/50] Move `auth_middleware`'s `authenticated` span field to the server span --- src/lib.rs | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7285fe6..535eb29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -187,15 +187,10 @@ fn init_engines(ctx: &SharedContext, http_client: Client) -> Result, req: Request, @@ -277,9 +272,7 @@ mod health_handler { /// /// The span is created within this crate /// (instead of using the default `tower_http` span factory) -/// so that it is not filtered out by the default log filter -/// (`RUST_LOG=commit_bridge=info`), -/// which only enables targets within this crate. +/// so that its attributes follow OpenTelemetry conventions. #[derive(Clone, Copy)] struct HttpRequestSpan; @@ -294,6 +287,7 @@ impl MakeSpan for HttpRequestSpan { http.response.status_code = tracing::field::Empty, otel.status_code = tracing::field::Empty, error.type = tracing::field::Empty, + authenticated = tracing::field::Empty, ) } } From 4a262af1db72417d310c3d062980fa02cb5dee59 Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 2 Sep 2026 10:15:37 +0200 Subject: [PATCH 49/50] Run `cargo update` --- Cargo.lock | 470 ++++++++++++++++++++++++++--------------------------- 1 file changed, 231 insertions(+), 239 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3ef04d8..8108df2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" dependencies = [ "memchr", ] @@ -40,9 +40,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "android_system_properties" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc" dependencies = [ "libc", ] @@ -95,13 +95,13 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.91" +version = "0.1.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +checksum = "82f6aeea286b8eb4dd3431a1be1b59d290ace00f5bfd8e2a159bc2a05e2c1667" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -138,9 +138,9 @@ checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" -version = "1.17.3" +version = "1.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00bdb5da18dac48ca2cc7cd4a98e533e8635a58e2361d13a1a4ee3888e0d72f1" +checksum = "b281d307588d634de920874890732659e2e7672f72b5e10e81badc1a8a83621e" dependencies = [ "aws-lc-sys", "untrusted 0.7.1", @@ -149,9 +149,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.43.0" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43103168cc76fe62678a375e722fc9cb3a0146159ac5828bc4f0dfd755c2224c" +checksum = "9bff6c3b54fad79a2e60b8102caf565819711497c1f5f092f49508e2f5c31b27" dependencies = [ "cc", "cmake", @@ -271,9 +271,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f7dc094d718f2e1c1559ad110e27eeaae14a5465d3d56dd6dbd793079fbd530" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" dependencies = [ "memchr", "regex-automata", @@ -300,15 +300,15 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" [[package]] name = "bytesize" -version = "2.4.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d7c8918969267b2932ffd5655509bbbea0833823058c378876953217f5fc50e" +checksum = "7354288c522e7e980fafd2075d63d1285794c3a6a16cdd492f189ea406e5f18b" [[package]] name = "cc" -version = "1.3.0" +version = "1.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89588d05638b5b4594a3348a2d6c20277e43a7f5c5202b05cc56888475a47b8" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" dependencies = [ "find-msvc-tools", "jobserver", @@ -330,12 +330,12 @@ checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" [[package]] name = "chacha20" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures 0.3.1", "rand_core 0.10.1", ] @@ -388,9 +388,9 @@ dependencies = [ [[package]] name = "combine" -version = "4.6.7" +version = "4.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +checksum = "cfc320937d09e6de266b31b9afb480f197d7a861be86be7cb2ea7e5d1bfffc5e" dependencies = [ "bytes", "memchr", @@ -436,15 +436,6 @@ dependencies = [ "wiremock", ] -[[package]] -name = "concurrent-queue" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" -dependencies = [ - "crossbeam-utils", -] - [[package]] name = "config" version = "0.15.25" @@ -537,9 +528,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" dependencies = [ "libc", ] @@ -561,9 +552,9 @@ checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" -version = "1.5.0" +version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +checksum = "8498c871161e1742aaa9d52551b2d6ebdd4c3d45a3be423e3728f33b955be550" dependencies = [ "cfg-if", ] @@ -759,13 +750,13 @@ dependencies = [ [[package]] name = "displaydoc" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -809,9 +800,9 @@ checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] name = "either" -version = "1.16.0" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34" dependencies = [ "serde", ] @@ -865,11 +856,10 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.4.1" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" dependencies = [ - "concurrent-queue", "parking", "pin-project-lite", ] @@ -902,9 +892,9 @@ dependencies = [ [[package]] name = "find-msvc-tools" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" [[package]] name = "flume" @@ -952,9 +942,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "futures" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a88cf1f829d945f548cf8fec32c61b1f202b6d93b45848602fc02af4b12ad218" +checksum = "9a31d2a3fbaaeb2af2368bbdd904aa8e812d3c04a1ee10d3171f52d556e5d0a3" dependencies = [ "futures-channel", "futures-core", @@ -967,9 +957,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +checksum = "b1f9e3d69d39e4862ffed03ed071a76f9a13ba1d9109d355b0f0aa6b15e393c4" dependencies = [ "futures-core", "futures-sink", @@ -977,15 +967,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" [[package]] name = "futures-executor" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458" +checksum = "031b47cf1a3c6cc8bc2fc76cd437f521619387907d469316e7c0bc278f1f5432" dependencies = [ "futures-core", "futures-task", @@ -1005,38 +995,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" [[package]] name = "futures-macro" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "futures-sink" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" +checksum = "1944426bf7d03f1d14f708785e4b33efd750b36d48a157b836b3efc15ede8e1d" [[package]] name = "futures-task" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109" +checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd" [[package]] name = "futures-util" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" +checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-channel", "futures-core", @@ -1163,9 +1153,9 @@ dependencies = [ [[package]] name = "gix-actor" -version = "0.41.1" +version = "0.41.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bc998b8f746dda8565450d08a63b792ced9165d8c27a1ed3f02799ec6a7820f" +checksum = "33f9308ad6fd35b2a865cbe4117ac61b2be59e4a9ef1621c7a9794f7c8e52c5b" dependencies = [ "bstr", "gix-date", @@ -1204,9 +1194,9 @@ dependencies = [ [[package]] name = "gix-bitmap" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ebef0c26ad305747649e727bbcd56a7b7910754eb7cea88f6dff6f93c51283" +checksum = "7cd1d118d0f5d88b96e6f6e13b566475fef4797ead4a02c26fed36c1375066f7" dependencies = [ "gix-error", ] @@ -1233,18 +1223,18 @@ dependencies = [ [[package]] name = "gix-chunk" -version = "0.7.2" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9faee47943b638e58ddd5e275a4906ad3e4b6c8584f1d41bd18ab9032ec52afb" +checksum = "b2a871e5cab12ba568845714473505deefffb3c04eb47f4708ce344cd459c1cc" dependencies = [ "gix-error", ] [[package]] name = "gix-command" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00706d4fef135ef4b01680d5218c6ee40cda8baf697b864296cbc887d19118f6" +checksum = "cf4363accdf6ef7ba861871d2d521ab7418a04aaaed919fadb022af71d379b12" dependencies = [ "bstr", "gix-path", @@ -1503,9 +1493,9 @@ dependencies = [ [[package]] name = "gix-imara-diff" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b305d85504de270ad3525d726a6b69cc59ee7b2269b014387651107ab9f0755b" +checksum = "1c91d8cffac8849493a82233811bd02b2b183b8cf39bf704de0fa0841b737595" dependencies = [ "bstr", "hashbrown 0.17.1", @@ -1552,9 +1542,9 @@ dependencies = [ [[package]] name = "gix-mailmap" -version = "0.33.1" +version = "0.33.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "195fd20808055824531be2fd0d34136d900e5fbca3ffb0a3c07e8beeefb9c828" +checksum = "a824767d38b81475059cb01f5020a13fb96e7ed6bbf9851c7112b46ada78db48" dependencies = [ "bstr", "gix-actor", @@ -1652,9 +1642,9 @@ dependencies = [ [[package]] name = "gix-path" -version = "0.12.2" +version = "0.12.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbbecb0f8dc5cdf6cbde69133f7072064dfc9da4cf0046913afb6857b07300fa" +checksum = "38fc6f029ea67de83cbcbd33fd98c05a48360d2932b39d9fdacbc6eae802d475" dependencies = [ "bstr", "gix-trace", @@ -1800,9 +1790,9 @@ dependencies = [ [[package]] name = "gix-sec" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab8519976e4c7e486270740a5400369f37940779b80bd1377d94cfa1125d01b3" +checksum = "af4fe6c152c1d50aea36f299825702cd37e303307832fec1d0fdd5844e47ce2f" dependencies = [ "bitflags 2.13.1", "gix-path", @@ -1878,9 +1868,9 @@ dependencies = [ [[package]] name = "gix-trace" -version = "0.1.20" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44dc45eae785c0eb14173e0f152e6e224dcf4d45b6a6999a3aed22af541ad678" +checksum = "be3eb81d9dc914335923e50d52829c551feefd6a72d176c4130c546b67a60814" [[package]] name = "gix-transport" @@ -1932,9 +1922,9 @@ dependencies = [ [[package]] name = "gix-utils" -version = "0.3.4" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d773a906e39472c2b00aaf1993cd120d40198c1ff6db07c0ee9a44d4431b66c1" +checksum = "0da1c46491b49458a446cc76f0085860f8164c2290742e0aa8c653ce67240a97" dependencies = [ "bstr", "fastrand", @@ -1944,9 +1934,9 @@ dependencies = [ [[package]] name = "gix-validate" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bc6fc771c4063ba7cd2f47b91fb6076251c6a823b64b7fe7b8874b0fe4afae3" +checksum = "4dae8780f63ed8a803b8bdabbd7aa5f5c5d74592c8b50eed875c1bb4f6545a6a" dependencies = [ "bstr", ] @@ -2007,9 +1997,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6cb093c84e8bd9b188d4c4a8cb6579fc016968d14c99882163cd3ff402a4f155" +checksum = "ef8e5e5a340588f4452631496976cf8636d4a7ecf600239fdc27615d2530bc16" dependencies = [ "atomic-waker", "bytes", @@ -2117,9 +2107,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.5.2" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" [[package]] name = "hex" @@ -2156,9 +2146,9 @@ dependencies = [ [[package]] name = "http" -version = "1.4.2" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6970f50e31d6fc17d3fa27329444bfa74e196cf62e95052a3f6fee181dba6425" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" dependencies = [ "bytes", "itoa", @@ -2176,9 +2166,9 @@ dependencies = [ [[package]] name = "http-body-util" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +checksum = "23169fe34a5fbcdd3f3862e78fb9b6fccd5f02a6dc6f732547005d45631ce71c" dependencies = [ "bytes", "futures-core", @@ -2223,9 +2213,9 @@ dependencies = [ [[package]] name = "hyper" -version = "1.10.1" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +checksum = "27b501faa50e7a26c3d3560ca625132f4078a17771f4810baf70475ae48cbe43" dependencies = [ "atomic-waker", "bytes", @@ -2322,9 +2312,9 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513" dependencies = [ "displaydoc", "potential_utf", @@ -2336,9 +2326,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", @@ -2349,9 +2339,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -2363,16 +2353,17 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" +checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" [[package]] name = "icu_properties" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148" dependencies = [ + "displaydoc", "icu_collections", "icu_locale_core", "icu_properties_data", @@ -2383,15 +2374,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" +checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa" [[package]] name = "icu_provider" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", @@ -2431,9 +2422,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.14.0" +version = "2.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +checksum = "07aa2048142242915a31d35844fb311e0e53fcca590c3a0a40dcf1b841fa09eb" dependencies = [ "equivalent", "hashbrown 0.17.1", @@ -2453,9 +2444,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.12.0" +version = "2.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" [[package]] name = "itertools" @@ -2474,9 +2465,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e184d09547b80eb7e20d141ba2fb1fbac843ca53f4cf1b31210adc4c1adc6e16" +checksum = "668b7183bd07af9a4885f5c35b0cc5c83c4607a913c16b7e17291832910d2dcc" dependencies = [ "defmt", "jiff-core", @@ -2500,9 +2491,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.34" +version = "0.2.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "323da076b7a6faf914dc677cb05a4b907742ff7375c8322c9e7f5061e5e0e9de" +checksum = "3a69dcb3a21cfb32ce1cd056169337ca284af0766dd766e7878819b251a49204" dependencies = [ "jiff-core", "proc-macro2", @@ -2586,9 +2577,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" +checksum = "0e0c1080212aad755ea003d18543e8768dd432c48819efd73a7bf1e39b7a5a3a" dependencies = [ "cfg-if", "futures-util", @@ -2644,9 +2635,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.186" +version = "0.2.189" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" [[package]] name = "libm" @@ -2656,14 +2647,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] name = "libredox" -version = "0.1.18" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c943259e342f1e06ff2da7a83eabdfe7f92ce10262688dbf1895ff0b3e6e4652" +checksum = "8d8f1ea3f21fd3405dcaf6c9b5c1630af9afc422d9073ea39c5f6d6c772e08ed" dependencies = [ "bitflags 2.13.1", "libc", "plain", - "redox_syscall 0.9.0", + "redox_syscall 0.9.3", ] [[package]] @@ -2685,9 +2676,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" +checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" [[package]] name = "lock_api" @@ -2700,9 +2691,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" [[package]] name = "lru-slab" @@ -2826,7 +2817,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "zeroize", ] @@ -2839,9 +2830,9 @@ checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" -version = "0.1.46" +version = "0.1.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b" dependencies = [ "num-traits", ] @@ -2872,7 +2863,7 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ - "hermit-abi 0.5.2", + "hermit-abi 0.5.3", "libc", ] @@ -3037,9 +3028,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pest" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47627dd7305c6a2d6c8c6bcd24c5a4c17dbbf425f4f9c5313e724b38fc9782e9" +checksum = "5a07a60cc7a4d00c91f95c685609d1d2f79050e6804b70ebedd7650f0b839bcf" dependencies = [ "memchr", "ucd-trie", @@ -3047,9 +3038,9 @@ dependencies = [ [[package]] name = "pest_derive" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b4254325ecad416ab689e27ba51da03ba01a9632bc6e108f5fe7c3c4ad29d58" +checksum = "b3a83744a5c8455b8b3e0dc5031362780a347c878bdd11584d1a8984228cc88d" dependencies = [ "pest", "pest_generator", @@ -3057,9 +3048,9 @@ dependencies = [ [[package]] name = "pest_generator" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c4c0e91ead7a8f7acecbca6f003fc2e8282b1dbe2dd9c9d2f16aba42995e0a7" +checksum = "e0cd3451aa3de60d4b9a1e736885e4dea6b31617598026f12256ad566d63304a" dependencies = [ "pest", "pest_meta", @@ -3070,9 +3061,9 @@ dependencies = [ [[package]] name = "pest_meta" -version = "2.8.7" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9744bc48116fee06334924bb5f2bad41eed5e89bd26e29b0b799f9a3f82c210" +checksum = "e04d3a0849e241d7dfce834c83b1c5edc8622009e8dd51a12ba1927c32f05496" dependencies = [ "pest", ] @@ -3126,9 +3117,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" [[package]] name = "plain" @@ -3138,9 +3129,9 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] name = "portable-atomic" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" [[package]] name = "portable-atomic-util" @@ -3153,9 +3144,9 @@ dependencies = [ [[package]] name = "potential_utf" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ "zerovec", ] @@ -3177,9 +3168,9 @@ dependencies = [ [[package]] name = "proc-macro-error-attr3" -version = "3.0.2" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34e4dd828515431dd6c4a030d26f7eaed7dd4778226e9d2bb968d65ca4ec3d4d" +checksum = "9e564d14133360e1ae169ffde5da25881b5fa47261665b8e5713c212c27799da" dependencies = [ "proc-macro2", "quote", @@ -3187,14 +3178,14 @@ dependencies = [ [[package]] name = "proc-macro-error3" -version = "3.0.2" +version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee475e440453418ff1335189eddf7101ba502cd818ab7ae04209bc83aa925aa" +checksum = "8f0d4471b3436c22106b21913b1dda531558918ae9b7ec55d58aa84b43552233" dependencies = [ "proc-macro-error-attr3", "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -3242,9 +3233,9 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.14.3" +version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8991c4cbdb8bc5b11f0b074ffe286c30e523de90fee5ba8132f1399f23cb3dd7" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" dependencies = [ "prost", ] @@ -3271,9 +3262,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.16" +version = "0.11.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +checksum = "04759210543be93709136e28212294a659ef5001836ff4eab4d663e4529bba83" dependencies = [ "aws-lc-rs", "bytes", @@ -3329,9 +3320,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a" +checksum = "e058c7de0b26af77780c769414d6257830bb240f3c38477dbc2c16e5f54d6d4c" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -3433,31 +3424,31 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.9.0" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5102a6aaa05aa011a238e178e6bca86d2cb56fc9f586d37cb80f5bca6e07759" +checksum = "d678d17679829e73d371e96880897e98fee2ded7acc0a50bdf8af2affa4b2fe5" dependencies = [ "bitflags 2.13.1", ] [[package]] name = "ref-cast" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" dependencies = [ "ref-cast-impl", ] [[package]] name = "ref-cast-impl" -version = "1.0.26" +version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -3474,9 +3465,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.16" +version = "0.4.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" dependencies = [ "aho-corasick", "memchr", @@ -3561,9 +3552,9 @@ dependencies = [ [[package]] name = "rovo" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3330342aa9d16f56b04bb8e9981754f8761effbb2194b6af23ccf56ac06fb830" +checksum = "a08fcd43f99a4c5a789e815feef185d6584f7cd2c094b7784ef437862b8cb80c" dependencies = [ "aide", "axum", @@ -3575,9 +3566,9 @@ dependencies = [ [[package]] name = "rovo-macros" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39ac0fd158aeb9b9d1c3208487e3f19c09273d76cad3ea9ad52a71d454f6d60b" +checksum = "6e92d027bc99c81ee8333965b9c97f1825bb29e0883fc30dda1cdfa75a792025" dependencies = [ "proc-macro2", "quote", @@ -3644,9 +3635,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.42" +version = "0.23.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c54fcab019b409d04215d3a17cb438fd7fbf192ee61461f20f4fe18704bc138" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" dependencies = [ "aws-lc-rs", "once_cell", @@ -3671,9 +3662,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ "web-time", "zeroize", @@ -3708,9 +3699,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "aws-lc-rs", "ring", @@ -3850,7 +3841,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -4054,9 +4045,9 @@ checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" -version = "1.15.2" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +checksum = "b9be42f50aa861c555654aa3a37f52f4b1074bacf4e48fe0ef7fa584e80f1f0f" dependencies = [ "serde", ] @@ -4208,7 +4199,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.7", + "rand 0.8.8", "rsa", "serde", "sha1", @@ -4247,7 +4238,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "sha2", @@ -4338,9 +4329,9 @@ dependencies = [ [[package]] name = "syn" -version = "3.0.2" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a207d6d6a2b7fc470b80443726053f18a2481b7e1eee970597051596567987a3" +checksum = "e6275cddf4610d1775e6d1fe9469b2e77d0f39fd98fb7450901b821e0c53649f" dependencies = [ "proc-macro2", "quote", @@ -4412,22 +4403,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.19" +version = "2.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -4441,9 +4432,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.54" +version = "0.3.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e1d5e639ff6bab73cb6885cc7e7b1de96c3f32c68ec55f3952614bec1092244" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" dependencies = [ "deranged", "num-conv", @@ -4480,9 +4471,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", "zerovec", @@ -4505,9 +4496,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.53.0" +version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d988bcd52dbe076d3d46903332f58c912b87a2c49b1428419a5845154762ffee" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ "bytes", "libc", @@ -4521,13 +4512,13 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6328af13490e73a9b4694030fafd93f8c8c6a9dede33e821c3fc63eddf8042ba" +checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] @@ -4542,9 +4533,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.18" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70" +checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b" dependencies = [ "futures-core", "pin-project-lite", @@ -4564,23 +4555,24 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" dependencies = [ "bytes", "futures-core", "futures-sink", "futures-util", + "libc", "pin-project-lite", "tokio", ] [[package]] name = "toml" -version = "1.1.3+spec-1.1.0" +version = "1.1.4+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53c96ecdfa941c8fc4fcaed14f99ada8ebed502eef533015095a07e3301d4c3c" +checksum = "3aace63f4bbcdfc2c965b059de67119c89c4017a70d633be6c104910f67056f5" dependencies = [ "serde_core", "serde_spanned", @@ -4600,9 +4592,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.1.2+spec-1.1.0" +version = "1.1.3+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +checksum = "1d38ac1cf9b95face32296c0a3ede1fdc270627c9d9c02a7274dd6d960dc4d56" dependencies = [ "winnow", ] @@ -4635,9 +4627,9 @@ dependencies = [ [[package]] name = "tonic-prost" -version = "0.14.5" +version = "0.14.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a55376a0bbaa4975a3f10d009ad763d8f4108f067c7c2e74f3001fb49778d309" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" dependencies = [ "bytes", "prost", @@ -4901,9 +4893,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.24.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +checksum = "b5772d71c9be8a8a6ac2117d949c5b224c1b72241bb611d9a3012edcf8af7812" [[package]] name = "validator" @@ -5000,9 +4992,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" [[package]] name = "wasm-bindgen" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" +checksum = "1b70935747edd64d89de3efa29d73789b806c15798f8e7dca4d8ac356b50ce70" dependencies = [ "cfg-if", "once_cell", @@ -5013,9 +5005,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.76" +version = "0.4.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c62df1340f32221cb9c54d6a27b030e3dba64361d4a95bed55f9aacb44da291d" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" dependencies = [ "js-sys", "wasm-bindgen", @@ -5023,9 +5015,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" +checksum = "77775f8f3f7217702089053b94958f8f54061a3f663417df76e19cbdcca29bc1" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -5033,9 +5025,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" +checksum = "e11d33f857dc2fb11b8bc75aee111aa9cbeb12cd9f25efd3d4c2a3dd4e235284" dependencies = [ "bumpalo", "proc-macro2", @@ -5046,18 +5038,18 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.126" +version = "0.2.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" +checksum = "7ef64dbcc55df09c7e5a46182d181c2cfa3e925f3da937ea764728b4bbb9dcbf" dependencies = [ "unicode-ident", ] [[package]] name = "web-sys" -version = "0.3.103" +version = "0.3.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8622dcb61c0bcc9fffa6938bed81210af2da9a7e4a1a834b2e37a59b6dfb6141" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" dependencies = [ "js-sys", "wasm-bindgen", @@ -5399,15 +5391,15 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" [[package]] name = "writeable" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" +checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" [[package]] name = "yaml-rust2" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "631a50d867fafb7093e709d75aaee9e0e0d5deb934021fcea25ac2fe09edc51e" +checksum = "b36710ce3a279cfce8465dbab826f161675a262950b922cb2c3663852dfe9eb0" dependencies = [ "arraydeque", "encoding_rs", @@ -5439,18 +5431,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" +checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.54" +version = "0.8.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" +checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1" dependencies = [ "proc-macro2", "quote", @@ -5500,9 +5492,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", "yoke", @@ -5511,9 +5503,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.6" +version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ "yoke", "zerofrom", @@ -5522,20 +5514,20 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.3" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +checksum = "34df6fc39dbd26ddc9c10e6a2984476e13acce22e64e4487636ef494369225da" dependencies = [ "proc-macro2", "quote", - "syn 2.0.119", + "syn 3.0.4", ] [[package]] name = "zlib-rs" -version = "0.6.6" +version = "0.6.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b142a20ec14a91d5bc708c1dc21b080c550113d8aa77afa29635673a65dd02c5" +checksum = "34b31d188d9d685a4f9c7b46d6e36631b07058d2cfe190267adce54dc230bf12" [[package]] name = "zmij" From 2393885bd6d87dca402817b5b9d5d21c2141360c Mon Sep 17 00:00:00 2001 From: Nilirad Date: Wed, 2 Sep 2026 10:27:03 +0200 Subject: [PATCH 50/50] Ignore typos on alphanumeric strings of 16 characters --- _typos.toml | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 _typos.toml diff --git a/_typos.toml b/_typos.toml new file mode 100644 index 0000000..6083cf7 --- /dev/null +++ b/_typos.toml @@ -0,0 +1,5 @@ +[default] +extend-ignore-re = [ + # 64-bit span IDs (W3C trace context) — hex substrings can look like typos, e.g. "ba" + "\\b[0-9a-fA-F]{16}\\b", +]