From 3e553eaf605d405abc191b4b95cc69a903d6a757 Mon Sep 17 00:00:00 2001 From: Luca Palmieri Date: Sun, 27 Sep 2020 17:09:42 +0100 Subject: [PATCH] Chapter04 (#9) * Add chapter 4 code. * Add logger middleware. * Add env_logger. * Instrumented. * Test logs. * Introduce instrument. * Refactor handler. * Use TracingLogger. * Update. * Fix linter error. --- Cargo.lock | 223 +++++++++++++++++- Cargo.toml | 2 +- chapter04/.env | 1 + chapter04/Cargo.toml | 36 +++ chapter04/configuration.yaml | 7 + ...00823135036_create_subscriptions_table.sql | 8 + chapter04/scripts/init_db.sh | 40 ++++ chapter04/src/configuration.rs | 38 +++ chapter04/src/lib.rs | 5 + chapter04/src/main.rs | 29 +++ chapter04/src/routes/health_check.rs | 5 + chapter04/src/routes/mod.rs | 5 + chapter04/src/routes/subscriptions.rs | 56 +++++ chapter04/src/startup.rs | 21 ++ chapter04/src/telemetry.rs | 29 +++ chapter04/tests/health_check.rs | 143 +++++++++++ 16 files changed, 644 insertions(+), 4 deletions(-) create mode 100644 chapter04/.env create mode 100644 chapter04/Cargo.toml create mode 100644 chapter04/configuration.yaml create mode 100644 chapter04/migrations/20200823135036_create_subscriptions_table.sql create mode 100755 chapter04/scripts/init_db.sh create mode 100644 chapter04/src/configuration.rs create mode 100644 chapter04/src/lib.rs create mode 100644 chapter04/src/main.rs create mode 100644 chapter04/src/routes/health_check.rs create mode 100644 chapter04/src/routes/mod.rs create mode 100644 chapter04/src/routes/subscriptions.rs create mode 100644 chapter04/src/startup.rs create mode 100644 chapter04/src/telemetry.rs create mode 100644 chapter04/tests/health_check.rs diff --git a/Cargo.lock b/Cargo.lock index 822ff87..530a279 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -271,6 +271,14 @@ dependencies = [ "memchr 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "anyhow" version = "1.0.32" @@ -304,6 +312,16 @@ dependencies = [ "num-traits 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "hermit-abi 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "libc 0.2.76 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -449,6 +467,31 @@ dependencies = [ "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "chapter04" +version = "0.1.0" +dependencies = [ + "actix-rt 1.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", + "config 0.10.1 (registry+https://github.com/rust-lang/crates.io-index)", + "env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "reqwest 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "sqlx 0.4.0-beta.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tokio 0.2.22 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-actix-web 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-bunyan-formatter 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-futures 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "chrono" version = "0.4.15" @@ -608,6 +651,18 @@ dependencies = [ "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "env_logger" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)", + "humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "failure" version = "0.1.8" @@ -774,6 +829,15 @@ dependencies = [ "version_check 0.9.2 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "gethostname" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "libc 0.2.76 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -880,6 +944,14 @@ name = "httparse" version = "1.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "humantime" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "quick-error 1.2.3 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "hyper" version = "0.13.7" @@ -1051,6 +1123,14 @@ name = "match_cfg" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "matches" version = "0.1.8" @@ -1396,6 +1476,15 @@ dependencies = [ "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "regex-automata" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "byteorder 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)", + "regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "regex-syntax" version = "0.6.18" @@ -1566,6 +1655,14 @@ dependencies = [ "opaque-debug 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "sharded-slab" +version = "0.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "signal-hook-registry" version = "1.2.1" @@ -1741,6 +1838,14 @@ dependencies = [ "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "termcolor" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "thiserror" version = "1.0.20" @@ -1877,17 +1982,104 @@ source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)", "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", - "tracing-core 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-attributes 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-actix-web" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "actix-web 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "futures 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-futures 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", + "uuid 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "proc-macro2 1.0.19 (registry+https://github.com/rust-lang/crates.io-index)", + "quote 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-bunyan-formatter" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "chrono 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", + "gethostname 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-subscriber 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "tracing-core" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", ] +[[package]] +name = "tracing-futures" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "pin-project 0.4.23 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-log" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "log 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-serde" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)", + "chrono 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "matchers 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.115 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.57 (registry+https://github.com/rust-lang/crates.io-index)", + "sharded-slab 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)", + "smallvec 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)", + "thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", + "tracing-serde 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "trust-dns-proto" version = "0.18.0-alpha.2" @@ -2116,6 +2308,14 @@ name = "winapi-i686-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -2173,11 +2373,13 @@ dependencies = [ "checksum adler 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" "checksum ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" "checksum aho-corasick 0.7.13 (registry+https://github.com/rust-lang/crates.io-index)" = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +"checksum ansi_term 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" "checksum anyhow 1.0.32 (registry+https://github.com/rust-lang/crates.io-index)" = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" "checksum arc-swap 0.4.7 (registry+https://github.com/rust-lang/crates.io-index)" = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" "checksum arrayvec 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" "checksum async-trait 0.1.38 (registry+https://github.com/rust-lang/crates.io-index)" = "6e1a4a2f97ce50c9d0282c1468816208588441492b40d813b2e0419c22c05e7f" "checksum atoi 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "e0afb7287b68575f5ca0e5c7e40191cbd4be59d325781f46faa603e176eaef47" +"checksum atty 0.2.14 (registry+https://github.com/rust-lang/crates.io-index)" = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" "checksum autocfg 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" "checksum awc 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d7601d4d1d7ef2335d6597a41b5fe069f6ab799b85f53565ab390e7b7065aac5" "checksum backtrace 0.3.50 (registry+https://github.com/rust-lang/crates.io-index)" = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" @@ -2214,6 +2416,7 @@ dependencies = [ "checksum either 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" "checksum encoding_rs 0.8.23 (registry+https://github.com/rust-lang/crates.io-index)" = "e8ac63f94732332f44fe654443c46f6375d1939684c17b0afb6cb56b0456e171" "checksum enum-as-inner 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7c5f0096a91d210159eceb2ff5e1c4da18388a170e1e3ce948aac9c8fdbbf595" +"checksum env_logger 0.7.1 (registry+https://github.com/rust-lang/crates.io-index)" = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" "checksum failure 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" "checksum failure_derive 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" "checksum flate2 1.0.17 (registry+https://github.com/rust-lang/crates.io-index)" = "766d0e77a2c1502169d4a93ff3b8c15a71fd946cd0126309752104e5f3c46d94" @@ -2233,6 +2436,7 @@ dependencies = [ "checksum futures-util 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" "checksum fxhash 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" "checksum generic-array 0.14.4 (registry+https://github.com/rust-lang/crates.io-index)" = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +"checksum gethostname 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "e692e296bfac1d2533ef168d0b60ff5897b8b70a4009276834014dd8924cc028" "checksum getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)" = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" "checksum gimli 0.22.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" "checksum h2 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" @@ -2245,6 +2449,7 @@ dependencies = [ "checksum http 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" "checksum http-body 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" "checksum httparse 1.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +"checksum humantime 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" "checksum hyper 0.13.7 (registry+https://github.com/rust-lang/crates.io-index)" = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" "checksum hyper-tls 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" "checksum idna 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "02e2673c30ee86b5b96a9cb52ad15718aa1f966f5ab9ad54a8b95d5ca33120a9" @@ -2266,6 +2471,7 @@ dependencies = [ "checksum lru-cache 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" "checksum maplit 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" "checksum match_cfg 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +"checksum matchers 0.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" "checksum matches 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" "checksum maybe-uninit 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" "checksum md-5 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b5a279bb9607f9f53c22d496eade00d138d1bdcccd07d74650387cf94942a15" @@ -2308,6 +2514,7 @@ dependencies = [ "checksum rand_hc 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" "checksum redox_syscall 0.1.57 (registry+https://github.com/rust-lang/crates.io-index)" = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" "checksum regex 1.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +"checksum regex-automata 0.1.9 (registry+https://github.com/rust-lang/crates.io-index)" = "ae1ded71d66a4a97f5e961fd0cb25a5f366a42a41570d16a763a69c092c26ae4" "checksum regex-syntax 0.6.18 (registry+https://github.com/rust-lang/crates.io-index)" = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" "checksum remove_dir_all 0.5.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" "checksum reqwest 0.10.7 (registry+https://github.com/rust-lang/crates.io-index)" = "12427a5577082c24419c9c417db35cfeb65962efc7675bb6b0d5f1f9d315bfe6" @@ -2325,6 +2532,7 @@ dependencies = [ "checksum sha-1 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "170a36ea86c864a3f16dd2687712dd6646f7019f301e57537c7f4dc9f5916770" "checksum sha1 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" "checksum sha2 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2933378ddfeda7ea26f48c555bdad8bb446bf8a3d17832dc83e380d444cfb8c1" +"checksum sharded-slab 0.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "06d5a3f5166fb5b42a5439f2eee8b9de149e235961e3eb21c5808fc3ea17ff3e" "checksum signal-hook-registry 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a3e12110bc539e657a646068aaf5eb5b63af9d0c1f7b29c97113fad80e15f035" "checksum slab 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" "checksum smallvec 1.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fbee7696b84bbf3d89a1c2eccff0850e3047ed46bfcd2e92c29a2d074d57e252" @@ -2340,6 +2548,7 @@ dependencies = [ "checksum syn 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)" = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" "checksum synstructure 0.12.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" "checksum tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +"checksum termcolor 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb6bfa289a4d7c5766392812c0a1f4c1ba45afa1ad47803c11e1f407d846d75f" "checksum thiserror 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)" = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" "checksum thiserror-impl 1.0.20 (registry+https://github.com/rust-lang/crates.io-index)" = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" "checksum thread_local 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" @@ -2354,7 +2563,14 @@ dependencies = [ "checksum tokio-util 0.3.1 (registry+https://github.com/rust-lang/crates.io-index)" = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" "checksum tower-service 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" "checksum tracing 0.1.19 (registry+https://github.com/rust-lang/crates.io-index)" = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" -"checksum tracing-core 0.1.15 (registry+https://github.com/rust-lang/crates.io-index)" = "4f0e00789804e99b20f12bc7003ca416309d28a6f495d6af58d1e2c2842461b5" +"checksum tracing-actix-web 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d9e2e3bdcd192ec7786b47addd10f66fbc7c06d32f1757ddaa31e28eec045aea" +"checksum tracing-attributes 0.1.11 (registry+https://github.com/rust-lang/crates.io-index)" = "80e0ccfc3378da0cce270c946b676a376943f5cd16aeba64568e7939806f4ada" +"checksum tracing-bunyan-formatter 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8d8dfd28e9ee6d79937f139ae4f112fa2172018cfdf111c0dac1f3f8c6912053" +"checksum tracing-core 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "5bcf46c1f1f06aeea2d6b81f3c863d0930a596c86ad1920d4e5bad6dd1d7119a" +"checksum tracing-futures 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "ab7bb6f14721aa00656086e9335d363c5c8747bae02ebe32ea2c7dece5689b4c" +"checksum tracing-log 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "5e0f8c7178e13481ff6765bd169b33e8d554c5d2bbede5e32c356194be02b9b9" +"checksum tracing-serde 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "fb65ea441fbb84f9f6748fd496cf7f63ec9af5bca94dd86456978d055e8eb28b" +"checksum tracing-subscriber 0.2.12 (registry+https://github.com/rust-lang/crates.io-index)" = "82bb5079aa76438620837198db8a5c529fb9878c730bc2b28179b0241cf04c10" "checksum trust-dns-proto 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "2a7f3a2ab8a919f5eca52a468866a67ed7d3efa265d48a652a9a3452272b413f" "checksum trust-dns-resolver 0.18.0-alpha.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6f90b1502b226f8b2514c6d5b37bafa8c200d7ca4102d57dc36ee0f3b7a04a2f" "checksum try-lock 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)" = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" @@ -2383,6 +2599,7 @@ dependencies = [ "checksum winapi 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +"checksum winapi-util 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" "checksum winreg 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b2986deb581c4fe11b621998a5e53361efe6b48a151178d0cd9eeffa4dc6acc9" "checksum winreg 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" diff --git a/Cargo.toml b/Cargo.toml index 912af18..5b8f01e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,2 +1,2 @@ [workspace] -members = ["chapter03-0", "chapter03-1"] +members = ["chapter03-0", "chapter03-1", "chapter04"] diff --git a/chapter04/.env b/chapter04/.env new file mode 100644 index 0000000..88cfb53 --- /dev/null +++ b/chapter04/.env @@ -0,0 +1 @@ +DATABASE_URL="postgres://postgres:password@localhost:5432/newsletter" diff --git a/chapter04/Cargo.toml b/chapter04/Cargo.toml new file mode 100644 index 0000000..fbde63f --- /dev/null +++ b/chapter04/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "chapter04" +version = "0.1.0" +authors = ["LukeMathWalker "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +path = "src/lib.rs" + +[[bin]] +path = "src/main.rs" +name = "chapter04" + +[dependencies] +actix-web = "2.0.0" +actix-rt = "1.1.1" +tokio = "0.2.22" +serde = "1.0.115" +config = { version = "0.10.1", default-features = false, features = ["yaml"] } +sqlx = { version = "0.4.0-beta.1", default-features = false, features = [ "runtime-tokio", "macros", "postgres", "uuid", "chrono", "migrate"] } +anyhow = "1.0.32" +uuid = { version = "0.8.1", features = ["v4"] } +chrono = "0.4.15" +env_logger = "0.7.1" +log = "0.4.11" +tracing = "0.1.19" +tracing-futures = "0.2.4" +tracing-subscriber = { version = "0.2.12", features = ["registry", "env-filter"] } +tracing-bunyan-formatter = "0.1.6" +tracing-log = "0.1.1" +tracing-actix-web = "0.1.1" + +[dev-dependencies] +reqwest = { version = "0.10.7", features = ["json"] } +lazy_static = "1.4.0" diff --git a/chapter04/configuration.yaml b/chapter04/configuration.yaml new file mode 100644 index 0000000..04dc5ef --- /dev/null +++ b/chapter04/configuration.yaml @@ -0,0 +1,7 @@ +application_port: 8000 +database: + host: "localhost" + port: 5432 + username: "postgres" + password: "password" + database_name: "newsletter" \ No newline at end of file diff --git a/chapter04/migrations/20200823135036_create_subscriptions_table.sql b/chapter04/migrations/20200823135036_create_subscriptions_table.sql new file mode 100644 index 0000000..2c0d262 --- /dev/null +++ b/chapter04/migrations/20200823135036_create_subscriptions_table.sql @@ -0,0 +1,8 @@ +-- Create Subscriptions Table +CREATE TABLE subscriptions( + id uuid NOT NULL, + PRIMARY KEY (id), + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + subscribed_at timestamptz NOT NULL +); \ No newline at end of file diff --git a/chapter04/scripts/init_db.sh b/chapter04/scripts/init_db.sh new file mode 100755 index 0000000..cc1df04 --- /dev/null +++ b/chapter04/scripts/init_db.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +set -x +set -eo pipefail + +# Check if a custom user has been set, otherwise default to 'postgres' +DB_USER=${POSTGRES_USER:=postgres} +# Check if a custom password has been set, otherwise default to 'password' +DB_PASSWORD="${POSTGRES_PASSWORD:=password}" +# Check if a custom password has been set, otherwise default to 'newsletter' +DB_NAME="${POSTGRES_DB:=newsletter}" +# Check if a custom port has been set, otherwise default to '5432' +DB_PORT="${POSTGRES_PORT:=5432}" + +# Allow to skip Docker if a dockerized Postgres database is already running +if [[ -z "${SKIP_DOCKER}" ]] +then + # Launch postgres using Docker + docker run \ + -e POSTGRES_USER=${DB_USER} \ + -e POSTGRES_PASSWORD=${DB_PASSWORD} \ + -e POSTGRES_DB=${DB_NAME} \ + -p "${DB_PORT}":5432 \ + -d postgres \ + postgres -N 1000 + # ^ Increased maximum number of connections for testing purposes +fi + +# Keep pinging Postgres until it's ready to accept commands +until PGPASSWORD="${DB_PASSWORD}" psql -h "localhost" -U "${DB_USER}" -p "${DB_PORT}" -d "postgres" -c '\q'; do + >&2 echo "Postgres is still unavailable - sleeping" + sleep 1 +done + +>&2 echo "Postgres is up and running on port ${DB_PORT} - running migrations now!" + +export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/${DB_NAME} +sqlx database create +sqlx migrate run + +>&2 echo "Postgres has been migrated, ready to go!" diff --git a/chapter04/src/configuration.rs b/chapter04/src/configuration.rs new file mode 100644 index 0000000..3ad6da7 --- /dev/null +++ b/chapter04/src/configuration.rs @@ -0,0 +1,38 @@ +#[derive(serde::Deserialize)] +pub struct Settings { + pub database: DatabaseSettings, + pub application_port: u16, +} + +#[derive(serde::Deserialize)] +pub struct DatabaseSettings { + pub username: String, + pub password: String, + pub port: u16, + pub host: String, + pub database_name: String, +} + +impl DatabaseSettings { + pub fn connection_string(&self) -> String { + format!( + "postgres://{}:{}@{}:{}/{}", + self.username, self.password, self.host, self.port, self.database_name + ) + } + + pub fn connection_string_without_db(&self) -> String { + format!( + "postgres://{}:{}@{}:{}", + self.username, self.password, self.host, self.port + ) + } +} + +pub fn get_configuration() -> Result { + let mut settings = config::Config::default(); + + settings.merge(config::File::with_name("configuration"))?; + + settings.try_into() +} diff --git a/chapter04/src/lib.rs b/chapter04/src/lib.rs new file mode 100644 index 0000000..5d8e21e --- /dev/null +++ b/chapter04/src/lib.rs @@ -0,0 +1,5 @@ +#![allow(clippy::toplevel_ref_arg)] +pub mod configuration; +pub mod routes; +pub mod startup; +pub mod telemetry; diff --git a/chapter04/src/main.rs b/chapter04/src/main.rs new file mode 100644 index 0000000..a7a516b --- /dev/null +++ b/chapter04/src/main.rs @@ -0,0 +1,29 @@ +use anyhow::Context; +use chapter04::configuration::get_configuration; +use chapter04::startup::run; +use chapter04::telemetry::{get_subscriber, init_subscriber}; +use sqlx::postgres::PgPool; +use std::net::TcpListener; + +#[actix_rt::main] +async fn main() -> Result<(), anyhow::Error> { + let subscriber = get_subscriber("zero2prod".into(), "info".into()); + init_subscriber(subscriber); + + let configuration = get_configuration().expect("Failed to read configuration."); + let connection_pool = PgPool::connect(&configuration.database.connection_string()) + .await + .map_err(anyhow::Error::from) + .with_context(|| "Failed to connect to Postgres.")?; + + // Here we choose to bind explicitly to localhost, 127.0.0.1, for security + // reasons. This binding may cause issues in some environments. For example, + // it causes connectivity issues running in WSL2, where you cannot reach the + // server when it is bound to WSL2's localhost interface. As a workaround, + // you can choose to bind to all interfaces, 0.0.0.0, instead, but be aware + // of the security implications when you expose the server on all interfaces. + let address = format!("127.0.0.1:{}", configuration.application_port); + let listener = TcpListener::bind(address)?; + run(listener, connection_pool)?.await?; + Ok(()) +} diff --git a/chapter04/src/routes/health_check.rs b/chapter04/src/routes/health_check.rs new file mode 100644 index 0000000..d7eb4e0 --- /dev/null +++ b/chapter04/src/routes/health_check.rs @@ -0,0 +1,5 @@ +use actix_web::HttpResponse; + +pub async fn health_check() -> HttpResponse { + HttpResponse::Ok().finish() +} diff --git a/chapter04/src/routes/mod.rs b/chapter04/src/routes/mod.rs new file mode 100644 index 0000000..90ffeed --- /dev/null +++ b/chapter04/src/routes/mod.rs @@ -0,0 +1,5 @@ +mod health_check; +mod subscriptions; + +pub use health_check::*; +pub use subscriptions::*; diff --git a/chapter04/src/routes/subscriptions.rs b/chapter04/src/routes/subscriptions.rs new file mode 100644 index 0000000..e5a86be --- /dev/null +++ b/chapter04/src/routes/subscriptions.rs @@ -0,0 +1,56 @@ +use actix_web::{web, HttpResponse}; +use chrono::Utc; +use sqlx::PgPool; +use uuid::Uuid; + +#[derive(serde::Deserialize)] +pub struct SubscribeRequest { + email: String, + name: String, +} + +#[tracing::instrument( + name = "Adding a new subscriber", + skip(payload, pool), + fields( + request_id=%Uuid::new_v4(), + email = %payload.email, + name = %payload.name + ) +)] +pub async fn subscribe( + payload: web::Form, + pool: web::Data, +) -> Result { + insert_subscriber(&pool, &payload) + .await + .map_err(|_| HttpResponse::InternalServerError().finish())?; + Ok(HttpResponse::Ok().finish()) +} + +#[tracing::instrument( + name = "Saving new subscriber details in the database", + skip(payload, pool) +)] +pub async fn insert_subscriber( + pool: &PgPool, + payload: &SubscribeRequest, +) -> Result<(), sqlx::Error> { + sqlx::query!( + r#" + INSERT INTO subscriptions (id, email, name, subscribed_at) + VALUES ($1, $2, $3, $4) + "#, + Uuid::new_v4(), + payload.email, + payload.name, + Utc::now() + ) + .execute(pool) + .await + .map_err(|e| { + tracing::error!("Failed to execute query: {:?}", e); + e + })?; + Ok(()) +} diff --git a/chapter04/src/startup.rs b/chapter04/src/startup.rs new file mode 100644 index 0000000..d091772 --- /dev/null +++ b/chapter04/src/startup.rs @@ -0,0 +1,21 @@ +use crate::routes::{health_check, subscribe}; +use actix_web::dev::Server; +use actix_web::web::Data; +use actix_web::{web, App, HttpServer}; +use sqlx::PgPool; +use std::net::TcpListener; +use tracing_actix_web::TracingLogger; + +pub fn run(listener: TcpListener, db_pool: PgPool) -> Result { + let db_pool = Data::new(db_pool); + let server = HttpServer::new(move || { + App::new() + .wrap(TracingLogger) + .route("/health_check", web::get().to(health_check)) + .route("/subscriptions", web::post().to(subscribe)) + .app_data(db_pool.clone()) + }) + .listen(listener)? + .run(); + Ok(server) +} diff --git a/chapter04/src/telemetry.rs b/chapter04/src/telemetry.rs new file mode 100644 index 0000000..27168fb --- /dev/null +++ b/chapter04/src/telemetry.rs @@ -0,0 +1,29 @@ +use tracing::subscriber::set_global_default; +use tracing::Subscriber; +use tracing_bunyan_formatter::{BunyanFormattingLayer, JsonStorageLayer}; +use tracing_log::LogTracer; +use tracing_subscriber::{layer::SubscriberExt, EnvFilter, Registry}; + +/// Compose multiple layers into a `tracing`'s subscriber. +/// +/// # Implementation Notes +/// +/// We are using `impl Subscriber` as return type to avoid having to spell out the actual +/// type of the returned subscriber, which is indeed quite complex. +pub fn get_subscriber(name: String, env_filter: String) -> impl Subscriber + Sync + Send { + let env_filter = + EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(env_filter)); + let formatting_layer = BunyanFormattingLayer::new(name, std::io::stdout); + Registry::default() + .with(env_filter) + .with(JsonStorageLayer) + .with(formatting_layer) +} + +/// Register a subscriber as global default to process span data. +/// +/// It should only be called once! +pub fn init_subscriber(subscriber: impl Subscriber + Sync + Send) { + LogTracer::init().expect("Failed to set logger"); + set_global_default(subscriber).expect("Failed to set subscriber"); +} diff --git a/chapter04/tests/health_check.rs b/chapter04/tests/health_check.rs new file mode 100644 index 0000000..c0ac230 --- /dev/null +++ b/chapter04/tests/health_check.rs @@ -0,0 +1,143 @@ +use chapter04::configuration::{get_configuration, DatabaseSettings}; +use chapter04::startup::run; +use chapter04::telemetry::{get_subscriber, init_subscriber}; +use sqlx::{Connection, Executor, PgConnection, PgPool}; +use std::net::TcpListener; +use uuid::Uuid; + +// Ensure that the `tracing` stack is only initialised once using `lazy_static` +lazy_static::lazy_static! { + static ref TRACING: () = { + let filter = if std::env::var("TEST_LOG").is_ok() { "debug" } else { "" }; + let subscriber = get_subscriber("test".into(), filter.into()); + init_subscriber(subscriber); + }; +} + +pub struct TestApp { + pub address: String, + pub db_pool: PgPool, +} + +async fn spawn_app() -> TestApp { + // The first time `initialize` is invoked the code in `TRACING` is executed. + // All other invocations will instead skip execution. + lazy_static::initialize(&TRACING); + + let listener = TcpListener::bind("127.0.0.1:0").expect("Failed to bind random port"); + // We retrieve the port assigned to us by the OS + let port = listener.local_addr().unwrap().port(); + let address = format!("http://127.0.0.1:{}", port); + + let mut configuration = get_configuration().expect("Failed to read configuration."); + configuration.database.database_name = Uuid::new_v4().to_string(); + let connection_pool = configure_database(&configuration.database).await; + + let server = run(listener, connection_pool.clone()).expect("Failed to bind address"); + let _ = tokio::spawn(server); + TestApp { + address, + db_pool: connection_pool, + } +} + +pub async fn configure_database(config: &DatabaseSettings) -> PgPool { + // Create database + let mut connection = PgConnection::connect(&config.connection_string_without_db()) + .await + .expect("Failed to connect to Postgres"); + connection + .execute(&*format!(r#"CREATE DATABASE "{}";"#, config.database_name)) + .await + .expect("Failed to create database."); + + // Migrate database + let connection_pool = PgPool::connect(&config.connection_string()) + .await + .expect("Failed to connect to Postgres."); + sqlx::migrate!("./migrations") + .run(&connection_pool) + .await + .expect("Failed to migrate the database"); + + connection_pool +} + +#[actix_rt::test] +async fn health_check_works() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + + // Act + let response = client + // Use the returned application address + .get(&format!("{}/health_check", &app.address)) + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert!(response.status().is_success()); + assert_eq!(Some(0), response.content_length()); +} + +#[actix_rt::test] +async fn subscribe_returns_a_200_for_valid_form_data() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + let body = "name=le%20guin&email=ursula_le_guin%40gmail.com"; + + // Act + let response = client + .post(&format!("{}/subscriptions", &app.address)) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(body) + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert_eq!(200, response.status().as_u16()); + + let saved = sqlx::query!("SELECT email, name FROM subscriptions",) + .fetch_one(&app.db_pool) + .await + .expect("Failed to fetch saved subscription."); + + assert_eq!(saved.email, "ursula_le_guin@gmail.com"); + assert_eq!(saved.name, "le guin"); +} + +#[actix_rt::test] +async fn subscribe_returns_a_400_when_data_is_missing() { + // Arrange + let app = spawn_app().await; + let client = reqwest::Client::new(); + let test_cases = vec![ + ("name=le%20guin", "missing the email"), + ("email=ursula_le_guin%40gmail.com", "missing the name"), + ("", "missing both name and email"), + ]; + + for (invalid_body, error_message) in test_cases { + // Act + let response = client + .post(&format!("{}/subscriptions", &app.address)) + .header("Content-Type", "application/x-www-form-urlencoded") + .body(invalid_body) + .send() + .await + .expect("Failed to execute request."); + + // Assert + assert_eq!( + 400, + response.status().as_u16(), + // Additional customised error message on test failure + "The API did not fail with 400 Bad Request when the payload was {}.", + error_message + ); + } +}