diff --git a/.env.example b/.env.example index 69f7e11..df05b6f 100644 --- a/.env.example +++ b/.env.example @@ -44,3 +44,11 @@ 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 +# RUST_LOG=commit_bridge=info +# CBRIDGE__TELEMETRY__MARK_CLIENT_ERRORS_AS_ERROR=true 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-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-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/.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" -} 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. diff --git a/Cargo.lock b/Cargo.lock index cb70ecc..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", ] @@ -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" @@ -89,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]] @@ -132,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", @@ -143,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", @@ -265,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", @@ -294,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", @@ -324,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", ] @@ -382,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", @@ -405,6 +411,9 @@ dependencies = [ "http", "humantime-serde", "jsonwebtoken", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", "rand 0.10.2", "reqwest", "rovo", @@ -420,21 +429,13 @@ dependencies = [ "tower", "tower-http", "tracing", + "tracing-opentelemetry", "tracing-subscriber", "url", "validator", "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" @@ -527,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", ] @@ -551,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", ] @@ -749,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]] @@ -799,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", ] @@ -855,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", ] @@ -892,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" @@ -942,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", @@ -957,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", @@ -967,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", @@ -995,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", @@ -1062,6 +1062,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" @@ -1071,7 +1083,7 @@ dependencies = [ "cfg-if", "js-sys", "libc", - "r-efi", + "r-efi 6.0.0", "rand_core 0.10.1", "wasm-bindgen", ] @@ -1141,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", @@ -1182,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", ] @@ -1211,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", @@ -1481,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", @@ -1530,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", @@ -1630,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", @@ -1778,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", @@ -1856,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" @@ -1910,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", @@ -1922,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", ] @@ -1985,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", @@ -2095,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" @@ -2134,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", @@ -2154,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", @@ -2201,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", @@ -2236,6 +2248,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" @@ -2287,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", @@ -2301,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", @@ -2314,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", @@ -2328,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", @@ -2348,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", @@ -2396,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", @@ -2418,9 +2444,18 @@ 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" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] [[package]] name = "itoa" @@ -2430,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", @@ -2456,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", @@ -2542,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", @@ -2600,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" @@ -2612,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]] @@ -2641,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" @@ -2656,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" @@ -2666,6 +2701,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" @@ -2773,7 +2817,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.7", + "rand 0.8.8", "smallvec", "zeroize", ] @@ -2786,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", ] @@ -2819,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", ] @@ -2850,6 +2894,68 @@ 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 = "opentelemetry-otlp" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9966929966d17620d7c316c643ba62631826e10021409357772d5eea84f62c35" +dependencies = [ + "http", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry_sdk", + "prost", + "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", + "tokio", + "tokio-stream", +] + [[package]] name = "ordered-multimap" version = "0.7.3" @@ -2922,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", @@ -2932,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", @@ -2942,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", @@ -2955,13 +3061,33 @@ 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", ] +[[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" @@ -2991,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" @@ -3003,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" @@ -3018,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", ] @@ -3042,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", @@ -3052,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]] @@ -3082,6 +3208,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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f94967dc7688f3054c7fac87473ffae4cc4c3904800e2d9f5b857246d8963b0a" +dependencies = [ + "prost", +] + [[package]] name = "quinn" version = "0.11.11" @@ -3104,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", @@ -3148,6 +3306,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" @@ -3156,15 +3320,25 @@ 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", "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" @@ -3186,6 +3360,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" @@ -3205,6 +3389,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" @@ -3231,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]] @@ -3272,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", @@ -3359,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", @@ -3373,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", @@ -3442,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", @@ -3469,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", @@ -3506,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", @@ -3648,7 +3841,7 @@ checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" dependencies = [ "proc-macro2", "quote", - "syn 3.0.2", + "syn 3.0.4", ] [[package]] @@ -3852,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", ] @@ -4006,7 +4199,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.7", + "rand 0.8.8", "rsa", "serde", "sha1", @@ -4045,7 +4238,7 @@ dependencies = [ "md-5", "memchr", "once_cell", - "rand 0.8.7", + "rand 0.8.8", "serde", "serde_json", "sha2", @@ -4136,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", @@ -4210,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]] @@ -4239,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", @@ -4278,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", @@ -4303,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", @@ -4319,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]] @@ -4340,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", @@ -4362,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", @@ -4398,13 +4592,61 @@ 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", ] +[[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.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50849f68853be452acf590cde0b146665b8d507b3b8af17261df47e02c209ea0" +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" @@ -4413,9 +4655,12 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", + "indexmap", "pin-project-lite", + "slab", "sync_wrapper", "tokio", + "tokio-util", "tower-layer", "tower-service", "tracing", @@ -4437,6 +4682,7 @@ dependencies = [ "tower", "tower-layer", "tower-service", + "tracing", "url", ] @@ -4496,16 +4742,36 @@ 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-subscriber" 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", ] @@ -4627,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" @@ -4709,6 +4975,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" @@ -4717,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", @@ -4730,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", @@ -4740,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", @@ -4750,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", @@ -4763,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", @@ -5108,17 +5383,23 @@ 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" +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", @@ -5150,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", @@ -5211,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", @@ -5222,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", @@ -5233,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" diff --git a/Cargo.toml b/Cargo.toml index 4fa3d2c..21fdc66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ 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" +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"] } @@ -37,9 +40,10 @@ 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 = "0.1.44" -tracing-subscriber = "0.3" +tracing-opentelemetry = "0.33.0" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } url = { version = "2.5.8", features = ["serde"] } validator = { version = "0.20.0", features = ["derive"] } 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", +] 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/config.rs b/src/config.rs index 4ae0c1d..39f48b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -35,23 +35,16 @@ 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 { /// 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); @@ -250,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/domain/branch_name.rs b/src/domain/branch_name.rs index 458b883..e8770cc 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; diff --git a/src/domain/commit_hash.rs b/src/domain/commit_hash.rs index 34dbb34..115baa9 100644 --- a/src/domain/commit_hash.rs +++ b/src/domain/commit_hash.rs @@ -1,7 +1,7 @@ //! Domain type to represent a git commit hash. use crate::error::ValidationError; -use rovo::schemars::JsonSchema; +use schemars::JsonSchema; use serde::{Deserialize, Serialize}; /// A git commit hash. diff --git a/src/domain/event_type.rs b/src/domain/event_type.rs index 813c43c..5e9b6a5 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; diff --git a/src/domain/repo_url.rs b/src/domain/repo_url.rs index 45a4bfd..408b60c 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; diff --git a/src/domain/target_repo.rs b/src/domain/target_repo.rs index ce81517..771a648 100644 --- a/src/domain/target_repo.rs +++ b/src/domain/target_repo.rs @@ -1,7 +1,7 @@ //! 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}; /// The target GitHub repository in owner/repo format. diff --git a/src/handler.rs b/src/handler.rs index a8a86f2..cb621b8 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,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.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, @@ -68,7 +76,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: {}): {:?}", @@ -82,7 +90,7 @@ async fn create_subscription_inner( } /// Query parameters for listing subscriptions. -#[derive(Debug, Deserialize, rovo::schemars::JsonSchema)] +#[derive(Debug, Deserialize, schemars::JsonSchema)] pub struct ListSubscriptionsQuery { /// Maximum number of subscriptions to return. pub limit: Option, @@ -111,6 +119,11 @@ pub struct ListSubscriptionsQuery { /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[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, @@ -131,13 +144,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() @@ -177,6 +193,7 @@ async fn list_subscriptions_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn get_subscription( state: State, Path(id): Path, @@ -191,7 +208,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))) @@ -219,6 +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.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, @@ -233,10 +257,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)?; @@ -264,6 +288,7 @@ async fn update_subscription_inner( /// @tag subscriptions #[allow(rustdoc::invalid_html_tags)] #[rovo] +#[instrument(skip_all, fields(otel.kind = "internal", id = %id))] pub async fn delete_subscription( state: State, Path(id): Path, @@ -276,7 +301,7 @@ 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(id).await?; Ok(()) } diff --git a/src/lib.rs b/src/lib.rs index 3b33ceb..535eb29 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,16 +5,18 @@ clippy::expect_used, clippy::todo, clippy::unimplemented, - clippy::indexing_slicing + clippy::indexing_slicing, + clippy::undocumented_unsafe_blocks )] use std::fs; use std::str::FromStr; +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, @@ -30,7 +32,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, OnResponse, TraceLayer}; +use tracing::{Span, info}; use crate::{ config::Config, @@ -58,6 +61,7 @@ pub mod model; pub mod polling; pub mod repository; pub mod state; +pub mod telemetry; #[cfg(test)] mod test_utils; #[cfg(test)] @@ -92,7 +96,28 @@ 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(()) +} + +/// Logs the outcome of the `.env` file load. +/// +/// Must only be called after the tracing subscriber is initialized. +pub 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. @@ -162,6 +187,10 @@ fn init_engines(ctx: &SharedContext, http_client: Client) -> Result, req: Request, @@ -174,8 +203,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 @@ -214,15 +246,90 @@ 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"))] pub async fn health_check(State(_state): State) -> &'static str { "CommitBridge is alive" } } +/// Span factory for incoming HTTP requests, +/// following OpenTelemetry semantic conventions. +/// +/// The span is created within this crate +/// (instead of using the default `tower_http` span factory) +/// so that its attributes follow OpenTelemetry conventions. +#[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(), + http.route = tracing::field::Empty, + http.response.status_code = tracing::field::Empty, + otel.status_code = tracing::field::Empty, + error.type = tracing::field::Empty, + authenticated = 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)] +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 self.should_mark_error(response.status()) { + span.record("otel.status_code", "ERROR"); + span.record("error.type", response.status().as_u16().to_string()); + } + } +} + /// Builds the application router. pub fn build_router( repository: std::sync::Arc, @@ -261,10 +368,18 @@ 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, )) + .layer( + TraceLayer::new_for_http() + .make_span_with(HttpRequestSpan) + .on_response(HttpRequestOnResponse::new( + config.telemetry.mark_client_errors_as_error, + )), + ) } /// Runs the server. diff --git a/src/main.rs b/src/main.rs index 8a3710b..a4fbaf5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,29 +5,34 @@ clippy::expect_used, clippy::todo, clippy::unimplemented, - clippy::indexing_slicing + clippy::indexing_slicing, + clippy::undocumented_unsafe_blocks )] -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}; #[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}")); + 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; - info!("All systems terminated. Terminating process.") + + 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 1cdebf7..3cffc4d 100644 --- a/src/model.rs +++ b/src/model.rs @@ -14,7 +14,7 @@ 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; @@ -222,4 +222,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/git.rs b/src/polling/git.rs index 06cbed7..2185397 100644 --- a/src/polling/git.rs +++ b/src/polling/git.rs @@ -44,55 +44,76 @@ impl MainGitFetcher { #[async_trait] impl GitFetcher for MainGitFetcher { + #[tracing::instrument( + skip_all, + 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 bbcc32b..2234b84 100644 --- a/src/polling/mod.rs +++ b/src/polling/mod.rs @@ -48,6 +48,9 @@ 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> { @@ -56,6 +59,34 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { 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( + otel.kind = "internal", + otel.status_code = tracing::field::Empty, + ) +)] +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 [`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 @@ -69,7 +100,9 @@ async fn poll_branches(ctx: &SharedContext) -> Result<(), PollingError> { /// Gathers stored branches that need to be updated. 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, @@ -105,6 +138,7 @@ fn execute_branch_updates<'a>( } /// Processes branch updates within a transaction. +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn process_branches( repo: std::sync::Arc, shared_branches: std::sync::Arc>, @@ -112,17 +146,58 @@ 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?; - - info!( - "New commit detected for branch {}. Hash: {}", - branch_info.branch.name, branch_info.latest_hash - ); + process_single_branch(repo.clone(), branch_info, tx).await?; + } + Ok(()) +} - repo.queue_triggers_for_branch(branch_info.branch.id, &branch_info.latest_hash, tx) - .await?; +/// Processes a single branch update within a transaction. +#[tracing::instrument( + 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(), + 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( + 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?; + + info!( + "New commit detected for branch {}. Hash: {}", + branch_info.branch.name, branch_info.latest_hash + ); + + let span_context = crate::telemetry::serialize_current_span_context(); + + 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(), + }; + repo.trigger_queue_upsert(trigger_params, tx).await?; Ok(()) } diff --git a/src/repository/branch.rs b/src/repository/branch.rs index 87edf99..92f8527 100644 --- a/src/repository/branch.rs +++ b/src/repository/branch.rs @@ -8,23 +8,10 @@ use async_trait::async_trait; #[async_trait] pub trait BranchRepository: Send + Sync { /// Returns all branches. - async fn get_all(&self) -> Result, RepositoryError>; - - /// Returns the branch with the given `id`. - async fn 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_get_all(&self) -> Result, RepositoryError>; /// Updates the last commit hash of the branch. - async fn 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( &self, id: i64, hash: &crate::domain::CommitHash, diff --git a/src/repository/sqlite.rs b/src/repository/sqlite.rs index 55f7d33..965892b 100644 --- a/src/repository/sqlite.rs +++ b/src/repository/sqlite.rs @@ -28,12 +28,8 @@ 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, 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, @@ -49,50 +45,16 @@ impl SqliteRepository { #[async_trait] impl BranchRepository for SqliteRepository { - async fn get_all(&self) -> Result, RepositoryError> { + #[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) .await .map_err(RepositoryError::Database) } - async fn 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) - } - - async fn 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(()) - } - - async fn 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(()) - } - - async fn update_last_commit_hash_in_tx( + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] + async fn branches_update_last_commit_hash( &self, id: i64, hash: &crate::domain::CommitHash, @@ -112,7 +74,8 @@ impl BranchRepository for SqliteRepository { #[async_trait] impl SubscriptionRepository for SqliteRepository { - async fn create( + #[tracing::instrument(skip_all, fields(otel.kind = "client"))] + async fn subscriptions_create( &self, subscription_payload: &CreateSubscription, ) -> Result { @@ -154,15 +117,8 @@ impl SubscriptionRepository for SqliteRepository { }) } - async fn 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) - } - - async fn get_by_id_with_branch( + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] + async fn subscriptions_get_by_id_with_branch( &self, id: i64, ) -> Result, RepositoryError> { @@ -201,7 +157,16 @@ impl SubscriptionRepository for SqliteRepository { } } - async fn get_by_keys_with_branch( + #[tracing::instrument( + skip_all, + 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, branch_id: i64, target_repo: &TargetRepo, @@ -244,22 +209,11 @@ impl SubscriptionRepository for SqliteRepository { } } - 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) - } - - async fn list_paginated_with_branches( + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", last_id = %last_id, limit = %limit) + )] + async fn subscriptions_list_paginated( &self, last_id: i64, limit: i64, @@ -303,14 +257,16 @@ impl SubscriptionRepository for SqliteRepository { subscriptions } - async fn count_remaining(&self, last_id: i64) -> Result { + #[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) .await .map_err(RepositoryError::Database) } - async fn update( + #[tracing::instrument(skip_all, fields(otel.kind = "client", id = %id))] + async fn subscriptions_update( &self, id: i64, subscription: &UpdateSubscription, @@ -348,42 +304,8 @@ impl SubscriptionRepository for SqliteRepository { .ok_or(RepositoryError::NotFound) } - async fn 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(()) - } - - async fn 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) - } - - async fn 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) - } - - async fn delete_subscription_and_cascade(&self, id: i64) -> Result<(), RepositoryError> { + #[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 { let branch_id = sqlx::query_scalar!( @@ -419,14 +341,8 @@ impl SubscriptionRepository for SqliteRepository { #[async_trait] impl TriggerRepository for SqliteRepository { - async fn get_all(&self) -> Result, RepositoryError> { - sqlx::query_as::<_, TriggerQueueItem>("SELECT * FROM trigger_queue") - .fetch_all(&self.pool) - .await - .map_err(RepositoryError::Database) - } - - async fn delete_by_id(&self, id: i64) -> Result<(), RepositoryError> { + #[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) .await @@ -434,7 +350,8 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - async fn find_oldest_pending_and_mark_processing( + #[tracing::instrument(skip_all, fields(otel.kind = "client"))] + async fn trigger_queue_process_oldest_pending( &self, ) -> Result, RepositoryError> { let trigger = sqlx::query_as::<_, TriggerQueueItem>( @@ -445,7 +362,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 @@ -454,7 +371,14 @@ impl TriggerRepository for SqliteRepository { Ok(trigger) } - async fn update_retry_status(&self, params: UpdateRetryStatus) -> Result<(), RepositoryError> { + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", id = %params.id, retry_count = %params.retry_count) + )] + 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 { @@ -481,7 +405,14 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - async fn recover_stuck_tasks(&self, threshold_seconds: u64) -> Result<(), RepositoryError> { + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", 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!( @@ -497,21 +428,28 @@ impl TriggerRepository for SqliteRepository { Ok(()) } - async fn queue_triggers_for_branch( + #[tracing::instrument( + skip_all, + fields(otel.kind = "client", branch_id = %params.branch_id) + )] + async fn trigger_queue_upsert( &self, - branch_id: i64, - new_hash: &crate::domain::CommitHash, + params: crate::repository::trigger::TriggerQueueUpsertParams<'_>, 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/subscription.rs b/src/repository/subscription.rs index 453edd4..37c90ef 100644 --- a/src/repository/subscription.rs +++ b/src/repository/subscription.rs @@ -9,22 +9,19 @@ 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>; - /// 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 +32,22 @@ 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>; - - /// Returns the branch ID associated to the given subscription's `id`. - async fn 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( - &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(&self, id: i64) -> Result<(), RepositoryError>; } diff --git a/src/repository/trigger.rs b/src/repository/trigger.rs index 8bbdb34..8f8be20 100644 --- a/src/repository/trigger.rs +++ b/src/repository/trigger.rs @@ -20,31 +20,46 @@ pub struct UpdateRetryStatus { pub backoff_base_secs: u64, } +/// Parameters for upserting trigger events for a branch. +#[derive(Debug, Clone)] +pub struct TriggerQueueUpsertParams<'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 { - /// Returns all the trigger queue items. - async fn 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(&self, id: i64) -> Result<(), RepositoryError>; /// Queues trigger events for all subscriptions of a branch. - async fn queue_triggers_for_branch( + async fn trigger_queue_upsert( &self, - branch_id: i64, - new_hash: &crate::domain::CommitHash, + params: TriggerQueueUpsertParams<'_>, executor: &mut sqlx::SqliteConnection, ) -> Result<(), RepositoryError>; } diff --git a/src/telemetry.rs b/src/telemetry.rs new file mode 100644 index 0000000..0ad7ea1 --- /dev/null +++ b/src/telemetry.rs @@ -0,0 +1,232 @@ +//! OpenTelemetry telemetry helpers. + +use opentelemetry::{ + global, + trace::{TraceContextExt, TracerProvider}, +}; +use opentelemetry_sdk::{Resource, trace::SdkTracerProvider}; +use thiserror::Error; +use tracing::{error, warn}; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::{ + EnvFilter, Layer, filter::LevelFilter, 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. +/// +/// 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 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. +/// +/// 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() + { + error!("Failed to gracefully shut down tracer provider: {e}"); + } + } +} + +/// 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()); + + let tracer_provider = init_tracer_provider(); + + 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)); + + tracing_subscriber::registry() + .with(tracing_subscriber::fmt::layer().with_filter(env_filter)) + .with(otel_layer.with_filter(LevelFilter::INFO)) + .init(); + + if let Err(reason) = &tracer_provider { + warn!("{reason}"); + } + + #[cfg(debug_assertions)] + warn!("APPLICATION IS RUNNING IN DEBUG MODE."); + + TelemetryGuard(tracer_provider.ok()) +} + +/// 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| 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; + }; + + 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); + } +} + +/// 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 env_var_is_truthy("OTEL_SDK_DISABLED") { + 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(build_resource()) + .build()) +} + +/// Builds the tracer provider resource. +/// +/// The SDK-provided resource attributes are kept +/// (`telemetry.sdk.*` and `OTEL_RESOURCE_ATTRIBUTES`), +/// 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 { + 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(), + } +} + +/// 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()) + }) + }) +} + +/// Returns `true` if an OTLP endpoint has been explicitly configured +/// through the standard OpenTelemetry environment variables. +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") +} + +/// 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()) +} + +/// Returns `true` if the environment variable is set to a truthy value (`true` or `1`). +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. +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)); + + Ok(context) +} 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..0b2947e 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -9,4 +9,6 @@ pub mod api_routes; 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..dcccb0a --- /dev/null +++ b/src/tests/telemetry_tests.rs @@ -0,0 +1,270 @@ +use crate::telemetry::{ + add_link_from_serialized_context, build_resource, configured_service_name, + deserialize_span_context, env_var_is_truthy, otlp_endpoint_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); + } +} + +/// 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 + .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_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_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_eq!(configured_service_name().as_deref(), Some("from-attrs")); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); + + assert_eq!(configured_service_name(), None); +} + +#[test] +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_eq!(configured_service_name(), None); + remove_env("OTEL_SERVICE_NAME"); + + set_env("OTEL_RESOURCE_ATTRIBUTES", "service.name="); + assert_eq!(configured_service_name(), None); + remove_env("OTEL_RESOURCE_ATTRIBUTES"); +} + +#[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!( + resource + .get(&SERVICE_NAME_KEY) + .map(|v| v.to_string()) + .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(); + 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(); + 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"); + 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(); + 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"); + + 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()); +} diff --git a/src/trigger/auth.rs b/src/trigger/auth.rs index 5b60151..a35ca75 100644 --- a/src/trigger/auth.rs +++ b/src/trigger/auth.rs @@ -33,6 +33,17 @@ pub struct GitHubAuthenticator { #[async_trait] impl Authenticator for GitHubAuthenticator { + #[tracing::instrument( + skip_all, + 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, subscription: &Subscription, diff --git a/src/trigger/mod.rs b/src/trigger/mod.rs index 7d5504c..7ed3762 100644 --- a/src/trigger/mod.rs +++ b/src/trigger/mod.rs @@ -55,38 +55,117 @@ 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 - .find_oldest_pending_and_mark_processing() + .trigger_queue_process_oldest_pending() .await? else { return Ok(()); }; + process_trigger(engine, trigger).await +} + +/// Processes a single queued trigger. +#[tracing::instrument( + 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, + 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_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_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(), + ); + + 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 { - Ok(_) => { - TriggerRepository::delete_by_id(&*engine.ctx.repository, 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 ); - TriggerRepository::delete_by_id(&*engine.ctx.repository, 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(()) } /// Schedules the next retry for a trigger in the `trigger_queue`. +#[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, @@ -102,6 +181,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, @@ -112,7 +194,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, @@ -124,13 +206,15 @@ async fn schedule_retry( } /// Recovers tasks that have been stuck in `PROCESSING` for too long. +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn recover_stuck_tasks( repo: &crate::repository::SqliteRepository, config: &crate::config::Config, ) -> 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(()) } @@ -138,6 +222,7 @@ pub async fn recover_stuck_tasks( /// /// /// [`Subscription`]: crate::model::Subscription +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] pub async fn dispatch_events( engine: &TriggerEngine, trigger: &TriggerQueueItem, @@ -145,7 +230,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) @@ -173,6 +262,7 @@ pub async fn dispatch_events( /// /// /// [`Subscription`]: crate::model::Subscription +#[tracing::instrument(skip_all, fields(otel.kind = "internal"))] async fn notify_subscription( engine: &TriggerEngine, iat: String, @@ -187,6 +277,14 @@ async fn notify_subscription( /// /// /// [`Subscription`]: crate::model::Subscription +#[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, @@ -249,6 +347,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?, @@ -386,7 +487,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(); @@ -426,6 +527,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 {