Observability #6
@@ -12,8 +12,12 @@
|
|||||||
"containerEnv": {
|
"containerEnv": {
|
||||||
"SHELL": "/bin/bash"
|
"SHELL": "/bin/bash"
|
||||||
},
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": ["fill-labs.dependi", "rust-lang.rust-analyzer", "tamasfe.even-better-toml"]
|
||||||
|
}
|
||||||
|
},
|
||||||
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
"workspaceMount": "source=${localWorkspaceFolder},target=/workspaces/herald,type=bind",
|
||||||
"workspaceFolder": "/workspaces/herald",
|
"workspaceFolder": "/workspaces/herald",
|
||||||
"runArgs": ["--userns=keep-id", "--security-opt", "label=disable"],
|
|
||||||
"appPort": [3000]
|
"appPort": [3000]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,3 +17,6 @@ GITEA_TIMEOUT=30
|
|||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
RUST_LOG=info
|
RUST_LOG=info
|
||||||
RUST_BACKTRACE=1
|
RUST_BACKTRACE=1
|
||||||
|
|
||||||
|
|
||||||
|
METRICS_BIND_ADDR=
|
||||||
|
qpismont marked this conversation as resolved
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"rust-analyzer.check.command": "clippy"
|
||||||
|
}
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
{
|
|
||||||
"lsp": {
|
|
||||||
"rust-analyzer": {
|
|
||||||
"initialization_options": {
|
|
||||||
"check": {
|
|
||||||
"command": "clippy"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -34,7 +34,7 @@ dependencies = [
|
|||||||
"bytestring",
|
"bytestring",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"http 0.2.12",
|
"http 0.2.12",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -129,7 +129,7 @@ dependencies = [
|
|||||||
"cfg-if",
|
"cfg-if",
|
||||||
"derive_more",
|
"derive_more",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
"foldhash",
|
"foldhash 0.1.5",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"impl-more",
|
"impl-more",
|
||||||
@@ -280,15 +280,6 @@ version = "2.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "block-buffer"
|
|
||||||
version = "0.12.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
|
|
||||||
dependencies = [
|
|
||||||
"hybrid-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block2"
|
name = "block2"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -341,18 +332,6 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cmov"
|
|
||||||
version = "0.5.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "const-oid"
|
|
||||||
version = "0.10.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "convert_case"
|
name = "convert_case"
|
||||||
version = "0.10.0"
|
version = "0.10.0"
|
||||||
@@ -379,31 +358,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cpufeatures"
|
name = "crossbeam-epoch"
|
||||||
version = "0.3.0"
|
version = "0.9.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
|
checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"crossbeam-utils",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crypto-common"
|
name = "crossbeam-utils"
|
||||||
version = "0.2.2"
|
version = "0.8.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
|
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||||
dependencies = [
|
|
||||||
"hybrid-array",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "ctutils"
|
|
||||||
version = "0.4.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
|
|
||||||
dependencies = [
|
|
||||||
"cmov",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "darling"
|
name = "darling"
|
||||||
@@ -523,18 +490,6 @@ dependencies = [
|
|||||||
"unicode-xid",
|
"unicode-xid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "digest"
|
|
||||||
version = "0.11.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
|
|
||||||
dependencies = [
|
|
||||||
"block-buffer",
|
|
||||||
"const-oid",
|
|
||||||
"crypto-common",
|
|
||||||
"ctutils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch2"
|
name = "dispatch2"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -605,6 +560,17 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "evmap"
|
||||||
|
version = "11.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1b8874945f036109c72242964c1174cf99434e30cfa45bf45fedc983f50046f8"
|
||||||
|
dependencies = [
|
||||||
|
"hashbag",
|
||||||
|
"left-right",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastrand"
|
name = "fastrand"
|
||||||
version = "2.4.1"
|
version = "2.4.1"
|
||||||
@@ -641,6 +607,12 @@ version = "0.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foldhash"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.3.2"
|
version = "0.3.2"
|
||||||
@@ -726,6 +698,21 @@ dependencies = [
|
|||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "generator"
|
||||||
|
version = "0.8.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b3b854b0e584ead1a33f18b2fcad7cf7be18b3875c78816b753639aa501513ae"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"rustversion",
|
||||||
|
"windows-link",
|
||||||
|
"windows-result",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.17"
|
version = "0.2.17"
|
||||||
@@ -778,6 +765,21 @@ dependencies = [
|
|||||||
"tracing",
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbag"
|
||||||
|
version = "0.1.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7040a10f52cba493ddb09926e15d10a9d8a28043708a405931fe4c6f19fac064"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hashbrown"
|
||||||
|
version = "0.16.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
|
||||||
|
dependencies = [
|
||||||
|
"foldhash 0.2.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.17.1"
|
version = "0.17.1"
|
||||||
@@ -786,7 +788,7 @@ checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "herald"
|
name = "herald"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
@@ -794,15 +796,15 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"hex",
|
"hex",
|
||||||
"hmac",
|
"metrics",
|
||||||
|
"metrics-exporter-prometheus",
|
||||||
"openrouter-rs",
|
"openrouter-rs",
|
||||||
"reqwest 0.12.28",
|
"reqwest 0.12.28",
|
||||||
|
"ring",
|
||||||
"sentry",
|
"sentry",
|
||||||
"sentry-anyhow",
|
"sentry-anyhow",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sha2",
|
|
||||||
"subtle",
|
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
@@ -819,15 +821,6 @@ version = "0.4.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hmac"
|
|
||||||
version = "0.13.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
|
|
||||||
dependencies = [
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hostname"
|
name = "hostname"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
@@ -895,15 +888,6 @@ version = "1.0.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "hybrid-array"
|
|
||||||
version = "0.4.12"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
|
|
||||||
dependencies = [
|
|
||||||
"typenum",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "1.9.0"
|
version = "1.9.0"
|
||||||
@@ -1103,7 +1087,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown",
|
"hashbrown 0.17.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1142,6 +1126,17 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "left-right"
|
||||||
|
version = "0.11.8"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8bc015ded5d9b3054dbbdb63332cdd6ee42352ccef19e911e25117490e2f48ee"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"loom",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
@@ -1181,6 +1176,19 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "loom"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"generator",
|
||||||
|
"scoped-tls",
|
||||||
|
"tracing",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lru-slab"
|
name = "lru-slab"
|
||||||
version = "0.1.2"
|
version = "0.1.2"
|
||||||
@@ -1208,12 +1216,70 @@ version = "2.8.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics"
|
||||||
|
version = "0.24.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "89550ee9f79e88fef3119de263694973a8adb26c21d75322164fb8c493039fe2"
|
||||||
|
dependencies = [
|
||||||
|
"portable-atomic",
|
||||||
|
"rapidhash",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics-exporter-prometheus"
|
||||||
|
version = "0.18.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1db0d8f1fc9e62caebd0319e11eaec5822b0186c171568f0480b46a0137f9108"
|
||||||
|
dependencies = [
|
||||||
|
"base64",
|
||||||
|
"evmap",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"indexmap",
|
||||||
|
"ipnet",
|
||||||
|
"metrics",
|
||||||
|
"metrics-util",
|
||||||
|
"quanta",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "metrics-util"
|
||||||
|
version = "0.20.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "96f8722f8562635f92f8ed992f26df0532266eb03d5202607c20c0d7e9745e13"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-epoch",
|
||||||
|
"crossbeam-utils",
|
||||||
|
"hashbrown 0.16.1",
|
||||||
|
"metrics",
|
||||||
|
"quanta",
|
||||||
|
"rand",
|
||||||
|
"rand_xoshiro",
|
||||||
|
"rapidhash",
|
||||||
|
"sketches-ddsketch",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mime"
|
name = "mime"
|
||||||
version = "0.3.17"
|
version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mime_guess"
|
||||||
|
version = "2.0.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||||
|
dependencies = [
|
||||||
|
"mime",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
@@ -1455,9 +1521,9 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openrouter-rs"
|
name = "openrouter-rs"
|
||||||
version = "0.10.0"
|
version = "0.12.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7f7101578df2f54d9013594e94367adbe656521ef6002f03e4577f1e00e57cb4"
|
checksum = "25918cfeef40cfd3adea348b268496ee5206eb0408f2aacfe4bdfef2bfbe3bf9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"derive_builder",
|
"derive_builder",
|
||||||
"dotenvy_macro",
|
"dotenvy_macro",
|
||||||
@@ -1602,6 +1668,12 @@ version = "0.3.33"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "portable-atomic"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3d20d5497ef88037a52ff98267d066e7f11fcc5e99bbfbd58a42336193aacec3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1635,6 +1707,21 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quanta"
|
||||||
|
version = "0.12.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"raw-cpuid",
|
||||||
|
"wasi",
|
||||||
|
"web-sys",
|
||||||
|
"winapi",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quinn"
|
name = "quinn"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -1734,6 +1821,33 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rand_xoshiro"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41"
|
||||||
|
dependencies = [
|
||||||
|
"rand_core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rapidhash"
|
||||||
|
version = "4.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5da7e78a036ce858e8d55b7e7dc8ba3a88b78350fd2155d3591bbd966b58589e"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "raw-cpuid"
|
||||||
|
version = "11.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "redox_syscall"
|
name = "redox_syscall"
|
||||||
version = "0.5.18"
|
version = "0.5.18"
|
||||||
@@ -1816,6 +1930,7 @@ dependencies = [
|
|||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
|
"mime_guess",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -2007,6 +2122,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "scoped-tls"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "scopeguard"
|
name = "scopeguard"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
@@ -2268,17 +2389,6 @@ dependencies = [
|
|||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sha2"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"cpufeatures",
|
|
||||||
"digest",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sharded-slab"
|
name = "sharded-slab"
|
||||||
version = "0.1.7"
|
version = "0.1.7"
|
||||||
@@ -2304,6 +2414,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sketches-ddsketch"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -2516,9 +2632,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.53.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -2701,12 +2817,6 @@ version = "0.2.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "typenum"
|
|
||||||
version = "1.20.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uname"
|
name = "uname"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2716,6 +2826,12 @@ dependencies = [
|
|||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -2981,6 +3097,15 @@ version = "0.2.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-result"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
|
||||||
|
dependencies = [
|
||||||
|
"windows-link",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.52.0"
|
version = "0.52.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "herald"
|
name = "herald"
|
||||||
version = "1.0.1"
|
version = "1.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -8,7 +8,7 @@ debug = 1
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
|
||||||
tokio = { version = "1.52", features = ["full"] }
|
tokio = { version = "1.53", features = ["full"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
tokio-util = "0.7"
|
tokio-util = "0.7"
|
||||||
futures-util = "0.3"
|
futures-util = "0.3"
|
||||||
@@ -16,7 +16,7 @@ serde_json = "1.0"
|
|||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sentry = { version = "0.48", features = ["tower-axum-matched-path"] }
|
sentry = { version = "0.48", features = ["tower-axum-matched-path"] }
|
||||||
sentry-anyhow = { version = "0.48", features = ["backtrace"] }
|
sentry-anyhow = { version = "0.48", features = ["backtrace"] }
|
||||||
openrouter-rs = "0.10"
|
openrouter-rs = "0.12"
|
||||||
dotenvy = "0.15"
|
dotenvy = "0.15"
|
||||||
tower = "0.5"
|
tower = "0.5"
|
||||||
tower-http = {version = "0.6", features = ["trace"] }
|
tower-http = {version = "0.6", features = ["trace"] }
|
||||||
@@ -25,8 +25,8 @@ tracing-subscriber = { version = "0.3", features=["env-filter"] }
|
|||||||
axum = "0.8"
|
axum = "0.8"
|
||||||
anyhow = { version = "1.0", features = ["backtrace"] }
|
anyhow = { version = "1.0", features = ["backtrace"] }
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
hmac = "0.13"
|
ring = "0.17"
|
||||||
sha2 = "0.11"
|
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
subtle = "2.6"
|
bytes = "1.1"
|
||||||
bytes = "1.11"
|
metrics = "0.24"
|
||||||
|
metrics-exporter-prometheus = { version = "0.18", default-features = false, features = ["http-listener"] }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ Herald is a Gitea bot that performs automated AI-powered code reviews on pull re
|
|||||||
- Concurrent review processing with configurable parallelism
|
- Concurrent review processing with configurable parallelism
|
||||||
- Graceful shutdown — in-progress reviews finish before the process exits
|
- Graceful shutdown — in-progress reviews finish before the process exits
|
||||||
- Error tracking via Sentry
|
- Error tracking via Sentry
|
||||||
|
- Prometheus metrics endpoint for monitoring
|
||||||
- Tiny memory footprint (~4MB) thanks to Rust
|
- Tiny memory footprint (~4MB) thanks to Rust
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
@@ -34,6 +35,7 @@ Herald reads its configuration from environment variables (a `.env` file is supp
|
|||||||
| `GITEA_URL` | Base URL of your Gitea instance |
|
| `GITEA_URL` | Base URL of your Gitea instance |
|
||||||
| `GITEA_TOKEN` | Gitea API token |
|
| `GITEA_TOKEN` | Gitea API token |
|
||||||
| `GITEA_TIMEOUT` | Gitea API request timeout in seconds |
|
| `GITEA_TIMEOUT` | Gitea API request timeout in seconds |
|
||||||
|
| `METRICS_BIND_ADDR` | *(optional)* Bind address for the Prometheus metrics endpoint (e.g. `0.0.0.0:9100`). If unset, the metrics exporter is disabled. |
|
||||||
| `SENTRY_DSN` | *(optional)* Sentry DSN for error tracking |
|
| `SENTRY_DSN` | *(optional)* Sentry DSN for error tracking |
|
||||||
| `RUST_LOG` | *(optional)* Log level, defaults to `info` |
|
| `RUST_LOG` | *(optional)* Log level, defaults to `info` |
|
||||||
|
|
||||||
@@ -51,3 +53,40 @@ cargo run
|
|||||||
```
|
```
|
||||||
|
|
||||||
Copy `.env.example` to `.env` and fill in your values before running.
|
Copy `.env.example` to `.env` and fill in your values before running.
|
||||||
|
|
||||||
|
## Metrics
|
||||||
|
|
||||||
|
Herald optionally exposes a Prometheus metrics endpoint, useful for scraping with an [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or any Prometheus-compatible scraper.
|
||||||
|
|
||||||
|
Set `METRICS_BIND_ADDR` (e.g. `0.0.0.0:9100`) to enable it. The metrics are then available at `http://<host>:9100/metrics`.
|
||||||
|
|
||||||
|
### Exposed metrics
|
||||||
|
|
||||||
|
| Metric | Type | Description |
|
||||||
|
|---|---|---|
|
||||||
|
| `herald_webhooks_received_total` | counter | Total webhooks received (label: `event_type`) |
|
||||||
|
| `herald_webhooks_duplicate_total` | counter | Webhooks rejected as duplicates (label: `event_type`) |
|
||||||
|
| `herald_webhooks_channel_full_total` | counter | Webhooks dropped because the bot channel was full (label: `event_type`) |
|
||||||
|
| `herald_bot_tasks_active` | gauge | Bot tasks currently in progress |
|
||||||
|
| `herald_bot_tasks_completed_total` | counter | Bot tasks completed successfully (label: `event_type`) |
|
||||||
|
| `herald_bot_tasks_failed_total` | counter | Bot tasks that failed (label: `event_type`) |
|
||||||
|
| `herald_openrouter_cost_cents_total` | counter | Total OpenRouter cost in cents (divide by 100 for USD) |
|
||||||
|
|
||||||
|
### OTel collector example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
receivers:
|
||||||
|
prometheus:
|
||||||
|
config:
|
||||||
|
scrape_configs:
|
||||||
|
- job_name: herald
|
||||||
|
scrape_interval: 15s
|
||||||
|
static_configs:
|
||||||
|
- targets: ["herald:9100"]
|
||||||
|
|
||||||
|
service:
|
||||||
|
pipelines:
|
||||||
|
metrics:
|
||||||
|
receivers: [prometheus]
|
||||||
|
exporters: [otlp]
|
||||||
|
```
|
||||||
@@ -4,12 +4,10 @@ use axum::http::Request;
|
|||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use axum::routing::{get, post};
|
use axum::routing::{get, post};
|
||||||
use axum::{Json, Router};
|
use axum::{Json, Router};
|
||||||
use hmac::{Hmac, KeyInit, Mac};
|
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
use ring::hmac;
|
||||||
use sentry::integrations::tower::{NewSentryLayer, SentryHttpLayer};
|
use sentry::integrations::tower::{NewSentryLayer, SentryHttpLayer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use sha2::Sha256;
|
|
||||||
use subtle::ConstantTimeEq;
|
|
||||||
use tower::ServiceBuilder;
|
use tower::ServiceBuilder;
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
use tracing::{info, instrument};
|
use tracing::{info, instrument};
|
||||||
@@ -19,6 +17,7 @@ use tokio_util::sync::CancellationToken;
|
|||||||
use crate::consts::{GITEA_EVENT_TYPE_HEADER_NAME, GITEA_SIG_HEADER_NAME, MAX_WEBHOOK_BODY_SIZE};
|
use crate::consts::{GITEA_EVENT_TYPE_HEADER_NAME, GITEA_SIG_HEADER_NAME, MAX_WEBHOOK_BODY_SIZE};
|
||||||
use crate::errors::AppError;
|
use crate::errors::AppError;
|
||||||
use crate::gitea::WebhookType;
|
use crate::gitea::WebhookType;
|
||||||
|
use crate::metrics;
|
||||||
use crate::state::AppState;
|
use crate::state::AppState;
|
||||||
|
|
||||||
pub async fn start(app_state: AppState, shutdown: CancellationToken) -> anyhow::Result<()> {
|
pub async fn start(app_state: AppState, shutdown: CancellationToken) -> anyhow::Result<()> {
|
||||||
@@ -58,13 +57,18 @@ async fn webhook(
|
|||||||
) -> Result<impl IntoResponse, AppError> {
|
) -> Result<impl IntoResponse, AppError> {
|
||||||
tracing::Span::current().record("webhook_type", tracing::field::debug(&wb));
|
tracing::Span::current().record("webhook_type", tracing::field::debug(&wb));
|
||||||
|
|
||||||
|
let event_type = wb.event_type_str();
|
||||||
|
metrics::webhook_received(event_type);
|
||||||
|
|
||||||
let event_id = wb.event_id();
|
let event_id = wb.event_id();
|
||||||
if app_state.bot.check_and_mark(event_id).await {
|
if app_state.bot.check_and_mark(event_id).await {
|
||||||
|
metrics::webhook_duplicate(event_type);
|
||||||
return Err(AppError::AlreadyProcessedErr);
|
return Err(AppError::AlreadyProcessedErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
if app_state.bot_tx.try_send(wb).is_err() {
|
if app_state.bot_tx.try_send(wb).is_err() {
|
||||||
app_state.bot.unmark(event_id).await;
|
app_state.bot.unmark(event_id).await;
|
||||||
|
metrics::webhook_channel_full(event_type);
|
||||||
return Err(AppError::ChannelFullErr);
|
return Err(AppError::ChannelFullErr);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +109,7 @@ where
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
let webhook = parse_webhook(&type_header, &app_state.config.bot_name, &body_bytes)?;
|
let webhook = parse_webhook(&type_header, &app_state.bot.name(), &body_bytes)?;
|
||||||
Ok(WebhookExtract(webhook))
|
Ok(WebhookExtract(webhook))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,12 +141,7 @@ fn parse_webhook(header: &str, bot_name: &str, body_bytes: &[u8]) -> Result<Webh
|
|||||||
fn verify_signature(secret_key: &[u8], sig_header: &str, body: &[u8]) -> Result<(), AppError> {
|
fn verify_signature(secret_key: &[u8], sig_header: &str, body: &[u8]) -> Result<(), AppError> {
|
||||||
let sig_header_decoded =
|
let sig_header_decoded =
|
||||||
hex::decode(sig_header).map_err(|_| AppError::WebHookSigHeaderInvalidErr)?;
|
hex::decode(sig_header).map_err(|_| AppError::WebHookSigHeaderInvalidErr)?;
|
||||||
let mut mac = Hmac::<Sha256>::new_from_slice(secret_key).map_err(anyhow::Error::from)?;
|
let key = hmac::Key::new(hmac::HMAC_SHA256, secret_key);
|
||||||
|
|
||||||
mac.update(body);
|
hmac::verify(&key, body, &sig_header_decoded).map_err(|_| AppError::WebHookSigHeaderInvalidErr)
|
||||||
|
|
||||||
let generated_hmac = mac.finalize().into_bytes();
|
|
||||||
bool::from(generated_hmac.ct_eq(&sig_header_decoded))
|
|
||||||
.then_some(())
|
|
||||||
.ok_or(AppError::WebHookSigHeaderInvalidErr)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
env::EnvConfig,
|
gitea::{GiteaAPI, WebhookType}, metrics, open_router::OpenRouterClient,
|
||||||
gitea::{GiteaAPI, WebhookType},
|
|
||||||
open_router::OpenRouterClient,
|
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::{collections::HashSet, sync::Arc, time::Duration};
|
use std::{collections::HashSet, sync::Arc};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use tracing::{error, info, instrument};
|
use tracing::{error, info, instrument};
|
||||||
@@ -26,33 +24,37 @@ pub struct ReviewItem {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Bot {
|
pub struct Bot {
|
||||||
config: EnvConfig,
|
bot_name: String,
|
||||||
gitea_api: GiteaAPI,
|
gitea_api: GiteaAPI,
|
||||||
open_router_client: OpenRouterClient,
|
open_router_client: OpenRouterClient,
|
||||||
http_client: reqwest::Client,
|
http_client: reqwest::Client,
|
||||||
max_concurrent: usize,
|
max_concurrent: usize,
|
||||||
|
open_router_model: String,
|
||||||
actions_handled: Arc<Mutex<HashSet<u64>>>,
|
actions_handled: Arc<Mutex<HashSet<u64>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Bot {
|
impl Bot {
|
||||||
pub fn new(config: EnvConfig) -> anyhow::Result<Self> {
|
pub fn new(
|
||||||
let gitea_timeout = config.gitea_timeout;
|
bot_name: String,
|
||||||
let open_router_timeout = config.open_router_timeout;
|
gitea_api: GiteaAPI,
|
||||||
|
open_router_client: OpenRouterClient,
|
||||||
Ok(Self {
|
http_client: reqwest::Client,
|
||||||
gitea_api: GiteaAPI::new(&config.gitea_url, &config.gitea_token, gitea_timeout)?,
|
max_concurrent: usize,
|
||||||
open_router_client: OpenRouterClient::new(
|
open_router_model: String,
|
||||||
&config.open_router_api_key,
|
) -> Self {
|
||||||
&config.open_router_model,
|
Self {
|
||||||
open_router_timeout,
|
bot_name,
|
||||||
)?,
|
gitea_api,
|
||||||
max_concurrent: config.bot_max_concurrent,
|
open_router_client,
|
||||||
config,
|
http_client,
|
||||||
|
max_concurrent,
|
||||||
|
open_router_model,
|
||||||
actions_handled: Arc::new(Mutex::new(HashSet::new())),
|
actions_handled: Arc::new(Mutex::new(HashSet::new())),
|
||||||
http_client: reqwest::Client::builder()
|
}
|
||||||
.timeout(Duration::from_secs(gitea_timeout))
|
}
|
||||||
.build()?,
|
|
||||||
})
|
pub fn name(&self) -> String {
|
||||||
|
qpismont marked this conversation as resolved
Herald
commented
La méthode La méthode `name()` retourne une `String` en clonant `self.bot_name`. Cela peut être inefficace si appelée fréquemment. Envisagez de retourner un `&str` pour éviter une allocation, ou changez le champ en `Arc<str>` si un partage est nécessaire.
|
|||||||
|
self.bot_name.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start(
|
pub async fn start(
|
||||||
@@ -84,10 +86,12 @@ impl Bot {
|
|||||||
info!(queued = rx.len(), active = tasks.len(), "Webhook received");
|
info!(queued = rx.len(), active = tasks.len(), "Webhook received");
|
||||||
let permit = sem.clone().acquire_owned().await?;
|
let permit = sem.clone().acquire_owned().await?;
|
||||||
let self_clone = self.clone();
|
let self_clone = self.clone();
|
||||||
|
metrics::increment_task_active();
|
||||||
|
|
||||||
tasks.spawn(async move {
|
tasks.spawn(async move {
|
||||||
self_clone.exec(wb).await;
|
self_clone.exec(wb).await;
|
||||||
drop(permit);
|
drop(permit);
|
||||||
|
metrics::decrement_task_active();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +103,8 @@ impl Bot {
|
|||||||
|
|
||||||
#[instrument(skip(self, webhook), fields(repo, pr))]
|
#[instrument(skip(self, webhook), fields(repo, pr))]
|
||||||
pub async fn exec(&self, webhook: WebhookType) {
|
pub async fn exec(&self, webhook: WebhookType) {
|
||||||
|
let event_type_str = webhook.event_type_str();
|
||||||
|
|
||||||
match &webhook {
|
match &webhook {
|
||||||
WebhookType::Review(p) => {
|
WebhookType::Review(p) => {
|
||||||
tracing::Span::current().record("repo", &p.repository.full_name);
|
tracing::Span::current().record("repo", &p.repository.full_name);
|
||||||
@@ -111,15 +117,19 @@ impl Bot {
|
|||||||
&self.gitea_api,
|
&self.gitea_api,
|
||||||
&self.open_router_client,
|
&self.open_router_client,
|
||||||
&self.http_client,
|
&self.http_client,
|
||||||
&self.config.open_router_model,
|
&self.open_router_model,
|
||||||
review_payload,
|
review_payload,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
match exec_result {
|
match exec_result {
|
||||||
Ok(_) => info!("Task completed"),
|
Ok(_) => {
|
||||||
|
metrics::task_completed(event_type_str);
|
||||||
|
info!("Task completed");
|
||||||
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
metrics::task_failed(event_type_str);
|
||||||
error!(%err, "Task error");
|
error!(%err, "Task error");
|
||||||
sentry_anyhow::capture_anyhow(&err);
|
sentry_anyhow::capture_anyhow(&err);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ use tokio_util::io::StreamReader;
|
|||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
bot::ReviewResult,
|
bot::ReviewResult, consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT}, gitea::{GiteaAPI, ReviewPayload}, metrics, open_router::OpenRouterClient,
|
||||||
consts::{BOT_PROCESS_MSG, MAX_DIFF_SIZE, REVIEW_PROMPT},
|
|
||||||
gitea::{GiteaAPI, ReviewPayload},
|
|
||||||
open_router::OpenRouterClient,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[instrument(skip(gitea_api, open_router_client, http_client, review_payload))]
|
#[instrument(skip(gitea_api, open_router_client, http_client, review_payload))]
|
||||||
@@ -48,6 +45,9 @@ pub async fn exec_review(
|
|||||||
let mut review_result = serde_json::from_str::<ReviewResult>(&chat_result.message)?;
|
let mut review_result = serde_json::from_str::<ReviewResult>(&chat_result.message)?;
|
||||||
|
|
||||||
review_result.cost = chat_result.cost;
|
review_result.cost = chat_result.cost;
|
||||||
|
if let Some(cost) = review_result.cost {
|
||||||
|
metrics::openrouter_cost_usd(cost);
|
||||||
|
}
|
||||||
|
|
||||||
let final_review_markdown = review_result_to_markdown(&review_result);
|
let final_review_markdown = review_result_to_markdown(&review_result);
|
||||||
|
|
||||||
|
|||||||
@@ -7,16 +7,15 @@ pub struct EnvConfig {
|
|||||||
pub open_router_api_key: String,
|
pub open_router_api_key: String,
|
||||||
pub open_router_model: String,
|
pub open_router_model: String,
|
||||||
pub open_router_timeout: u64,
|
pub open_router_timeout: u64,
|
||||||
pub bot_name: String,
|
|
||||||
pub bot_max_concurrent: usize,
|
pub bot_max_concurrent: usize,
|
||||||
pub gitea_url: String,
|
pub gitea_url: String,
|
||||||
pub gitea_token: String,
|
pub gitea_token: String,
|
||||||
pub gitea_timeout: u64,
|
pub gitea_timeout: u64,
|
||||||
|
pub metrics_bind_addr: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_config() -> anyhow::Result<EnvConfig> {
|
pub fn load_config() -> anyhow::Result<EnvConfig> {
|
||||||
let http_port = try_get_env("HTTP_PORT")?.parse()?;
|
let http_port = try_get_env("HTTP_PORT")?.parse()?;
|
||||||
let bot_name = try_get_env("BOT_NAME")?;
|
|
||||||
let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?;
|
let webhook_secret = try_get_env("WEBHOOK_SIG_HEADER_SECRET")?;
|
||||||
let open_router_api_key = try_get_env("OPEN_ROUTER_API_KEY")?;
|
let open_router_api_key = try_get_env("OPEN_ROUTER_API_KEY")?;
|
||||||
let open_router_model = try_get_env("OPEN_ROUTER_MODEL")?;
|
let open_router_model = try_get_env("OPEN_ROUTER_MODEL")?;
|
||||||
@@ -25,11 +24,11 @@ pub fn load_config() -> anyhow::Result<EnvConfig> {
|
|||||||
let gitea_url = try_get_env("GITEA_URL")?;
|
let gitea_url = try_get_env("GITEA_URL")?;
|
||||||
let gitea_token = try_get_env("GITEA_TOKEN")?;
|
let gitea_token = try_get_env("GITEA_TOKEN")?;
|
||||||
let gitea_timeout = try_get_env("GITEA_TIMEOUT")?.parse()?;
|
let gitea_timeout = try_get_env("GITEA_TIMEOUT")?.parse()?;
|
||||||
|
let metrics_bind_addr = std::env::var("METRICS_BIND_ADDR").ok();
|
||||||
|
|
||||||
Ok(EnvConfig {
|
Ok(EnvConfig {
|
||||||
http_port,
|
http_port,
|
||||||
webhook_secret,
|
webhook_secret,
|
||||||
bot_name,
|
|
||||||
open_router_api_key,
|
open_router_api_key,
|
||||||
open_router_model,
|
open_router_model,
|
||||||
open_router_timeout,
|
open_router_timeout,
|
||||||
@@ -37,6 +36,7 @@ pub fn load_config() -> anyhow::Result<EnvConfig> {
|
|||||||
gitea_url,
|
gitea_url,
|
||||||
gitea_token,
|
gitea_token,
|
||||||
gitea_timeout,
|
gitea_timeout,
|
||||||
|
metrics_bind_addr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,21 @@ impl GiteaAPI {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[instrument(skip(self))]
|
||||||
|
pub async fn get_authorized_user(&self) -> anyhow::Result<User> {
|
||||||
|
let url = format!("{}/api/v1/user", self.base_url);
|
||||||
|
let res = self.client.get(url).send().await?;
|
||||||
|
|
||||||
|
if !res.status().is_success() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Failed to get authorized user: {}",
|
||||||
|
res.status()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json::<User>().await.map_err(anyhow::Error::from)
|
||||||
|
}
|
||||||
|
|
||||||
#[instrument(skip(self))]
|
#[instrument(skip(self))]
|
||||||
pub async fn comment(
|
pub async fn comment(
|
||||||
&self,
|
&self,
|
||||||
@@ -164,6 +179,12 @@ impl WebhookType {
|
|||||||
WebhookType::Review(payload) => payload.comment.id,
|
WebhookType::Review(payload) => payload.comment.id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn event_type_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
WebhookType::Review(_) => "review",
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -192,6 +213,7 @@ pub struct Comment {
|
|||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
pub struct User {
|
pub struct User {
|
||||||
pub id: u64,
|
pub id: u64,
|
||||||
|
pub login: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -248,7 +270,8 @@ mod tests {
|
|||||||
"id": 7,
|
"id": 7,
|
||||||
"body": "@test_bot LGTM",
|
"body": "@test_bot LGTM",
|
||||||
"user": {
|
"user": {
|
||||||
"id": 100
|
"id": 100,
|
||||||
|
"login": "test_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -312,7 +335,8 @@ mod tests {
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"body": "@test_bot body",
|
"body": "@test_bot body",
|
||||||
"user": {
|
"user": {
|
||||||
"id": 1
|
"id": 1,
|
||||||
|
"login": "test_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -343,7 +367,8 @@ mod tests {
|
|||||||
"id": 12,
|
"id": 12,
|
||||||
"body": "Needs work",
|
"body": "Needs work",
|
||||||
"user": {
|
"user": {
|
||||||
"id": 200
|
"id": 200,
|
||||||
|
"login": "test_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -380,7 +405,8 @@ mod tests {
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"body": "@other_bot do something",
|
"body": "@other_bot do something",
|
||||||
"user": {
|
"user": {
|
||||||
"id": 1
|
"id": 1,
|
||||||
|
"login": "test_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -406,7 +432,8 @@ mod tests {
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"body": "just a comment without bot mention",
|
"body": "just a comment without bot mention",
|
||||||
"user": {
|
"user": {
|
||||||
"id": 1
|
"id": 1,
|
||||||
|
"login": "test_user"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
use std::sync::Arc;
|
use crate::{bot::Bot, gitea::{GiteaAPI, WebhookType}, open_router::OpenRouterClient, state::AppState};
|
||||||
|
|
||||||
use crate::{bot::Bot, gitea::WebhookType, state::AppState};
|
|
||||||
|
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use tokio::signal::unix::{SignalKind, signal};
|
use tokio::signal::unix::{SignalKind, signal};
|
||||||
@@ -15,6 +13,7 @@ mod consts;
|
|||||||
mod env;
|
mod env;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod gitea;
|
mod gitea;
|
||||||
|
mod metrics;
|
||||||
mod open_router;
|
mod open_router;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
@@ -51,17 +50,38 @@ fn main() -> anyhow::Result<()> {
|
|||||||
async fn run() -> anyhow::Result<()> {
|
async fn run() -> anyhow::Result<()> {
|
||||||
let config = env::load_config()?;
|
let config = env::load_config()?;
|
||||||
|
|
||||||
|
if let Some(metric_bind_addr) = &config.metrics_bind_addr {
|
||||||
|
qpismont marked this conversation as resolved
Herald
commented
Le démarrage conditionnel de l'exportateur Prometheus est une bonne pratique. Toutefois, si l'installation échoue, l'erreur est propagée via Le démarrage conditionnel de l'exportateur Prometheus est une bonne pratique. Toutefois, si l'installation échoue, l'erreur est propagée via `?`, ce qui met fin à l'application. Cela peut être acceptable, mais un message d'erreur plus explicite (ex: avec `tracing::error`) aiderait au débogage.
|
|||||||
|
metrics::install(metric_bind_addr)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let gitea_api = GiteaAPI::new(&config.gitea_url, &config.gitea_token, config.gitea_timeout)?;
|
||||||
|
let gitea_user = gitea_api.get_authorized_user().await?;
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
port = config.http_port,
|
port = config.http_port,
|
||||||
model = %config.open_router_model,
|
model = %config.open_router_model,
|
||||||
gitea_url = %config.gitea_url,
|
gitea_url = %config.gitea_url,
|
||||||
bot_name = %config.bot_name,
|
bot_name = %gitea_user.login,
|
||||||
"Starting Herald"
|
"Starting Herald"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let open_router_client = OpenRouterClient::new(
|
||||||
|
&config.open_router_api_key,
|
||||||
|
&config.open_router_model,
|
||||||
|
config.open_router_timeout,
|
||||||
|
)?;
|
||||||
|
|
||||||
let shutdown = CancellationToken::new();
|
let shutdown = CancellationToken::new();
|
||||||
|
|
||||||
let bot = Bot::new(config.clone())?;
|
let bot = Bot::new(
|
||||||
|
gitea_user.login,
|
||||||
|
gitea_api,
|
||||||
|
open_router_client,
|
||||||
|
reqwest::Client::new(),
|
||||||
|
config.bot_max_concurrent,
|
||||||
|
config.open_router_model.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel::<WebhookType>(config.bot_max_concurrent * 2);
|
let (tx, rx) = tokio::sync::mpsc::channel::<WebhookType>(config.bot_max_concurrent * 2);
|
||||||
let app_state = AppState {
|
let app_state = AppState {
|
||||||
bot_tx: tx,
|
bot_tx: tx,
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
use std::{net::SocketAddr, str::FromStr};
|
||||||
|
|
||||||
|
use metrics::{Unit, describe_counter, describe_gauge, counter, gauge};
|
||||||
|
|
||||||
|
pub fn webhook_received(event_type: &str) {
|
||||||
|
counter!("herald_webhooks_received_total", "event_type" => event_type.to_string())
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webhook_duplicate(event_type: &str) {
|
||||||
|
counter!("herald_webhooks_duplicate_total", "event_type" => event_type.to_string())
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn webhook_channel_full(event_type: &str) {
|
||||||
|
counter!("herald_webhooks_channel_full_total", "event_type" => event_type.to_string())
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_task_active() {
|
||||||
|
gauge!("herald_bot_tasks_active").increment(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decrement_task_active() {
|
||||||
|
gauge!("herald_bot_tasks_active").decrement(1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn task_completed(event_type: &str) {
|
||||||
|
counter!("herald_bot_tasks_completed_total", "event_type" => event_type.to_string())
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn task_failed(event_type: &str) {
|
||||||
|
counter!("herald_bot_tasks_failed_total", "event_type" => event_type.to_string())
|
||||||
|
.increment(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn openrouter_cost_usd(cost: f64) {
|
||||||
|
qpismont marked this conversation as resolved
Outdated
Herald
commented
La métrique La métrique `herald_openrouter_cost_usd_total` utilise un compteur de type `Count`, mais elle représente un coût en dollars. Bien que cela soit fonctionnel, il serait plus clair d'utiliser un type `Gauge` ou d'ajouter une unité personnalisée si possible. Actuellement l'unité `Count` peut prêter à confusion pour les opérateurs.
|
|||||||
|
counter!("herald_openrouter_cost_cents_total").increment((cost * 100.0).round() as u64);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn describe() {
|
||||||
|
describe_counter!("herald_webhooks_received_total", Unit::Count, "Total webhooks received");
|
||||||
|
describe_counter!("herald_webhooks_duplicate_total", Unit::Count, "Webhooks rejected as duplicates");
|
||||||
|
describe_counter!("herald_webhooks_channel_full_total", Unit::Count, "Webhooks dropped because the bot channel was full");
|
||||||
|
describe_gauge!("herald_bot_tasks_active", Unit::Count, "Bot tasks currently in progress");
|
||||||
|
describe_counter!("herald_bot_tasks_completed_total", Unit::Count, "Bot tasks completed successfully");
|
||||||
|
describe_counter!("herald_bot_tasks_failed_total", Unit::Count, "Bot tasks that failed");
|
||||||
|
describe_counter!("herald_openrouter_cost_cents_total", Unit::Count, "Total OpenRouter cost in cents (divide by 100 for USD)");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn install(bind_addr: &str) -> anyhow::Result<()> {
|
||||||
|
describe();
|
||||||
|
|
||||||
|
let builder = metrics_exporter_prometheus::PrometheusBuilder::new();
|
||||||
|
builder
|
||||||
|
.with_http_listener(SocketAddr::from_str(bind_addr)?)
|
||||||
|
.install()?;
|
||||||
|
|
||||||
|
tracing::info!(bind_addr, "Prometheus metrics exporter installed");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
La variable d'environnement
METRICS_BIND_ADDRest définie sans valeur par défaut ni commentaire. Il serait judicieux d'ajouter un exemple de format (ex:0.0.0.0:9090) ou un commentaire indiquant le format attendu, afin d'éviter des erreurs de configuration.