diff --git a/Cargo.lock b/Cargo.lock index 359e61d..a4930ad 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,17 +17,41 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" -version = "0.6.19" +version = "0.6.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" +checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" dependencies = [ "anstyle", "anstyle-parse", @@ -55,29 +79,29 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" +checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" dependencies = [ - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.9" +version = "3.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" +checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "anyhow" -version = "1.0.98" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "atomic-waker" @@ -114,9 +138,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bitflags" -version = "2.9.1" +version = "2.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +checksum = "34efbcccd345379ca2868b2b2c9d3782e9cc58ba87bc7d79d5b53d9c9ae6f25d" [[package]] name = "bumpalo" @@ -132,18 +156,18 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "cc" -version = "1.2.29" +version = "1.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" +checksum = "42bc4aea80032b7bf409b0bc7ccad88853858911b7713a8062fdc0623867bedc" dependencies = [ "shlex", ] [[package]] name = "cfg-if" -version = "1.0.1" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "cfg_aliases" @@ -151,11 +175,23 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-link", +] + [[package]] name = "clap" -version = "4.5.41" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9" +checksum = "2c5e4fcf9c21d2e544ca1ee9d8552de13019a42aa7dbf32747fa7aaf1df76e57" dependencies = [ "clap_builder", "clap_derive", @@ -163,9 +199,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.41" +version = "4.5.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d" +checksum = "fecb53a0e6fcfb055f686001bc2e2592fa527efaf38dbe81a6a9563562e57d41" dependencies = [ "anstream", "anstyle", @@ -175,9 +211,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.41" +version = "4.5.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491" +checksum = "14cb31bb0a7d536caef2639baa7fad459e15c3144efefa6dbd1c84562c4739f6" dependencies = [ "heck", "proc-macro2", @@ -275,6 +311,19 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "flexi_logger" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759bfa52db036a2db54f0b5f0ff164efa249b3014720459c5ea4198380c529bc" +dependencies = [ + "chrono", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -304,9 +353,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" dependencies = [ "percent-encoding", ] @@ -441,9 +490,9 @@ checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" [[package]] name = "governor" -version = "0.10.0" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbe789d04bf14543f03c4b60cd494148aa79438c8440ae7d81a7778147745c3" +checksum = "444405bbb1a762387aa22dd569429533b54a1d8759d35d3b64cb39b0293eaa19" dependencies = [ "cfg-if", "dashmap", @@ -451,7 +500,7 @@ dependencies = [ "futures-timer", "futures-util", "getrandom 0.3.3", - "hashbrown 0.15.4", + "hashbrown 0.15.5", "nonzero_ext", "parking_lot", "portable-atomic", @@ -464,9 +513,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" dependencies = [ "atomic-waker", "bytes", @@ -489,9 +538,9 @@ checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" [[package]] name = "hashbrown" -version = "0.15.4" +version = "0.15.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" dependencies = [ "allocator-api2", "equivalent", @@ -546,19 +595,21 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" [[package]] name = "hyper" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +checksum = "eb3aa54a13a0dfe7fbe3a59e0c76093041720fdc77b110cc0fc260fafb4dc51e" dependencies = [ + "atomic-waker", "bytes", "futures-channel", - "futures-util", + "futures-core", "h2", "http", "http-body", "httparse", "itoa", "pin-project-lite", + "pin-utils", "smallvec", "tokio", "want", @@ -599,9 +650,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" dependencies = [ "base64", "bytes", @@ -623,6 +674,30 @@ dependencies = [ "windows-registry", ] +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -711,9 +786,9 @@ dependencies = [ [[package]] name = "idna" -version = "1.0.3" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" dependencies = [ "idna_adapter", "smallvec", @@ -732,19 +807,19 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +checksum = "f2481980430f9f78649238835720ddccc57e52df14ffce1c6f37391d61b563e9" dependencies = [ "equivalent", - "hashbrown 0.15.4", + "hashbrown 0.15.5", ] [[package]] name = "io-uring" -version = "0.7.8" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013" +checksum = "046fa2d4d00aea763528b4950358d0ead425372445dc8ff86312b3c69ff7727b" dependencies = [ "bitflags", "cfg-if", @@ -789,17 +864,11 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - [[package]] name = "libc" -version = "0.2.174" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] name = "linux-raw-sys" @@ -892,12 +961,20 @@ checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" [[package]] name = "nu-ansi-term" -version = "0.46.0" +version = "0.50.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" dependencies = [ - "overload", - "winapi", + "windows-sys 0.52.0", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -965,14 +1042,26 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "orcid-fetcher-logger" +version = "0.0.0" +dependencies = [ + "anyhow", + "flexi_logger", + "log", +] + [[package]] name = "orcid-works-cli" version = "0.2.1" dependencies = [ "anyhow", "clap", + "flexi_logger", "futures", "governor", + "log", + "orcid-fetcher-logger", "orcid-works-model", "reqwest", "serde", @@ -980,8 +1069,7 @@ dependencies = [ "serde_path_to_error", "tempfile", "tokio", - "tracing", - "tracing-subscriber", + "uuid", ] [[package]] @@ -992,12 +1080,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - [[package]] name = "parking_lot" version = "0.12.4" @@ -1023,9 +1105,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "pin-project-lite" @@ -1071,9 +1153,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.95" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] @@ -1095,9 +1177,9 @@ dependencies = [ [[package]] name = "quinn" -version = "0.11.8" +version = "0.11.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "626214629cda6781b6dc1d316ba307189c85ba657213ce642d9c77670f8202c8" +checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20" dependencies = [ "bytes", "cfg_aliases", @@ -1115,9 +1197,9 @@ dependencies = [ [[package]] name = "quinn-proto" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49df843a9161c85bb8aae55f101bc0bac8bcafd637a620d9122fd7e0b2f7422e" +checksum = "f1906b49b0c3bc04b5fe5d86a77925ae6524a19b816ae38ce1e426255f1d8a31" dependencies = [ "bytes", "getrandom 0.3.3", @@ -1136,16 +1218,16 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.13" +version = "0.5.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcebb1209ee276352ef14ff8732e24cc2b02bbac986cd74a4c81bcb2f9881970" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" dependencies = [ "cfg_aliases", "libc", "once_cell", "socket2", "tracing", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1165,9 +1247,9 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" [[package]] name = "rand" -version = "0.9.1" +version = "0.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" dependencies = [ "rand_chacha", "rand_core", @@ -1203,18 +1285,47 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.13" +version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" + [[package]] name = "reqwest" -version = "0.12.22" +version = "0.12.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531" +checksum = "d429f34c8092b2d42c7c93cec323bb4adeb7c67698f70839adec842ec10c7ceb" dependencies = [ "base64", "bytes", @@ -1270,9 +1381,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.25" +version = "0.1.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" [[package]] name = "rustc-hash" @@ -1282,22 +1393,22 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" [[package]] name = "rustix" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "rustls" -version = "0.23.29" +version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ "once_cell", "ring", @@ -1330,9 +1441,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.21" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" @@ -1400,9 +1511,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.143" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a" dependencies = [ "itoa", "memchr", @@ -1432,15 +1543,6 @@ dependencies = [ "serde", ] -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - [[package]] name = "shlex" version = "1.3.0" @@ -1449,9 +1551,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "slab" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" @@ -1461,12 +1563,12 @@ checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" -version = "0.5.10" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1498,9 +1600,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.104" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", @@ -1550,46 +1652,37 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.20.0" +version = "3.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e" dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", "rustix", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] name = "thiserror" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +checksum = "3467d614147380f2e4e374161426ff399c91084acd2363eaf549172b3d5e60c0" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.12" +version = "2.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +checksum = "6c5e1be1c48b9172ee610da68fd9cd2770e7a4056cb3fc98710ee6906f0c7960" dependencies = [ "proc-macro2", "quote", "syn", ] -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - [[package]] name = "tinystr" version = "0.8.1" @@ -1602,9 +1695,9 @@ dependencies = [ [[package]] name = "tinyvec" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" dependencies = [ "tinyvec_macros", ] @@ -1617,9 +1710,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.46.1" +version = "1.47.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" dependencies = [ "backtrace", "bytes", @@ -1630,7 +1723,7 @@ dependencies = [ "slab", "socket2", "tokio-macros", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1666,9 +1759,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.15" +version = "0.7.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" dependencies = [ "bytes", "futures-core", @@ -1729,21 +1822,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "pin-project-lite", - "tracing-attributes", "tracing-core", ] -[[package]] -name = "tracing-attributes" -version = "0.1.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "tracing-core" version = "0.1.34" @@ -1751,32 +1832,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", ] [[package]] @@ -1799,13 +1854,14 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.5.4" +version = "2.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" dependencies = [ "form_urlencoded", "idna", "percent-encoding", + "serde", ] [[package]] @@ -1821,10 +1877,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "valuable" -version = "0.1.1" +name = "uuid" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" +checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "wasm-bindgen", +] [[package]] name = "vcpkg" @@ -1949,9 +2010,9 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8782dd5a41a24eed3a4f40b606249b3e236ca61adf1f25ea4d45c73de122b502" +checksum = "7e8983c3ab33d6fb807cfcdad2491c4ea8cbc8ed839181c7dfd9c67c83e261b2" dependencies = [ "rustls-pki-types", ] @@ -1978,6 +2039,41 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "windows-link" version = "0.1.3" @@ -2037,7 +2133,7 @@ version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.2", + "windows-targets 0.53.3", ] [[package]] @@ -2058,10 +2154,11 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.2" +version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -2267,9 +2364,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.2" +version = "0.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" dependencies = [ "yoke", "zerofrom", diff --git a/Cargo.toml b/Cargo.toml index 385630c..33f29a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ + "crates/orcid-fetcher-logger", "crates/orcid-works-model", - "crates/orcid-works-cli" + "crates/orcid-works-cli", ] resolver = "2" diff --git a/README.md b/README.md index 9183a90..ef3aa02 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,9 @@ orcid-works-cli --id $ORCID_ID [Options] | `--rate-limit` \ | Requests-per-second cap (1–40). See also [Guidelines](#guidelines) section. | `12` | | `--user-agent-note` \ | Text appended to the built-in User-Agent string | *(none)* | | `--force-fetch` | Ignore diff and refetch every work-detail entry | `false` | +| `-v, -vv`, `--verbose` | Increase console verbosity | — | +| `-q, -qq, -qqq`, `--quiet` | Decrease console verbosity | — | +| `-l`, `--log` \ | Output trace log file path (parent dirs auto-created) | *(none)* | | `-h`, `--help` | Print help | — | | `-V`, `--version` | Print version | — | diff --git a/crates/orcid-fetcher-logger/Cargo.toml b/crates/orcid-fetcher-logger/Cargo.toml new file mode 100644 index 0000000..147e792 --- /dev/null +++ b/crates/orcid-fetcher-logger/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "orcid-fetcher-logger" +version = "0.0.0" +edition = "2024" +license = "Apache-2.0" + +[dependencies] +anyhow = "1" +flexi_logger = "0.31" +log = "0.4" diff --git a/crates/orcid-fetcher-logger/src/lib.rs b/crates/orcid-fetcher-logger/src/lib.rs new file mode 100644 index 0000000..c0085d8 --- /dev/null +++ b/crates/orcid-fetcher-logger/src/lib.rs @@ -0,0 +1,32 @@ +use anyhow::{Context, Result}; +use flexi_logger::{Duplicate, FileSpec, LevelFilter, Logger}; +use std::path::PathBuf; + +pub fn console_level(v: u8, q: u8) -> LevelFilter { + let def_idx = 3i8; // def: Info + let idx = def_idx + v as i8 - q as i8; + match idx { + i8::MIN..=0 => LevelFilter::Off, // -qqq + 1 => LevelFilter::Error, // -qq + 2 => LevelFilter::Warn, // -q + 3 => LevelFilter::Info, // default + 4 => LevelFilter::Debug, // -v + _ => LevelFilter::Trace, // -vv + } +} + +pub fn init_logger(console_level: LevelFilter, log_file: Option) -> Result<()> { + let logger = if let Some(p) = &log_file { + Logger::with(LevelFilter::Trace) + .log_to_file(FileSpec::try_from(p)?) + .duplicate_to_stderr(Duplicate::from(console_level)) + } else { + Logger::with(console_level) + }; + + if let Err(e) = logger.start() { + return Err(e).context("failed to start logger"); + } + + Ok(()) +} diff --git a/crates/orcid-works-cli/Cargo.toml b/crates/orcid-works-cli/Cargo.toml index ad8f936..44228dc 100644 --- a/crates/orcid-works-cli/Cargo.toml +++ b/crates/orcid-works-cli/Cargo.toml @@ -6,16 +6,19 @@ build = "build.rs" license = "Apache-2.0" [dependencies] +orcid-fetcher-logger = { path = "../orcid-fetcher-logger" , version = "0.0.0" } orcid-works-model = { path = "../orcid-works-model" , version = "0.2.1" } + anyhow = "1" clap = { version = "4", features = ["derive"] } +flexi_logger = "0.31" futures = "0.3" governor = "0.10" +log = "0.4" reqwest = { version = "0.12", features = ["json", "rustls-tls"] } serde = { version = "1", features = ["derive"] } serde_json = "1" serde_path_to_error = "0.1" tempfile = "3" tokio = { version = "1", features = ["rt-multi-thread", "macros"] } -tracing = "0.1" -tracing-subscriber = "0.3" +uuid = { version = "1", features = ["v4"] } diff --git a/crates/orcid-works-cli/src/api.rs b/crates/orcid-works-cli/src/api.rs index 3c2f4cb..68b6505 100644 --- a/crates/orcid-works-cli/src/api.rs +++ b/crates/orcid-works-cli/src/api.rs @@ -1,69 +1,85 @@ use anyhow::{Context, Result, bail}; +use log::{debug, error}; use reqwest::{ Client, header::{ACCEPT, HeaderValue}, }; use serde::de::DeserializeOwned; -use tracing::{Instrument, error, instrument}; +use uuid::Uuid; use orcid_works_model::{OrcidWorkDetail, OrcidWorks}; const BASE: &str = "https://pub.orcid.org/v3.0"; -const JSON_ACCEPT: &str = "application/json"; // Build HTTP Client pub(crate) fn build_client(ua: &str) -> Result { - let client = Client::builder().user_agent(ua).build()?; + let client = match Client::builder().user_agent(ua).build() { + Ok(cli) => cli, + Err(e) => { + let eid = Uuid::new_v4(); + error!("[{eid}] failed to build HTTP client"); + debug!("[{eid}] ua={ua}"); + return Err(e).with_context(|| format!("[{eid}] failed to build HTTP client")); + } + }; Ok(client) } // Get JSON from URL -#[instrument(name = "get_json", skip_all)] async fn get_json(client: &Client, url: &str) -> Result where T: DeserializeOwned, { - let res = client + let res = match client .get(url) - .header(ACCEPT, HeaderValue::from_static(JSON_ACCEPT)) + .header(ACCEPT, HeaderValue::from_static("application/json")) .send() .await - .with_context(|| format!("GET {url}"))?; + { + Ok(r) => r, + Err(e) => { + let eid = Uuid::new_v4(); + error!("E[{eid}] HTTP connection failure"); + debug!("E[{eid}] url={url}"); + return Err(e).with_context(|| format!("[{eid}] HTTP connection failure")); + } + }; if res.error_for_status_ref().is_err() { let status = res.status(); let body = res.text().await.unwrap_or_default(); - error!(%status, %url, response_body = %body, "HTTP error"); - bail!("HTTP {status} while GET {url}: {body}"); + let eid = Uuid::new_v4(); + error!("E[{eid}] HTTP {status}"); + debug!("E[{eid}] url={url}"); + debug!("E[{eid}] response body={body}"); + bail!("E[{eid}] HTTP {status}"); } match res.json::().await { Ok(parsed) => Ok(parsed), - Err(e) => { + let eid = Uuid::new_v4(); if e.is_decode() { - error!(%url, err = %e, "JSON parse failure"); + error!("E[{eid}] JSON parse failure"); } else { - error!(%url, err = %e, "response body read failure"); + error!("E[{eid}] response body read failure"); } - Err(e).with_context(|| format!("parse JSON from {url}")) + debug!("E[{eid}] url={url}"); + Err(e).with_context(|| format!("E[{eid}] failed to parse JSON")) } } } // GET /{id}/works -#[instrument(name = "fetch_works", skip_all)] pub async fn fetch_works(client: &reqwest::Client, id: &str) -> Result { let url = format!("{BASE}/{id}/works"); get_json::(client, &url) - .in_current_span() .await - .with_context(|| format!("fetch work summaries for ORCID iD {id}")) + .with_context(|| format!("failed to fetch work summaries of ORCID iD {id}")) } // GET /{id}/work/{putcode} -#[instrument(name = "fetch_work_detail", skip_all)] pub async fn fetch_work_detail( client: &reqwest::Client, id: &str, @@ -72,7 +88,6 @@ pub async fn fetch_work_detail( let url = format!("{BASE}/{id}/work/{putcode}"); get_json::(client, &url) - .in_current_span() .await - .with_context(|| format!("fetch work detail of putcode {putcode}")) + .with_context(|| format!("failed to fetch work detail of putcode {putcode}")) } diff --git a/crates/orcid-works-cli/src/io.rs b/crates/orcid-works-cli/src/io.rs index 385046f..bac4204 100644 --- a/crates/orcid-works-cli/src/io.rs +++ b/crates/orcid-works-cli/src/io.rs @@ -1,101 +1,126 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; +use log::{debug, error, info}; use serde_path_to_error::deserialize; use std::{ fs::File, - io::{BufReader, BufWriter, ErrorKind, Write}, + io::{BufReader, ErrorKind, Write}, path::Path, }; - use tempfile::NamedTempFile; -use tracing::{error, info, instrument, warn}; +use uuid::Uuid; use orcid_works_model::OrcidWorkDetailFile; // Read the existing JSON file; use the empty list if absent. -#[instrument(name = "read_work_details_json", skip_all)] pub(crate) fn read_work_details_json>(path: P) -> Result { let path = path.as_ref(); match File::open(path) { Ok(file) => { + // Treat zero-byte files as an empty JSON list. + if file.metadata()?.len() == 0 { + return Ok(OrcidWorkDetailFile { records: vec![] }); + } + let reader = BufReader::new(file); let mut de = serde_json::Deserializer::from_reader(reader); - let data: OrcidWorkDetailFile = deserialize(&mut de).map_err(|e| { - error!( - path = path.display().to_string(), - err = %e, - "JSON parse failure" - ); - e - })?; + let eid = Uuid::new_v4(); + let data: OrcidWorkDetailFile = deserialize(&mut de) + .inspect_err(|e| { + error!("E[{eid}] JSON parse failure: path={}", path.display()); + debug!("E[{eid}] error: {e}"); + }) + .with_context(|| { + format!("E[{eid}] failed to parse JSON file {}", path.display()) + })?; Ok(data) } Err(e) if e.kind() == ErrorKind::NotFound => { - info!( - path = path.display().to_string(), - "file not found; use empty JSON" - ); + info!("file not found; use empty JSON: path={}", path.display()); Ok(OrcidWorkDetailFile { records: vec![] }) } Err(e) => { - error!(path= path.display().to_string(), err = %e, "failed to open work-detail file"); - Err(e).with_context(|| format!("open {}", path.display())) + let eid = Uuid::new_v4(); + error!( + "E[{eid}] failed to open work-detail file: path={}", + path.display() + ); + debug!("E[{eid}] error: {e}"); + Err(e).with_context(|| format!("E[{eid}] failed to open {}", path.display())) } } } // Write JSON file -#[instrument(name = "write_pretty_json", skip_all)] pub(crate) fn write_pretty_json>( path: P, value: &OrcidWorkDetailFile, ) -> Result<()> { let path = path.as_ref(); + + // Fail fast if the target path is a directory. + if path.is_dir() { + bail!("output path is a directory: {}", path.display()); + } + let parent = path.parent().unwrap_or(Path::new(".")); - let mut tmp = match NamedTempFile::new_in(parent) - .with_context(|| format!("create temp file for {}", path.display())) - { + let mut temp = match NamedTempFile::new_in(parent) { Ok(f) => f, Err(e) => { - error!(path = path.display().to_string(), err = %e, "failed to create temp file"); - return Err(e); + let eid = Uuid::new_v4(); + debug!("E[{eid}] error: {e}"); + error!( + "E[{eid}] failed to create temp file: path={}", + path.display() + ); + return Err(e).with_context(|| { + format!("E[{eid}] failed to create temp file for {}", path.display()) + }); } }; - if let Err(e) = serde_json::to_writer_pretty(BufWriter::new(&mut tmp), value) - .with_context(|| format!("serialize JSON into {}", path.display())) - { - error!(path = path.display().to_string(), err = %e, "JSON serialization failure"); - return Err(e); + if let Err(e) = serde_json::to_writer_pretty(&mut temp, value) { + let eid = Uuid::new_v4(); + debug!("E[{eid}] error: {e}"); + error!( + "E[{eid}] JSON serialisation failure: path={}", + path.display(), + ); + return Err(e) + .with_context(|| format!("E[{eid}] failed to serialise JSON into {}", path.display())); } - if let Err(e) = tmp.as_file_mut().flush() { - error!(path = path.display().to_string(), err = %e, "flush failure"); - return Err(e).context("flush tmp file"); - } - if let Err(e) = tmp.as_file_mut().sync_all() { - error!(path = path.display().to_string(), err = %e, "fsync failure"); - return Err(e).context("fsync tmp file"); + if let Err(e) = temp.as_file_mut().flush() { + let eid = Uuid::new_v4(); + debug!("E[{eid}] error: {e}"); + error!("E[{eid}] flush failure: path={}", path.display()); + return Err(e) + .with_context(|| format!("E[{eid}] failed to flush temp file {}", path.display())); } - if let Err(e) = tmp - .persist(path) - .map_err(|e| e.error) - .with_context(|| format!("rename temp file into {}", path.display())) - { - error!(path = path.display().to_string(), err = %e, "atomic rename failure"); - return Err(e); + if let Err(e) = temp.as_file_mut().sync_all() { + let eid = Uuid::new_v4(); + debug!("E[{eid}] error: {e}"); + error!("E[{eid}] fsync failure: path={}", path.display()); + return Err(e) + .with_context(|| format!("E[{eid}] failed to fsync temp file {}", path.display())); } - if let Ok(dir) = parent.to_owned().canonicalize() { - if let Ok(dir_fd) = File::open(&dir) { - let _ = dir_fd.sync_all(); - } + if let Err(e) = temp.persist(path).map_err(|e| e.error) { + let eid = Uuid::new_v4(); + debug!("E[{eid}] error: {e}"); + error!("E[{eid}] atomic rename failure path={}", path.display()); + return Err(e).with_context(|| { + format!( + "E[{eid}] failed to rename temp file into {}", + path.display() + ) + }); } Ok(()) diff --git a/crates/orcid-works-cli/src/main.rs b/crates/orcid-works-cli/src/main.rs index 486780c..61e444f 100644 --- a/crates/orcid-works-cli/src/main.rs +++ b/crates/orcid-works-cli/src/main.rs @@ -1,11 +1,11 @@ use anyhow::{Context, Result}; -use clap::Parser; +use clap::{ArgAction::Count, Parser, ValueHint}; use futures::stream::{self, StreamExt, TryStreamExt}; use governor::{Quota, RateLimiter, clock::DefaultClock, state::InMemoryState, state::NotKeyed}; +use log::{debug, error, info, warn}; use std::{collections::HashMap, num::NonZeroU32, path::PathBuf, sync::Arc}; -use tracing::{Instrument, info, info_span, warn}; - +use orcid_fetcher_logger::{console_level, init_logger}; use orcid_works_model::{OrcidWorkDetail, OrcidWorkDetailFile, OrcidWorks}; mod api; @@ -67,7 +67,8 @@ struct Cli { short = 'o', long, default_value = "./output.json", - help = "Output path to the JSON file; Parent dirs are created if absent." + help = "Write full work details JSON to FILE; Parent dirs are created if absent.", + value_hint = ValueHint::FilePath, )] out: PathBuf, @@ -99,34 +100,65 @@ struct Cli { help = "Ignore diff and refetch every work-detail entry" )] force_fetch: bool, + + #[arg( + short, + long, + action = Count, + help = "Increase console verbosity: -v = DEBUG, -vv = TRACE. [default: INFO]", + )] + verbose: u8, + + #[arg( + short, + long, + action = Count, + help = "Decrease console verbosity: -q = WARN, -qq = ERROR, -qqq = OFF. [default: INFO]", + )] + quiet: u8, + + #[arg( + short, + long, + help = "Write full trace log to FILE; Parent dirs are created if absent.", + value_hint = ValueHint::FilePath, + )] + log: Option, } #[tokio::main] async fn main() -> Result<()> { - tracing_subscriber::fmt().init(); + let cli = Cli::parse(); + + // initialise logger + let console_level = console_level(cli.verbose, cli.quiet); + init_logger(console_level, cli.log.clone())?; + info!("console log level: {console_level}"); - if let Err(err) = run().await { - tracing::error!(err = %err, "fatal error; exiting:"); - std::process::exit(1); + // run + if let Err(e) = run(cli).await { + error!("fatal error; exiting: {e}"); + debug!("error chain: {e:#?}"); + return Err(e); } Ok(()) } -async fn run() -> anyhow::Result<()> { - let cli = Cli::parse(); - +async fn run(cli: Cli) -> Result<()> { // Concurrency & Rate Limit check if cli.concurrency > cli.rate_limit.try_into().unwrap() { warn!( - concurrency = &cli.concurrency, - rate_limit = &cli.rate_limit, - "concurrency exceeds rate-limit" + "concurrency exceeds rate-limit: concurrency={} rate_limit={}", + cli.concurrency, cli.rate_limit ); } // HTTP client let ua = build_user_agent(cli.user_agent_note); + + debug!("building HTTP client: UA={ua}"); + let client = build_client(&ua)?; let id: Arc = cli.id.clone().into(); @@ -137,12 +169,12 @@ async fn run() -> anyhow::Result<()> { // Open the existing work details JSON info!( - path = &cli.out.display().to_string(), - "opening the existing work-details JSON" + "loading the existing work-details JSON from {}", + &cli.out.display() ); let existing: OrcidWorkDetailFile = read_work_details_json(&cli.out).with_context(|| { format!( - "open the existing work-details JSON from {}", + "failed to load the existing work-details JSON from {}", &cli.out.display() ) })?; @@ -153,11 +185,13 @@ async fn run() -> anyhow::Result<()> { .collect(); // Fetch works summaries - info!(id = &cli.id, "fetching work summaries"); - let works: OrcidWorks = fetch_works(&client, &cli.id) - .in_current_span() - .await - .with_context(|| "update cache: fetch work summaries")?; + info!("Check updates on work summaries of ORCID iD {}", cli.id); + let works: OrcidWorks = fetch_works(&client, &cli.id).await.with_context(|| { + format!( + "failed to check updates on work summaries of ORCID iD {}", + cli.id + ) + })?; // Detect changes let diff_map = diff_putcodes(&existing_map, &works, cli.force_fetch); @@ -167,50 +201,34 @@ async fn run() -> anyhow::Result<()> { let deleted = deleted_putcodes(&diff_map); info!( - added = added.len(), - updated = updated.len(), - deleted = deleted.len(), - "diff stats" + "diff stats: added={} updated={} deleted={}", + added.len(), + updated.len(), + deleted.len(), ); // Exit if no changes detected let to_fetch: Vec = added.into_iter().chain(updated.into_iter()).collect(); if to_fetch.len() + deleted.len() == 0 { - info!( - to_fetch = to_fetch.len(), - deleted = deleted.len(), - "no changes detected - skip fetch & rewrite" - ); + info!("no changes detected - skip fetch & rewrite"); return Ok(()); } // Parallel fetch work details - info!(id = &cli.id, "fetching work details"); - let batch_span = info_span!("fetch_work_details_batch", - id = %*id, - total = to_fetch.len()); + info!("fetching work details of ORCID iD {}", cli.id); let fetched: Vec = stream::iter(to_fetch) .map(|pc| { - let task_span = info_span!("work_detail_task", %pc); let limiter = limiter.clone(); let client = client.clone(); let id = id.clone(); - async move { - guarded_fetch( - &limiter, - fetch_work_detail(&client, &id, pc).in_current_span(), - ) - .await - } - .instrument(task_span) + async move { guarded_fetch(&limiter, fetch_work_detail(&client, &id, pc)).await } }) .buffer_unordered(cli.concurrency) .try_collect::>() - .instrument(batch_span) .await - .with_context(|| format!("batch fetch for ORCID iD {id}"))?; + .with_context(|| format!("failed to batch-fetch work details of ORCID iD {id}"))?; // Merge let mut merged: Vec = kept @@ -222,13 +240,10 @@ async fn run() -> anyhow::Result<()> { merged.sort_by_key(|d| d.summary.put_code); // Write JSON - info!( - path = cli.out.display().to_string(), - "writing work-details JSON" - ); + info!("writing work details to {}", cli.out.display()); let out_json = OrcidWorkDetailFile { records: merged }; write_pretty_json(&cli.out, &out_json) - .with_context(|| format!("write work-details JSON to {}", cli.out.display()))?; + .with_context(|| format!("failed to write work details to {}", cli.out.display()))?; info!("finished successfully");