An easy-to-host PDS on the ATProtocol, iPhone and MacOS. Maintain control of your keys and data, always.
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(relay): implement OpenTelemetry baseline tracing (MM-137)

- Add `[telemetry]` config section (enabled, otlp_endpoint, service_name)
with env overrides: EZPDS_TELEMETRY_ENABLED, EZPDS_OTLP_ENDPOINT,
OTEL_SERVICE_NAME
- New crates/relay/src/telemetry.rs: layered subscriber init with
conditional OTel layer; OtelGuard flushes spans on graceful shutdown
- OtelMakeSpan in app.rs: custom TraceLayer make_span_with that extracts
W3C traceparent/tracestate headers and links upstream traces via
TraceContextPropagator
- DB instrumentation: #[tracing::instrument] on open_pool and
run_migrations with db.system = "sqlite" span attribute
- 7 new config tests covering telemetry TOML parsing, all three env
overrides, env-wins-over-toml precedence, and invalid bool error
- Zero overhead when telemetry.enabled = false (default)

authored by

Malpercio and committed by
Tangled
b5b51a8a 464a8398

+916 -30
+606 -19
Cargo.lock
··· 74 74 checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" 75 75 76 76 [[package]] 77 + name = "async-stream" 78 + version = "0.3.6" 79 + source = "registry+https://github.com/rust-lang/crates.io-index" 80 + checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" 81 + dependencies = [ 82 + "async-stream-impl", 83 + "futures-core", 84 + "pin-project-lite", 85 + ] 86 + 87 + [[package]] 88 + name = "async-stream-impl" 89 + version = "0.3.6" 90 + source = "registry+https://github.com/rust-lang/crates.io-index" 91 + checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" 92 + dependencies = [ 93 + "proc-macro2", 94 + "quote", 95 + "syn", 96 + ] 97 + 98 + [[package]] 77 99 name = "async-trait" 78 100 version = "0.1.89" 79 101 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 133 155 "serde_urlencoded", 134 156 "sync_wrapper", 135 157 "tokio", 136 - "tower", 158 + "tower 0.5.3", 137 159 "tower-layer", 138 160 "tower-service", 139 161 "tracing", ··· 189 211 dependencies = [ 190 212 "generic-array", 191 213 ] 214 + 215 + [[package]] 216 + name = "bumpalo" 217 + version = "3.20.2" 218 + source = "registry+https://github.com/rust-lang/crates.io-index" 219 + checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" 192 220 193 221 [[package]] 194 222 name = "byteorder" ··· 457 485 ] 458 486 459 487 [[package]] 488 + name = "fnv" 489 + version = "1.0.7" 490 + source = "registry+https://github.com/rust-lang/crates.io-index" 491 + checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 492 + 493 + [[package]] 460 494 name = "foldhash" 461 495 version = "0.1.5" 462 496 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 516 550 checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" 517 551 518 552 [[package]] 553 + name = "futures-macro" 554 + version = "0.3.32" 555 + source = "registry+https://github.com/rust-lang/crates.io-index" 556 + checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" 557 + dependencies = [ 558 + "proc-macro2", 559 + "quote", 560 + "syn", 561 + ] 562 + 563 + [[package]] 519 564 name = "futures-sink" 520 565 version = "0.3.32" 521 566 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 535 580 dependencies = [ 536 581 "futures-core", 537 582 "futures-io", 583 + "futures-macro", 538 584 "futures-sink", 539 585 "futures-task", 540 586 "memchr", ··· 575 621 "wasip2", 576 622 "wasip3", 577 623 ] 624 + 625 + [[package]] 626 + name = "glob" 627 + version = "0.3.3" 628 + source = "registry+https://github.com/rust-lang/crates.io-index" 629 + checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" 630 + 631 + [[package]] 632 + name = "h2" 633 + version = "0.4.13" 634 + source = "registry+https://github.com/rust-lang/crates.io-index" 635 + checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" 636 + dependencies = [ 637 + "atomic-waker", 638 + "bytes", 639 + "fnv", 640 + "futures-core", 641 + "futures-sink", 642 + "http", 643 + "indexmap 2.13.0", 644 + "slab", 645 + "tokio", 646 + "tokio-util", 647 + "tracing", 648 + ] 649 + 650 + [[package]] 651 + name = "hashbrown" 652 + version = "0.12.3" 653 + source = "registry+https://github.com/rust-lang/crates.io-index" 654 + checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 578 655 579 656 [[package]] 580 657 name = "hashbrown" ··· 696 773 "bytes", 697 774 "futures-channel", 698 775 "futures-core", 776 + "h2", 699 777 "http", 700 778 "http-body", 701 779 "httparse", ··· 705 783 "pin-utils", 706 784 "smallvec", 707 785 "tokio", 786 + "want", 787 + ] 788 + 789 + [[package]] 790 + name = "hyper-timeout" 791 + version = "0.5.2" 792 + source = "registry+https://github.com/rust-lang/crates.io-index" 793 + checksum = "2b90d566bffbce6a75bd8b09a05aa8c2cb1fabb6cb348f8840c9e4c90a0d83b0" 794 + dependencies = [ 795 + "hyper", 796 + "hyper-util", 797 + "pin-project-lite", 798 + "tokio", 799 + "tower-service", 708 800 ] 709 801 710 802 [[package]] ··· 713 805 source = "registry+https://github.com/rust-lang/crates.io-index" 714 806 checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" 715 807 dependencies = [ 808 + "base64", 716 809 "bytes", 810 + "futures-channel", 811 + "futures-util", 717 812 "http", 718 813 "http-body", 719 814 "hyper", 815 + "ipnet", 816 + "libc", 817 + "percent-encoding", 720 818 "pin-project-lite", 819 + "socket2 0.6.3", 721 820 "tokio", 722 821 "tower-service", 822 + "tracing", 723 823 ] 724 824 725 825 [[package]] ··· 832 932 833 933 [[package]] 834 934 name = "indexmap" 935 + version = "1.9.3" 936 + source = "registry+https://github.com/rust-lang/crates.io-index" 937 + checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 938 + dependencies = [ 939 + "autocfg", 940 + "hashbrown 0.12.3", 941 + ] 942 + 943 + [[package]] 944 + name = "indexmap" 835 945 version = "2.13.0" 836 946 source = "registry+https://github.com/rust-lang/crates.io-index" 837 947 checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" ··· 843 953 ] 844 954 845 955 [[package]] 956 + name = "ipnet" 957 + version = "2.12.0" 958 + source = "registry+https://github.com/rust-lang/crates.io-index" 959 + checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" 960 + 961 + [[package]] 962 + name = "iri-string" 963 + version = "0.7.10" 964 + source = "registry+https://github.com/rust-lang/crates.io-index" 965 + checksum = "c91338f0783edbd6195decb37bae672fd3b165faffb89bf7b9e6942f8b1a731a" 966 + dependencies = [ 967 + "memchr", 968 + "serde", 969 + ] 970 + 971 + [[package]] 846 972 name = "is_terminal_polyfill" 847 973 version = "1.70.2" 848 974 source = "registry+https://github.com/rust-lang/crates.io-index" 849 975 checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" 850 976 851 977 [[package]] 978 + name = "itertools" 979 + version = "0.14.0" 980 + source = "registry+https://github.com/rust-lang/crates.io-index" 981 + checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" 982 + dependencies = [ 983 + "either", 984 + ] 985 + 986 + [[package]] 852 987 name = "itoa" 853 988 version = "1.0.17" 854 989 source = "registry+https://github.com/rust-lang/crates.io-index" 855 990 checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" 991 + 992 + [[package]] 993 + name = "js-sys" 994 + version = "0.3.91" 995 + source = "registry+https://github.com/rust-lang/crates.io-index" 996 + checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" 997 + dependencies = [ 998 + "once_cell", 999 + "wasm-bindgen", 1000 + ] 856 1001 857 1002 [[package]] 858 1003 name = "lazy_static" ··· 1047 1192 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 1048 1193 1049 1194 [[package]] 1195 + name = "opentelemetry" 1196 + version = "0.28.0" 1197 + source = "registry+https://github.com/rust-lang/crates.io-index" 1198 + checksum = "236e667b670a5cdf90c258f5a55794ec5ac5027e960c224bff8367a59e1e6426" 1199 + dependencies = [ 1200 + "futures-core", 1201 + "futures-sink", 1202 + "js-sys", 1203 + "pin-project-lite", 1204 + "thiserror", 1205 + "tracing", 1206 + ] 1207 + 1208 + [[package]] 1209 + name = "opentelemetry-http" 1210 + version = "0.28.0" 1211 + source = "registry+https://github.com/rust-lang/crates.io-index" 1212 + checksum = "a8863faf2910030d139fb48715ad5ff2f35029fc5f244f6d5f689ddcf4d26253" 1213 + dependencies = [ 1214 + "async-trait", 1215 + "bytes", 1216 + "http", 1217 + "opentelemetry", 1218 + "reqwest", 1219 + "tracing", 1220 + ] 1221 + 1222 + [[package]] 1223 + name = "opentelemetry-otlp" 1224 + version = "0.28.0" 1225 + source = "registry+https://github.com/rust-lang/crates.io-index" 1226 + checksum = "5bef114c6d41bea83d6dc60eb41720eedd0261a67af57b66dd2b84ac46c01d91" 1227 + dependencies = [ 1228 + "async-trait", 1229 + "futures-core", 1230 + "http", 1231 + "opentelemetry", 1232 + "opentelemetry-http", 1233 + "opentelemetry-proto", 1234 + "opentelemetry_sdk", 1235 + "prost", 1236 + "reqwest", 1237 + "thiserror", 1238 + "tokio", 1239 + "tonic", 1240 + "tracing", 1241 + ] 1242 + 1243 + [[package]] 1244 + name = "opentelemetry-proto" 1245 + version = "0.28.0" 1246 + source = "registry+https://github.com/rust-lang/crates.io-index" 1247 + checksum = "56f8870d3024727e99212eb3bb1762ec16e255e3e6f58eeb3dc8db1aa226746d" 1248 + dependencies = [ 1249 + "opentelemetry", 1250 + "opentelemetry_sdk", 1251 + "prost", 1252 + "tonic", 1253 + ] 1254 + 1255 + [[package]] 1256 + name = "opentelemetry_sdk" 1257 + version = "0.28.0" 1258 + source = "registry+https://github.com/rust-lang/crates.io-index" 1259 + checksum = "84dfad6042089c7fc1f6118b7040dc2eb4ab520abbf410b79dc481032af39570" 1260 + dependencies = [ 1261 + "async-trait", 1262 + "futures-channel", 1263 + "futures-executor", 1264 + "futures-util", 1265 + "glob", 1266 + "opentelemetry", 1267 + "percent-encoding", 1268 + "rand", 1269 + "serde_json", 1270 + "thiserror", 1271 + "tokio", 1272 + "tokio-stream", 1273 + "tracing", 1274 + ] 1275 + 1276 + [[package]] 1050 1277 name = "parking" 1051 1278 version = "2.2.1" 1052 1279 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1091 1318 checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" 1092 1319 1093 1320 [[package]] 1321 + name = "pin-project" 1322 + version = "1.1.11" 1323 + source = "registry+https://github.com/rust-lang/crates.io-index" 1324 + checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" 1325 + dependencies = [ 1326 + "pin-project-internal", 1327 + ] 1328 + 1329 + [[package]] 1330 + name = "pin-project-internal" 1331 + version = "1.1.11" 1332 + source = "registry+https://github.com/rust-lang/crates.io-index" 1333 + checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" 1334 + dependencies = [ 1335 + "proc-macro2", 1336 + "quote", 1337 + "syn", 1338 + ] 1339 + 1340 + [[package]] 1094 1341 name = "pin-project-lite" 1095 1342 version = "0.2.17" 1096 1343 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1173 1420 ] 1174 1421 1175 1422 [[package]] 1423 + name = "prost" 1424 + version = "0.13.5" 1425 + source = "registry+https://github.com/rust-lang/crates.io-index" 1426 + checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" 1427 + dependencies = [ 1428 + "bytes", 1429 + "prost-derive", 1430 + ] 1431 + 1432 + [[package]] 1433 + name = "prost-derive" 1434 + version = "0.13.5" 1435 + source = "registry+https://github.com/rust-lang/crates.io-index" 1436 + checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" 1437 + dependencies = [ 1438 + "anyhow", 1439 + "itertools", 1440 + "proc-macro2", 1441 + "quote", 1442 + "syn", 1443 + ] 1444 + 1445 + [[package]] 1176 1446 name = "quote" 1177 1447 version = "1.0.45" 1178 1448 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1260 1530 "axum", 1261 1531 "clap", 1262 1532 "common", 1533 + "opentelemetry", 1534 + "opentelemetry-otlp", 1535 + "opentelemetry_sdk", 1263 1536 "serde", 1264 1537 "serde_json", 1265 1538 "sqlx", 1266 1539 "tempfile", 1267 1540 "thiserror", 1268 1541 "tokio", 1269 - "tower", 1270 - "tower-http", 1542 + "tower 0.5.3", 1543 + "tower-http 0.5.2", 1271 1544 "tracing", 1545 + "tracing-opentelemetry", 1272 1546 "tracing-subscriber", 1273 1547 ] 1274 1548 ··· 1277 1551 version = "0.1.0" 1278 1552 1279 1553 [[package]] 1554 + name = "reqwest" 1555 + version = "0.12.28" 1556 + source = "registry+https://github.com/rust-lang/crates.io-index" 1557 + checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" 1558 + dependencies = [ 1559 + "base64", 1560 + "bytes", 1561 + "futures-channel", 1562 + "futures-core", 1563 + "futures-util", 1564 + "http", 1565 + "http-body", 1566 + "http-body-util", 1567 + "hyper", 1568 + "hyper-util", 1569 + "js-sys", 1570 + "log", 1571 + "percent-encoding", 1572 + "pin-project-lite", 1573 + "serde", 1574 + "serde_json", 1575 + "serde_urlencoded", 1576 + "sync_wrapper", 1577 + "tokio", 1578 + "tower 0.5.3", 1579 + "tower-http 0.6.8", 1580 + "tower-service", 1581 + "url", 1582 + "wasm-bindgen", 1583 + "wasm-bindgen-futures", 1584 + "web-sys", 1585 + ] 1586 + 1587 + [[package]] 1280 1588 name = "rsa" 1281 1589 version = "0.9.10" 1282 1590 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1482 1790 1483 1791 [[package]] 1484 1792 name = "socket2" 1793 + version = "0.5.10" 1794 + source = "registry+https://github.com/rust-lang/crates.io-index" 1795 + checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" 1796 + dependencies = [ 1797 + "libc", 1798 + "windows-sys 0.52.0", 1799 + ] 1800 + 1801 + [[package]] 1802 + name = "socket2" 1485 1803 version = "0.6.3" 1486 1804 source = "registry+https://github.com/rust-lang/crates.io-index" 1487 1805 checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" ··· 1540 1858 "futures-util", 1541 1859 "hashbrown 0.15.5", 1542 1860 "hashlink", 1543 - "indexmap", 1861 + "indexmap 2.13.0", 1544 1862 "log", 1545 1863 "memchr", 1546 1864 "once_cell", ··· 1742 2060 version = "1.0.2" 1743 2061 source = "registry+https://github.com/rust-lang/crates.io-index" 1744 2062 checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" 2063 + dependencies = [ 2064 + "futures-core", 2065 + ] 1745 2066 1746 2067 [[package]] 1747 2068 name = "synstructure" ··· 1833 2154 "parking_lot", 1834 2155 "pin-project-lite", 1835 2156 "signal-hook-registry", 1836 - "socket2", 2157 + "socket2 0.6.3", 1837 2158 "tokio-macros", 1838 2159 "windows-sys 0.61.2", 1839 2160 ] ··· 1861 2182 ] 1862 2183 1863 2184 [[package]] 2185 + name = "tokio-util" 2186 + version = "0.7.18" 2187 + source = "registry+https://github.com/rust-lang/crates.io-index" 2188 + checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" 2189 + dependencies = [ 2190 + "bytes", 2191 + "futures-core", 2192 + "futures-sink", 2193 + "pin-project-lite", 2194 + "tokio", 2195 + ] 2196 + 2197 + [[package]] 1864 2198 name = "toml" 1865 2199 version = "0.8.23" 1866 2200 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1887 2221 source = "registry+https://github.com/rust-lang/crates.io-index" 1888 2222 checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" 1889 2223 dependencies = [ 1890 - "indexmap", 2224 + "indexmap 2.13.0", 1891 2225 "serde", 1892 2226 "serde_spanned", 1893 2227 "toml_datetime", ··· 1902 2236 checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" 1903 2237 1904 2238 [[package]] 2239 + name = "tonic" 2240 + version = "0.12.3" 2241 + source = "registry+https://github.com/rust-lang/crates.io-index" 2242 + checksum = "877c5b330756d856ffcc4553ab34a5684481ade925ecc54bcd1bf02b1d0d4d52" 2243 + dependencies = [ 2244 + "async-stream", 2245 + "async-trait", 2246 + "axum", 2247 + "base64", 2248 + "bytes", 2249 + "h2", 2250 + "http", 2251 + "http-body", 2252 + "http-body-util", 2253 + "hyper", 2254 + "hyper-timeout", 2255 + "hyper-util", 2256 + "percent-encoding", 2257 + "pin-project", 2258 + "prost", 2259 + "socket2 0.5.10", 2260 + "tokio", 2261 + "tokio-stream", 2262 + "tower 0.4.13", 2263 + "tower-layer", 2264 + "tower-service", 2265 + "tracing", 2266 + ] 2267 + 2268 + [[package]] 2269 + name = "tower" 2270 + version = "0.4.13" 2271 + source = "registry+https://github.com/rust-lang/crates.io-index" 2272 + checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" 2273 + dependencies = [ 2274 + "futures-core", 2275 + "futures-util", 2276 + "indexmap 1.9.3", 2277 + "pin-project", 2278 + "pin-project-lite", 2279 + "rand", 2280 + "slab", 2281 + "tokio", 2282 + "tokio-util", 2283 + "tower-layer", 2284 + "tower-service", 2285 + "tracing", 2286 + ] 2287 + 2288 + [[package]] 1905 2289 name = "tower" 1906 2290 version = "0.5.3" 1907 2291 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1935 2319 ] 1936 2320 1937 2321 [[package]] 2322 + name = "tower-http" 2323 + version = "0.6.8" 2324 + source = "registry+https://github.com/rust-lang/crates.io-index" 2325 + checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" 2326 + dependencies = [ 2327 + "bitflags", 2328 + "bytes", 2329 + "futures-util", 2330 + "http", 2331 + "http-body", 2332 + "iri-string", 2333 + "pin-project-lite", 2334 + "tower 0.5.3", 2335 + "tower-layer", 2336 + "tower-service", 2337 + ] 2338 + 2339 + [[package]] 1938 2340 name = "tower-layer" 1939 2341 version = "0.3.3" 1940 2342 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 1991 2393 ] 1992 2394 1993 2395 [[package]] 2396 + name = "tracing-opentelemetry" 2397 + version = "0.29.0" 2398 + source = "registry+https://github.com/rust-lang/crates.io-index" 2399 + checksum = "721f2d2569dce9f3dfbbddee5906941e953bfcdf736a62da3377f5751650cc36" 2400 + dependencies = [ 2401 + "js-sys", 2402 + "once_cell", 2403 + "opentelemetry", 2404 + "opentelemetry_sdk", 2405 + "smallvec", 2406 + "tracing", 2407 + "tracing-core", 2408 + "tracing-log", 2409 + "tracing-subscriber", 2410 + "web-time", 2411 + ] 2412 + 2413 + [[package]] 1994 2414 name = "tracing-subscriber" 1995 2415 version = "0.3.22" 1996 2416 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2007 2427 "tracing-core", 2008 2428 "tracing-log", 2009 2429 ] 2430 + 2431 + [[package]] 2432 + name = "try-lock" 2433 + version = "0.2.5" 2434 + source = "registry+https://github.com/rust-lang/crates.io-index" 2435 + checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2010 2436 2011 2437 [[package]] 2012 2438 name = "typenum" ··· 2090 2516 checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 2091 2517 2092 2518 [[package]] 2519 + name = "want" 2520 + version = "0.3.1" 2521 + source = "registry+https://github.com/rust-lang/crates.io-index" 2522 + checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 2523 + dependencies = [ 2524 + "try-lock", 2525 + ] 2526 + 2527 + [[package]] 2093 2528 name = "wasi" 2094 2529 version = "0.11.1+wasi-snapshot-preview1" 2095 2530 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2120 2555 checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" 2121 2556 2122 2557 [[package]] 2558 + name = "wasm-bindgen" 2559 + version = "0.2.114" 2560 + source = "registry+https://github.com/rust-lang/crates.io-index" 2561 + checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" 2562 + dependencies = [ 2563 + "cfg-if", 2564 + "once_cell", 2565 + "rustversion", 2566 + "wasm-bindgen-macro", 2567 + "wasm-bindgen-shared", 2568 + ] 2569 + 2570 + [[package]] 2571 + name = "wasm-bindgen-futures" 2572 + version = "0.4.64" 2573 + source = "registry+https://github.com/rust-lang/crates.io-index" 2574 + checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" 2575 + dependencies = [ 2576 + "cfg-if", 2577 + "futures-util", 2578 + "js-sys", 2579 + "once_cell", 2580 + "wasm-bindgen", 2581 + "web-sys", 2582 + ] 2583 + 2584 + [[package]] 2585 + name = "wasm-bindgen-macro" 2586 + version = "0.2.114" 2587 + source = "registry+https://github.com/rust-lang/crates.io-index" 2588 + checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" 2589 + dependencies = [ 2590 + "quote", 2591 + "wasm-bindgen-macro-support", 2592 + ] 2593 + 2594 + [[package]] 2595 + name = "wasm-bindgen-macro-support" 2596 + version = "0.2.114" 2597 + source = "registry+https://github.com/rust-lang/crates.io-index" 2598 + checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" 2599 + dependencies = [ 2600 + "bumpalo", 2601 + "proc-macro2", 2602 + "quote", 2603 + "syn", 2604 + "wasm-bindgen-shared", 2605 + ] 2606 + 2607 + [[package]] 2608 + name = "wasm-bindgen-shared" 2609 + version = "0.2.114" 2610 + source = "registry+https://github.com/rust-lang/crates.io-index" 2611 + checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" 2612 + dependencies = [ 2613 + "unicode-ident", 2614 + ] 2615 + 2616 + [[package]] 2123 2617 name = "wasm-encoder" 2124 2618 version = "0.244.0" 2125 2619 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2136 2630 checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" 2137 2631 dependencies = [ 2138 2632 "anyhow", 2139 - "indexmap", 2633 + "indexmap 2.13.0", 2140 2634 "wasm-encoder", 2141 2635 "wasmparser", 2142 2636 ] ··· 2149 2643 dependencies = [ 2150 2644 "bitflags", 2151 2645 "hashbrown 0.15.5", 2152 - "indexmap", 2646 + "indexmap 2.13.0", 2153 2647 "semver", 2154 2648 ] 2155 2649 2156 2650 [[package]] 2651 + name = "web-sys" 2652 + version = "0.3.91" 2653 + source = "registry+https://github.com/rust-lang/crates.io-index" 2654 + checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" 2655 + dependencies = [ 2656 + "js-sys", 2657 + "wasm-bindgen", 2658 + ] 2659 + 2660 + [[package]] 2661 + name = "web-time" 2662 + version = "1.1.0" 2663 + source = "registry+https://github.com/rust-lang/crates.io-index" 2664 + checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" 2665 + dependencies = [ 2666 + "js-sys", 2667 + "wasm-bindgen", 2668 + ] 2669 + 2670 + [[package]] 2157 2671 name = "whoami" 2158 2672 version = "1.6.1" 2159 2673 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2175 2689 source = "registry+https://github.com/rust-lang/crates.io-index" 2176 2690 checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" 2177 2691 dependencies = [ 2178 - "windows-targets", 2692 + "windows-targets 0.48.5", 2693 + ] 2694 + 2695 + [[package]] 2696 + name = "windows-sys" 2697 + version = "0.52.0" 2698 + source = "registry+https://github.com/rust-lang/crates.io-index" 2699 + checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2700 + dependencies = [ 2701 + "windows-targets 0.52.6", 2179 2702 ] 2180 2703 2181 2704 [[package]] ··· 2193 2716 source = "registry+https://github.com/rust-lang/crates.io-index" 2194 2717 checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2195 2718 dependencies = [ 2196 - "windows_aarch64_gnullvm", 2197 - "windows_aarch64_msvc", 2198 - "windows_i686_gnu", 2199 - "windows_i686_msvc", 2200 - "windows_x86_64_gnu", 2201 - "windows_x86_64_gnullvm", 2202 - "windows_x86_64_msvc", 2719 + "windows_aarch64_gnullvm 0.48.5", 2720 + "windows_aarch64_msvc 0.48.5", 2721 + "windows_i686_gnu 0.48.5", 2722 + "windows_i686_msvc 0.48.5", 2723 + "windows_x86_64_gnu 0.48.5", 2724 + "windows_x86_64_gnullvm 0.48.5", 2725 + "windows_x86_64_msvc 0.48.5", 2726 + ] 2727 + 2728 + [[package]] 2729 + name = "windows-targets" 2730 + version = "0.52.6" 2731 + source = "registry+https://github.com/rust-lang/crates.io-index" 2732 + checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 2733 + dependencies = [ 2734 + "windows_aarch64_gnullvm 0.52.6", 2735 + "windows_aarch64_msvc 0.52.6", 2736 + "windows_i686_gnu 0.52.6", 2737 + "windows_i686_gnullvm", 2738 + "windows_i686_msvc 0.52.6", 2739 + "windows_x86_64_gnu 0.52.6", 2740 + "windows_x86_64_gnullvm 0.52.6", 2741 + "windows_x86_64_msvc 0.52.6", 2203 2742 ] 2204 2743 2205 2744 [[package]] ··· 2207 2746 version = "0.48.5" 2208 2747 source = "registry+https://github.com/rust-lang/crates.io-index" 2209 2748 checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2749 + 2750 + [[package]] 2751 + name = "windows_aarch64_gnullvm" 2752 + version = "0.52.6" 2753 + source = "registry+https://github.com/rust-lang/crates.io-index" 2754 + checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 2210 2755 2211 2756 [[package]] 2212 2757 name = "windows_aarch64_msvc" ··· 2215 2760 checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2216 2761 2217 2762 [[package]] 2763 + name = "windows_aarch64_msvc" 2764 + version = "0.52.6" 2765 + source = "registry+https://github.com/rust-lang/crates.io-index" 2766 + checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 2767 + 2768 + [[package]] 2218 2769 name = "windows_i686_gnu" 2219 2770 version = "0.48.5" 2220 2771 source = "registry+https://github.com/rust-lang/crates.io-index" 2221 2772 checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2222 2773 2223 2774 [[package]] 2775 + name = "windows_i686_gnu" 2776 + version = "0.52.6" 2777 + source = "registry+https://github.com/rust-lang/crates.io-index" 2778 + checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 2779 + 2780 + [[package]] 2781 + name = "windows_i686_gnullvm" 2782 + version = "0.52.6" 2783 + source = "registry+https://github.com/rust-lang/crates.io-index" 2784 + checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 2785 + 2786 + [[package]] 2224 2787 name = "windows_i686_msvc" 2225 2788 version = "0.48.5" 2226 2789 source = "registry+https://github.com/rust-lang/crates.io-index" 2227 2790 checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2228 2791 2229 2792 [[package]] 2793 + name = "windows_i686_msvc" 2794 + version = "0.52.6" 2795 + source = "registry+https://github.com/rust-lang/crates.io-index" 2796 + checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 2797 + 2798 + [[package]] 2230 2799 name = "windows_x86_64_gnu" 2231 2800 version = "0.48.5" 2232 2801 source = "registry+https://github.com/rust-lang/crates.io-index" 2233 2802 checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2234 2803 2235 2804 [[package]] 2805 + name = "windows_x86_64_gnu" 2806 + version = "0.52.6" 2807 + source = "registry+https://github.com/rust-lang/crates.io-index" 2808 + checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 2809 + 2810 + [[package]] 2236 2811 name = "windows_x86_64_gnullvm" 2237 2812 version = "0.48.5" 2238 2813 source = "registry+https://github.com/rust-lang/crates.io-index" 2239 2814 checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2240 2815 2241 2816 [[package]] 2817 + name = "windows_x86_64_gnullvm" 2818 + version = "0.52.6" 2819 + source = "registry+https://github.com/rust-lang/crates.io-index" 2820 + checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 2821 + 2822 + [[package]] 2242 2823 name = "windows_x86_64_msvc" 2243 2824 version = "0.48.5" 2244 2825 source = "registry+https://github.com/rust-lang/crates.io-index" 2245 2826 checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2246 2827 2247 2828 [[package]] 2829 + name = "windows_x86_64_msvc" 2830 + version = "0.52.6" 2831 + source = "registry+https://github.com/rust-lang/crates.io-index" 2832 + checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 2833 + 2834 + [[package]] 2248 2835 name = "winnow" 2249 2836 version = "0.7.15" 2250 2837 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 2281 2868 dependencies = [ 2282 2869 "anyhow", 2283 2870 "heck", 2284 - "indexmap", 2871 + "indexmap 2.13.0", 2285 2872 "prettyplease", 2286 2873 "syn", 2287 2874 "wasm-metadata", ··· 2312 2899 dependencies = [ 2313 2900 "anyhow", 2314 2901 "bitflags", 2315 - "indexmap", 2902 + "indexmap 2.13.0", 2316 2903 "log", 2317 2904 "serde", 2318 2905 "serde_derive", ··· 2331 2918 dependencies = [ 2332 2919 "anyhow", 2333 2920 "id-arena", 2334 - "indexmap", 2921 + "indexmap 2.13.0", 2335 2922 "log", 2336 2923 "semver", 2337 2924 "serde",
+4
Cargo.toml
··· 39 39 # Observability 40 40 tracing = "0.1" 41 41 tracing-subscriber = { version = "0.3", features = ["env-filter"] } 42 + tracing-opentelemetry = "0.29" 43 + opentelemetry = { version = "0.28", features = ["trace"] } 44 + opentelemetry-otlp = { version = "0.28", features = ["trace", "grpc-tonic"] } 45 + opentelemetry_sdk = { version = "0.28", features = ["rt-tokio", "trace"] } 42 46 43 47 # HTTP middleware 44 48 tower-http = { version = "0.5", features = ["trace", "cors"] }
+157
crates/common/src/config.rs
··· 18 18 pub blobs: BlobsConfig, 19 19 pub oauth: OAuthConfig, 20 20 pub iroh: IrohConfig, 21 + pub telemetry: TelemetryConfig, 21 22 } 22 23 23 24 /// Optional privacy/ToS links surfaced by `com.atproto.server.describeServer`. ··· 44 45 /// Stub for future Iroh networking configuration. 45 46 #[derive(Debug, Clone, Deserialize, Default)] 46 47 pub struct IrohConfig {} 48 + 49 + /// OpenTelemetry telemetry configuration. 50 + #[derive(Debug, Clone)] 51 + pub struct TelemetryConfig { 52 + /// Whether to export traces via OTLP. Off by default — zero overhead when disabled. 53 + pub enabled: bool, 54 + /// OTLP gRPC endpoint for the trace exporter. 55 + pub otlp_endpoint: String, 56 + /// `service.name` resource attribute reported to the trace backend. 57 + pub service_name: String, 58 + } 59 + 60 + impl Default for TelemetryConfig { 61 + fn default() -> Self { 62 + Self { 63 + enabled: false, 64 + otlp_endpoint: "http://localhost:4317".to_string(), 65 + service_name: "ezpds-relay".to_string(), 66 + } 67 + } 68 + } 69 + 70 + #[derive(Debug, Deserialize, Default)] 71 + pub(crate) struct RawTelemetryConfig { 72 + pub(crate) enabled: Option<bool>, 73 + pub(crate) otlp_endpoint: Option<String>, 74 + pub(crate) service_name: Option<String>, 75 + } 47 76 48 77 /// Raw TOML-deserialized config with all fields optional to support env-var overlays. 49 78 #[derive(Debug, Deserialize, Default)] ··· 66 95 pub(crate) oauth: OAuthConfig, 67 96 #[serde(default)] 68 97 pub(crate) iroh: IrohConfig, 98 + #[serde(default)] 99 + pub(crate) telemetry: RawTelemetryConfig, 69 100 } 70 101 71 102 #[derive(Debug, thiserror::Error)] ··· 127 158 .map(str::to_string) 128 159 .collect(), 129 160 ); 161 + } 162 + if let Some(v) = env.get("EZPDS_TELEMETRY_ENABLED") { 163 + raw.telemetry.enabled = Some(v.parse::<bool>().map_err(|e| { 164 + ConfigError::Invalid(format!( 165 + "EZPDS_TELEMETRY_ENABLED is not a valid boolean: '{v}': {e}" 166 + )) 167 + })?); 168 + } 169 + if let Some(v) = env.get("EZPDS_OTLP_ENDPOINT") { 170 + raw.telemetry.otlp_endpoint = Some(v.clone()); 171 + } 172 + if let Some(v) = env.get("OTEL_SERVICE_NAME") { 173 + raw.telemetry.service_name = Some(v.clone()); 130 174 } 131 175 Ok(raw) 132 176 } ··· 171 215 } 172 216 let invite_code_required = raw.invite_code_required.unwrap_or(true); 173 217 218 + let telemetry_defaults = TelemetryConfig::default(); 219 + let telemetry = TelemetryConfig { 220 + enabled: raw.telemetry.enabled.unwrap_or(telemetry_defaults.enabled), 221 + otlp_endpoint: raw 222 + .telemetry 223 + .otlp_endpoint 224 + .unwrap_or(telemetry_defaults.otlp_endpoint), 225 + service_name: raw 226 + .telemetry 227 + .service_name 228 + .unwrap_or(telemetry_defaults.service_name), 229 + }; 230 + 174 231 Ok(Config { 175 232 bind_address, 176 233 port, ··· 185 242 blobs: raw.blobs, 186 243 oauth: raw.oauth, 187 244 iroh: raw.iroh, 245 + telemetry, 188 246 }) 189 247 } 190 248 ··· 496 554 let config = validate_and_build(raw).unwrap(); 497 555 498 556 assert_eq!(config.available_user_domains, vec!["foo.com", "bar.com"]); 557 + } 558 + 559 + // --- telemetry config tests --- 560 + 561 + #[test] 562 + fn telemetry_defaults_to_disabled() { 563 + let config = validate_and_build(minimal_raw()).unwrap(); 564 + assert!(!config.telemetry.enabled); 565 + assert_eq!(config.telemetry.otlp_endpoint, "http://localhost:4317"); 566 + assert_eq!(config.telemetry.service_name, "ezpds-relay"); 567 + } 568 + 569 + #[test] 570 + fn parses_telemetry_section_from_toml() { 571 + let toml = r#" 572 + data_dir = "/var/pds" 573 + public_url = "https://pds.example.com" 574 + available_user_domains = ["example.com"] 575 + 576 + [telemetry] 577 + enabled = true 578 + otlp_endpoint = "http://otel-collector:4317" 579 + service_name = "my-pds" 580 + "#; 581 + let raw: RawConfig = toml::from_str(toml).unwrap(); 582 + let config = validate_and_build(raw).unwrap(); 583 + 584 + assert!(config.telemetry.enabled); 585 + assert_eq!(config.telemetry.otlp_endpoint, "http://otel-collector:4317"); 586 + assert_eq!(config.telemetry.service_name, "my-pds"); 587 + } 588 + 589 + #[test] 590 + fn env_override_telemetry_enabled() { 591 + let env = HashMap::from([( 592 + "EZPDS_TELEMETRY_ENABLED".to_string(), 593 + "true".to_string(), 594 + )]); 595 + let raw = apply_env_overrides(minimal_raw(), &env).unwrap(); 596 + let config = validate_and_build(raw).unwrap(); 597 + 598 + assert!(config.telemetry.enabled); 599 + } 600 + 601 + #[test] 602 + fn env_override_otlp_endpoint() { 603 + let env = HashMap::from([( 604 + "EZPDS_OTLP_ENDPOINT".to_string(), 605 + "http://custom:4317".to_string(), 606 + )]); 607 + let raw = apply_env_overrides(minimal_raw(), &env).unwrap(); 608 + let config = validate_and_build(raw).unwrap(); 609 + 610 + assert_eq!(config.telemetry.otlp_endpoint, "http://custom:4317"); 611 + } 612 + 613 + #[test] 614 + fn env_override_otel_service_name() { 615 + let env = HashMap::from([( 616 + "OTEL_SERVICE_NAME".to_string(), 617 + "my-service".to_string(), 618 + )]); 619 + let raw = apply_env_overrides(minimal_raw(), &env).unwrap(); 620 + let config = validate_and_build(raw).unwrap(); 621 + 622 + assert_eq!(config.telemetry.service_name, "my-service"); 623 + } 624 + 625 + #[test] 626 + fn otel_service_name_env_overrides_toml() { 627 + let toml = r#" 628 + data_dir = "/var/pds" 629 + public_url = "https://pds.example.com" 630 + available_user_domains = ["example.com"] 631 + 632 + [telemetry] 633 + service_name = "from-toml" 634 + "#; 635 + let raw: RawConfig = toml::from_str(toml).unwrap(); 636 + let env = HashMap::from([( 637 + "OTEL_SERVICE_NAME".to_string(), 638 + "from-env".to_string(), 639 + )]); 640 + let raw = apply_env_overrides(raw, &env).unwrap(); 641 + let config = validate_and_build(raw).unwrap(); 642 + 643 + assert_eq!(config.telemetry.service_name, "from-env"); 644 + } 645 + 646 + #[test] 647 + fn env_override_telemetry_enabled_invalid_returns_error() { 648 + let env = HashMap::from([( 649 + "EZPDS_TELEMETRY_ENABLED".to_string(), 650 + "maybe".to_string(), 651 + )]); 652 + let err = apply_env_overrides(minimal_raw(), &env).unwrap_err(); 653 + 654 + assert!(matches!(err, ConfigError::Invalid(_))); 655 + assert!(err.to_string().contains("EZPDS_TELEMETRY_ENABLED")); 499 656 } 500 657 }
+6 -3
crates/common/src/config_loader.rs
··· 3 3 4 4 use crate::config::{apply_env_overrides, validate_and_build, Config, ConfigError, RawConfig}; 5 5 6 - /// Collect only `EZPDS_*` env vars from the process environment, rejecting any with non-UTF-8 7 - /// values rather than panicking (as `std::env::vars()` would on non-UTF-8 data). 6 + /// Standard OpenTelemetry env vars we read in addition to our `EZPDS_*` prefix. 7 + const OTEL_ENV_KEYS: &[&str] = &["OTEL_SERVICE_NAME"]; 8 + 9 + /// Collect `EZPDS_*` env vars and selected OTel standard vars from the process environment, 10 + /// rejecting any with non-UTF-8 values rather than panicking. 8 11 fn collect_ezpds_env() -> Result<HashMap<String, String>, ConfigError> { 9 12 let mut map = HashMap::new(); 10 13 for (key_os, val_os) in std::env::vars_os() { 11 14 let key = match key_os.to_str() { 12 - Some(k) if k.starts_with("EZPDS_") => k.to_owned(), 15 + Some(k) if k.starts_with("EZPDS_") || OTEL_ENV_KEYS.contains(&k) => k.to_owned(), 13 16 _ => continue, 14 17 }; 15 18 let val = val_os.into_string().map_err(|_| {
+1
crates/common/src/lib.rs
··· 4 4 5 5 pub use config::{ 6 6 BlobsConfig, Config, ConfigError, ContactConfig, IrohConfig, OAuthConfig, ServerLinksConfig, 7 + TelemetryConfig, 7 8 }; 8 9 pub use config_loader::load_config; 9 10 pub use error::{ApiError, ErrorCode};
+4
crates/relay/Cargo.toml
··· 18 18 thiserror = { workspace = true } 19 19 tracing = { workspace = true } 20 20 tracing-subscriber = { workspace = true } 21 + tracing-opentelemetry = { workspace = true } 22 + opentelemetry = { workspace = true } 23 + opentelemetry-otlp = { workspace = true } 24 + opentelemetry_sdk = { workspace = true } 21 25 tokio = { workspace = true } 22 26 tower-http = { workspace = true } 23 27 serde = { workspace = true }
+47 -3
crates/relay/src/app.rs
··· 1 1 use std::sync::Arc; 2 2 3 - use axum::{extract::Path, routing::get, Router}; 3 + use axum::{extract::Path, http::Request, routing::get, Router}; 4 4 use common::{ApiError, Config, ErrorCode}; 5 + use opentelemetry::propagation::Extractor; 5 6 use tower_http::{cors::CorsLayer, trace::TraceLayer}; 7 + use tracing_opentelemetry::OpenTelemetrySpanExt; 6 8 7 9 use crate::routes::describe_server::describe_server; 8 10 use crate::routes::health::health; 9 11 12 + /// Wraps an `axum::http::HeaderMap` as an OTel text-map [`Extractor`] so that 13 + /// the W3C `traceparent` and `tracestate` headers can be read by the global propagator. 14 + struct HeaderMapCarrier<'a>(&'a axum::http::HeaderMap); 15 + 16 + impl Extractor for HeaderMapCarrier<'_> { 17 + fn get(&self, key: &str) -> Option<&str> { 18 + self.0.get(key).and_then(|v| v.to_str().ok()) 19 + } 20 + 21 + fn keys(&self) -> Vec<&str> { 22 + self.0.keys().map(|k| k.as_str()).collect() 23 + } 24 + } 25 + 26 + /// Custom `MakeSpan` for [`TraceLayer`] that: 27 + /// 1. Creates an `info_span` with standard HTTP attributes pre-declared. 28 + /// 2. Extracts an incoming W3C `traceparent` header and sets it as the parent context 29 + /// on the new span so upstream traces are joined correctly. 30 + #[derive(Clone, Default)] 31 + struct OtelMakeSpan; 32 + 33 + impl<B> tower_http::trace::MakeSpan<B> for OtelMakeSpan { 34 + fn make_span(&mut self, request: &Request<B>) -> tracing::Span { 35 + let span = tracing::info_span!( 36 + "HTTP request", 37 + http.method = %request.method(), 38 + http.target = request.uri().path_and_query().map_or("", |pq| pq.as_str()), 39 + http.status_code = tracing::field::Empty, 40 + otel.status_code = tracing::field::Empty, 41 + ); 42 + 43 + // Inject parent trace context from incoming W3C traceparent/tracestate headers. 44 + // When telemetry is disabled the global propagator is a no-op, so this is free. 45 + let parent_cx = opentelemetry::global::get_text_map_propagator(|p| { 46 + p.extract(&HeaderMapCarrier(request.headers())) 47 + }); 48 + span.set_parent(parent_cx); 49 + span 50 + } 51 + } 52 + 10 53 /// Shared application state cloned into every request handler via Axum's `State` extractor. 11 54 #[derive(Clone)] 12 55 pub struct AppState { ··· 27 70 ) 28 71 .route("/xrpc/:method", get(xrpc_handler).post(xrpc_handler)) 29 72 .layer(CorsLayer::permissive()) 30 - .layer(TraceLayer::new_for_http()) 73 + .layer(TraceLayer::new_for_http().make_span_with(OtelMakeSpan)) 31 74 .with_state(state) 32 75 } 33 76 ··· 46 89 /// The pool is fully migrated, so the schema is present and ready for handler tests. 47 90 #[cfg(test)] 48 91 pub(crate) async fn test_state() -> AppState { 49 - use common::{BlobsConfig, IrohConfig, OAuthConfig}; 92 + use common::{BlobsConfig, IrohConfig, OAuthConfig, TelemetryConfig}; 50 93 use std::path::PathBuf; 51 94 52 95 let pool = crate::db::open_pool("sqlite::memory:") ··· 70 113 blobs: BlobsConfig::default(), 71 114 oauth: OAuthConfig::default(), 72 115 iroh: IrohConfig::default(), 116 + telemetry: TelemetryConfig::default(), 73 117 }), 74 118 db: pool, 75 119 }
+2
crates/relay/src/db/mod.rs
··· 33 33 }]; 34 34 35 35 /// Open a WAL-mode SQLite connection pool with a maximum of 1 connection. 36 + #[tracing::instrument(err, fields(db.system = "sqlite"))] 36 37 /// 37 38 /// Accepts any sqlx URL string (e.g. `"sqlite:relay.db"`, `"sqlite::memory:"`). 38 39 /// `create_if_missing` is enabled so the file is created on first run. ··· 55 56 } 56 57 57 58 /// Apply any pending migrations from `MIGRATIONS` to the given pool. 59 + #[tracing::instrument(skip(pool), err, fields(db.system = "sqlite"))] 58 60 /// 59 61 /// The schema_migrations bootstrap DDL runs outside any transaction. Pending migrations 60 62 /// and their bookkeeping inserts run inside a single transaction per call.
+6 -5
crates/relay/src/main.rs
··· 5 5 mod app; 6 6 mod db; 7 7 mod routes; 8 + mod telemetry; 8 9 9 10 /// Convert a config database_url (which may be a plain filesystem path or a sqlx URL) to a valid sqlx URL. 10 11 /// ··· 43 44 } 44 45 45 46 async fn run() -> anyhow::Result<()> { 46 - tracing_subscriber::fmt() 47 - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) 48 - .try_init() 49 - .map_err(|e| anyhow::anyhow!("failed to initialize tracing subscriber: {e}"))?; 50 - 51 47 let cli = Cli::parse(); 52 48 let config_path = cli.config.unwrap_or_else(|| PathBuf::from("relay.toml")); 53 49 54 50 let config = common::load_config(&config_path) 55 51 .with_context(|| format!("failed to load config from {}", config_path.display()))?; 52 + 53 + // Initialize tracing after config is loaded so telemetry settings can be applied. 54 + // Any config parse error surfaces via eprintln (the error propagation above); tracing 55 + // is not available until this line succeeds. 56 + let _otel_guard = telemetry::init_subscriber(&config.telemetry)?; 56 57 57 58 tracing::info!( 58 59 bind_address = %config.bind_address,
+83
crates/relay/src/telemetry.rs
··· 1 + use anyhow::Context; 2 + use common::TelemetryConfig; 3 + use opentelemetry::trace::TracerProvider as _; 4 + use opentelemetry_otlp::WithExportConfig; 5 + use opentelemetry_sdk::{trace::SdkTracerProvider, Resource}; 6 + use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; 7 + 8 + /// Holds the OTel tracer provider and shuts it down (flushing all pending spans) on drop. 9 + /// 10 + /// Keep this value alive for the full process lifetime. It is returned by 11 + /// [`init_subscriber`] when telemetry is enabled and should be stored in `main` before 12 + /// the server starts and dropped after the server completes its graceful shutdown. 13 + pub struct OtelGuard { 14 + provider: SdkTracerProvider, 15 + } 16 + 17 + impl Drop for OtelGuard { 18 + fn drop(&mut self) { 19 + if let Err(e) = self.provider.shutdown() { 20 + eprintln!("failed to flush OTel spans on shutdown: {e:?}"); 21 + } 22 + } 23 + } 24 + 25 + /// Initialize the global tracing subscriber. 26 + /// 27 + /// When `telemetry.enabled` is `true`, installs a layered subscriber that writes to both 28 + /// stdout (the `fmt` layer, keeping existing log behaviour) and an OTLP-compatible 29 + /// backend (the `opentelemetry` layer). Also registers the W3C Trace Context propagator 30 + /// globally so that incoming `traceparent`/`tracestate` headers are honoured. 31 + /// 32 + /// When `telemetry.enabled` is `false` (the default), only the `fmt` layer is installed — 33 + /// identical to the previous behaviour with zero OTel overhead. 34 + /// 35 + /// Returns an [`OtelGuard`] when telemetry is enabled. Drop it after the server shuts 36 + /// down to guarantee all buffered spans are flushed before the process exits. 37 + pub fn init_subscriber(telemetry: &TelemetryConfig) -> anyhow::Result<Option<OtelGuard>> { 38 + let env_filter = 39 + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); 40 + let fmt_layer = tracing_subscriber::fmt::layer(); 41 + 42 + if !telemetry.enabled { 43 + tracing_subscriber::registry() 44 + .with(env_filter) 45 + .with(fmt_layer) 46 + .try_init() 47 + .map_err(|e| anyhow::anyhow!("failed to initialize tracing subscriber: {e}"))?; 48 + return Ok(None); 49 + } 50 + 51 + let resource = Resource::builder() 52 + .with_service_name(telemetry.service_name.clone()) 53 + .build(); 54 + 55 + let exporter = opentelemetry_otlp::SpanExporter::builder() 56 + .with_tonic() 57 + .with_endpoint(&telemetry.otlp_endpoint) 58 + .build() 59 + .context("failed to build OTLP span exporter")?; 60 + 61 + let provider = SdkTracerProvider::builder() 62 + .with_resource(resource) 63 + .with_batch_exporter(exporter) 64 + .build(); 65 + 66 + // W3C Trace Context propagator: extracts/injects `traceparent` and `tracestate` headers. 67 + opentelemetry::global::set_text_map_propagator( 68 + opentelemetry_sdk::propagation::TraceContextPropagator::new(), 69 + ); 70 + opentelemetry::global::set_tracer_provider(provider.clone()); 71 + 72 + let tracer = provider.tracer("relay"); 73 + let otel_layer = tracing_opentelemetry::layer().with_tracer(tracer); 74 + 75 + tracing_subscriber::registry() 76 + .with(env_filter) 77 + .with(fmt_layer) 78 + .with(otel_layer) 79 + .try_init() 80 + .map_err(|e| anyhow::anyhow!("failed to initialize tracing subscriber: {e}"))?; 81 + 82 + Ok(Some(OtelGuard { provider })) 83 + }