終於要開始寫API了嗎,可是我們要用的warp竟然不像Rocket、Actix有專屬的網頁,也不像axum的代碼文件寫的很完整,warp的文件顯得陽春許多,這時候我們還是直接看範例比較快,所以只好到Example資料夾,最好是下載一份一邊對照著跑,想試試的朋友可以clone下來自己的環境跑。
往下繼續寫API之前我們先扎好馬步,點一下前置技能,有印象我們在第7篇跑axum, warp, salvo三個框架用的指令不太一樣嗎,雖然都是執行框架各自提供的helloworld代碼,但因為專案架構不同,所以執行方式也略為不同。往後我們要常常 抄襲 參考別人的範例代碼,所以先來了解一下怎麼看別人的example,下次要驗證別人提供的代碼就可以git clone
下來,然後知道怎麼跑起來。我們現在還是小朋友在學習,等長大了以後,希望大家也可以取之社群,回饋社群,順便幫忙修一下bug,貢獻代碼,或是多參與社群活動 XDDD。
我們上次看過Rust的文檔註釋,可以使用markdown語法來寫文件說明,也可以用代碼區塊來demo案例,順便還可以測試,但只侷限在該fn。有時候我們需要一個比較完整一點的範例,rust有提供寫examples範例的方式,讓我們可以快速POC,或是提供範例給其他人參考。我們看一下Cargo package 的資料夾結構:
.
├── Cargo.toml
├── src/
│ ├── lib.rs
│ ├── main.rs
├── examples/
│ ├── simple.rs
└── tests/
└── some-integration-tests.rs
以上是簡化版的,原來就像看到的這麼簡單(?),我們只要開一個examples的資料夾,把要跑的檔案放裡面就可以,以下是warp放examples裡的hello.rs
// ~/warp/examples/hello.rs
#![deny(warnings)]
use warp::Filter;
#[tokio::main]
async fn main() {
// Match any request and return hello world!
let routes = warp::any().map(|| "Hello, World!");
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
有一個main
是程式進入點這個我們知道,那怎麼讓它跑起來呢:
~/warp$ cargo run --example hello
PS warp> cargo run --example hello
我們之前學過cargo run
的 -p
是選擇 package,--bin
是選擇執行檔,而--example
則是選擇example。先來來試試看,因為我們的專案是用工作空間workspace,而--example
是作用域是在package裡,所以我們在core裡開一個examples的資料夾,放上demo.rs
:
core/examples/demo.rs
fn main() {
println!("Hello, this is example message from core package");
}
再執行:
~/demo-app$ cargo run -p core --example demo
如果只是自己開啟的package專案,沒有使用工作空間workspace的話,就不用加上
-p
的參數
但我們的專案demo-app是開workspace,加example到子項裡好像有點不方便呢,我們可以參照axum的workspace的使用方式,先看axum的Cargo.toml檔案:
[workspace]
members = ["axum", "axum-*", "examples/*"]
# Only check / build main crates by default (check all with `--workspace`)
default-members = ["axum", "axum-*"]
# Example has been deleted, but README.md remains
exclude = ["examples/async-graphql"]
resolver = "2"
從這裡我們可以看到,members的設定可以用萬用字元比對,詳細可以參考Cargo Guide的說明,members是使用glob去比對檔名,所以如果我們同時要包含多個檔名,可以用這種方式去簡化設定。
再來是在workspace裡開exampes資料夾,可以把裡面的專案當一般的專案處理,借鏡一下axum
專案的資料夾開法,可以看到examples裡又開了好多資料夾,每個資料夾都是獨立的一個package,有各自的Cargo.toml
檔案:
看一下Cargo.toml
的內容
注意到這裡有個name
屬性設為example-hello-world
,我們想跑這個範例,就要用package的方式執行:
~/axum$ cargo run -p example-hello-world
之前提過 -p
指的不是資料夾名字,而是我們設定該package(crate)的name
屬性。
好了看完別人的,我們也模仿來自己試一下,首先建立example資料夾,再建立一個demo package:
~/demo-app$ cargo new examples/demo
這時cargo幫我們建好一個專案,打開examples/demo/Cargo.toml
來看看:
# examples/demo/Cargo.toml
[package]
name = "demo"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
還記得我們是在workspace裡嗎,所以專案根目錄的工作空間要設定才能參照的到:
@@ Cargo.toml @@
members = [
...
+ "examples/*",
]
預設名字是demo,我們跑看看:
~/demo-app$ cargo run -p demo
這樣,有沒有對cargo的指令越來越熟悉了呢。
做到這裡,要加入git的時候,又出況狀了。
windows:
debian:
查了一下原來是剛剛我們跑的cargo new
很 雞婆 貼心地幫我們幫我們加了git的 repo,先移除多出來的git庫:
windows:
PS demo-app> rm -Recurse -Force .\examples\demo\.git\
linux or mac:
~/demo-app$ rm -rf ./examples/demo/.git
刪掉後就可以順利的推到git裡,沒有問題。
不過,不會以後都這麼麻煩吧,其實查一下文件就知道cargo new 有參數 --vcs none
可以使用,記得要把字拼對唷:
~/demo-app$ cargo new examples/demo --vcs none
一般專案如果架構比較大的話,通常我們會傾像用比較好的命名慣例,來增加可維護性,所以我們把demo專案改命名加prefix example-
:
@@ examples/demo/Cargo.toml @@
[package]
+name = "example-demo"
-name = "demo"
version = "0.1.0"
之後要執行就使用:
~/demo-app$ cargo run -p example-demo
之前在hello warp裡跑起來的程式只有Running,沒有其他訊息,這樣我們很難知道程式的狀態,通常這時候會把程式執行的東西輸出到日誌檔(Log),以便追蹤(tracing),就依warp的tracing example 抄上來:
// web/src/main.rs
use warp::Filter;
#[tokio::main]
async fn main() {
let filter = std::env::var("RUST_LOG")
.unwrap_or_else(|_| "tracing=info,warp=debug".to_owned());
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_span_events(FmtSpan::CLOSE)
.init();
let routes = warp::any().map(|| "Hello, World!");
warp::serve(routes).run(([127, 0, 0, 1], 3030)).await;
}
記得把run.sh
或run.ps1
裡的web跑起來,然後就開啟了解任(錯)務(誤)的旅程了:
沒有tracing_subscriber
的參考,去crates.io搜尋一下:
加入Cargo.toml:
@@ Cargo.toml @@
rand = { version = "0.8" }
thiserror = { version = "1.0" }
+tracing-subscriber = { version = "0.3" }
tokio = { version = "1.29", features = ["full"] }
warp = { version = "0.3"}
@@ web/Cargo.toml @@
tokio = { workspace = true }
+tracing-subscriber = { workspace = true }
warp = { workspace = true }
注意程式裡是
_
(underscore)而不是-
(hyphen),但在package裡是-
不是_
。
依提示加在引用:
@@ web/src/main.rs @@
+use tracing_subscriber::fmt::format::FmtSpan;
use warp::Filter;
現在訊息變成:
這裡說找不到with_env_filter
這個方法,可是我們明明把crate引入了呀,這時候可以到tracing-subscriber的文檔查找,插尋with_env_filter
如下:
好像有很多個,我們先點第一個看看:
底下有一句話清楚寫著:「Available on crate feature env-filter only.」,很直白了,就是說這個fn是env-filter這個feature(功能)才有,我們再點網頁最上方中間小小的「Feature flags」:
原來分這麼多功能,這就是之前稍微提到的crate可以分別以功能開關來決定要不要引入,到Cargo.toml補上該Feature:
rust在同一個crate還可以設定feature功能註記,來決定是否要編譯該功能,
@@ Cargo.toml @@
+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
-tracing-subscriber = { version = "0.3" }
加完後:
因為這次修改是加在專案root資料夾裡,不在我們watch的清單中,會發生watch console沒有更新的情境,建議可以
Ctrl + C
中斷再重新執行run
。
成功執行了,這次有多一行INFO的資訊,看過LOG檔的話應該不陌生,接下來我們回顧一下這段程式碼做了什麼:
let filter = std::env::var("RUST_LOG")
.unwrap_or_else(|_| "tracing=info,warp=debug".to_owned());
這裡是在說我們要篩選出哪些log進行輸出(至Console或檔案)。unwrap
大家知道是取得Option
的值,那麼unwrap_or_else
就是在說如果取不到值要做後面閉包(Closure)裡面的事囉。好的看一下內容,tracing篩info,而warp篩debug。這裡有一些tracing常見的level,不過文件看了一堆比大小的範例,但好像不是很完整看不出所以然:
我們直接點右邊的source進去看程式碼,之前才說明要怎麼使用rust產生的文件格式,我們立馬就能派上用場了:
程式碼裡其實很清楚的說明每個層級,基本上我們可以依使用情境去輸出log,要知道log檔的爆增是很可怕的,雖然現在的硬碟空間不貴,但也抵擋不了log的資料量。記得把重要資訊輸出就好,輸出的訊息儘量是能幫助我們在排查問題所使用的。
一般在各語言設定logger的層級都是以上,比如設為
warn
就是指warn
(含)及以上層級(以上例為Error)。
好了接下來我們繼續模仿warp的範例輸出log看看,調整我們的hello
路由如下:
// web/src/main.rs
// 把 hello 拆出為單獨的 router fn
let hello = warp::path("hello") // 設定 http rest api 路由
.and(warp::get()) // 在這使用 get
.map(|| { // http 請求進來要處理的 fn
tracing::info!("saying hello..."); // 紀錄到 log 檔
"Hello, World!" // API 回傳值,這裡是字串
})
.with(warp::trace::named("hello"));
let routes = hello
.with(warp::trace::request());
這裡又說沒有tracing的模組,我們看一下:
到Cargo.toml設定補上:
@@ Cargo.toml @@
thiserror = { version = "1.0" }
+tracing = { version = "0.1" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ web/Cargo.toml @@
tokio = { workspace = true }
+tracing = { workspace = true }
tracing-subscriber = { workspace = true }
補上好了是好了,不過要看一下hello裡面的tracing有沒有work,到網址輸入http://localhost:3030/hello 開看看,便可以看到LOG的確有增加
但現在log檔只有輸出到終端介面畫面,我們想要輸出到檔案怎麼辦,直接google「tracing to file rust」,第一個就是tracing-appender,直接裝起來
@@ Cargo.toml @@
tracing = { version = "0.1" }
+tracing-appender = { version = "0.2" }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
@@ web/Cargo.toml @@
tracing = { workspace = true }
+tracing-appender = { workspace = true }
tracing-subscriber = { workspace = true }
看官方範例說的使用方式,好像滿簡單的,不過其中的blocking
和non_blocking
是什麼呢?在多執行緒裡面,我們習慣開non_blocking(非阻塞式?)的thread(執行緒)去跑,避免我們的程式hang(停)住,(白話講blocking就是有人打API進來,我們要等LOG寫完才吐Response回去,但寫log其實是另一件事,不應該影響回傳資料給前端使用者的功能),先複製範例貼上web/src/main.rs
,再修改一下範例的檔名路徑,把hourly改daily:
+let file_appender = tracing_appender::rolling::daily("./logs", "log");
+let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
tracing_subscriber::fmt()
.with_env_filter(filter)
.with_span_events(FmtSpan::CLOSE)
+ .with_writer(non_blocking)
.init();
這裡可以看到原先的tracing_subscriber只加了一行,其實在這裡就是把寫入檔案的
writer
注入,類似DI的概念。
這時候執行結果的終端畫面:
找一下資料夾裡有多出log檔案:
Log檔正確的產生了,可是之前console的訊息怎麼沒了,而且看檔案內容好像怪怪的?多了一堆奇怪的符號,這個ESC其實是為了設定ANSI色碼所使用的,是要在terminal底下標顏色用的,之後我們會有機會玩到。
看一下文件,我們要同時把log輸出至終端畫面及檔案,似乎要用不同的layer去組合,所以我們用registry
改寫subscriber:
use tracing_subscriber::prelude::*; // 加入引用
// ...略
#[tokio::main]
async fn main() {
// ...略
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer()) // 輸出至終端
.with(tracing_subscriber::fmt::layer()
.with_writer(non_blocking)) // 輸出至檔案
.init();
另外找到了tracing裡的ansi設定,試著把輸出到log檔的ansi關掉:
tracing_subscriber::registry()
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::fmt::layer()
.with_ansi(false) // 加上這行,不輸出ANSI色碼
.with_writer(non_blocking))
.init();
試打一下API,跳一堆訊息出來,也太多,有很多好像跟我們沒關係。
原來是剛剛在改成layer設定的時候把filter拿掉了,所以連帶出現一堆hyper的log訊息,hyper是rust裡處理http協定的套件,axum等底層的http連線也都是用hyper,不過以我們開發而言似乎不需要看到這麼低階的訊息,那要怎麼過濾出我們要的訊息再紀錄呢,還好這時副駕即時救援,給了我們這個fn:
// web/src/main.rs
use tracing_subscriber::EnvFilter;
pub fn tracing_filter() -> EnvFilter {
EnvFilter::from_default_env()
.add_directive("warp=debug".parse().unwrap())
.add_directive("web=debug".parse().unwrap())
.add_directive("tracing=debug".parse().unwrap())
}
直接引用看看:
tracing_subscriber::registry()
+ .with(tracing_filter())
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::fmt::layer()
看起來螢幕輸出和檔案輸出都OK了,只是檔案裡warp產的ansi消不掉,但hyper產出來的log沒有ansi,看起來可能是warp的問題,目前找了半天沒找到解法,影響也不大的情況下,先跳過這部分。但是現在main被tracing佔了很大版面,我們把日誌相關的部分抽到logger.rs
檔,讓我們的main比較乾淨:
// web/src/main.rs
mod logger; // 抽出去的檔案
#[tokio::main]
async fn main() {
let file_appender = tracing_appender::rolling::daily("./logs", "log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
logger::register_tracing(non_blocking);
// routers
}
// web/src/logger.rs
use tracing_appender::non_blocking::NonBlocking;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};
pub fn tracing_filter() -> EnvFilter {
EnvFilter::from_default_env()
.add_directive("warp=debug".parse().unwrap())
.add_directive("web=debug".parse().unwrap())
.add_directive("tracing=debug".parse().unwrap())
}
pub fn register_tracing(non_blocking: NonBlocking) {
tracing_subscriber::registry()
.with(tracing_filter())
.with(tracing_subscriber::fmt::layer())
.with(tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(non_blocking))
.init()
}
剛剛的日誌檔雖然幾經波折之下設定好了,不過好像哪裡怪怪的?我們都把log等級的debug
寫死(hard code)在程式碼裡,萬一要發佈到線上環境,到時候需要調整成info
或以上的等級的時候怎麼辦?嗯,要把這裡的debug
改成用設定的方式,查一下rust有沒有環境變數的設定,google一下:
點進去看看:
維護竟然是4年前,感覺用起來心不太安(?),我們進去github看看,看看有沒有人提出問題來:
第二個Issue看起來很醒目,點進去看看:
這裡有熱心的朋友回答,原來可以用dotenvy
,查看看:
感覺比較有在維護,下載量也滿多的,就用這個來設環境吧,看一下文件怎麼說:
呃,好啦我知道你是因為dotenv
作者沒繼續維護而fork分出來的,可是我也沒用過dotenv啊,這樣叫我怎麼開始 複製貼上 開發呀。可是回去看dotenv的文件可能也不是很正確,好吧我們去看看dotenvy有沒有提供我們剛剛一開始提到的examples:
有沒有發現看example的重要性了 XDDD
雖然很簡單,但還是可以看得出來基本上的用法,一般正式環境會直接讀取作業系統的環境變數,而開發時才會讀.env
檔,我們一樣先加套件:
@@ Cargo.toml @@
[workspace.dependencies]
+dotenvy = { version = "0.15" }
rand = { version = "0.8" }
@@ web/Cargo.toml @@
+dotenvy = { workspace = true }
tokio = { workspace = true }
新增config模組(mod):
// web/src/main.rs
mod config;
// web/src/config.rs
use std::env;
pub const ENV_LOG_LEVEL: &str = "LOG_LEVEL";
pub const ENV_APP_ENV: &str = "APP_ENV";
pub fn init() {
let env = match env::var(ENV_APP_ENV) { // 取得環境變數的環境
Ok(v) => v,
_ => "dev".to_string(),
};
if env.starts_with("prod") { return; } // 正式區則離開不讀.env檔
match dotenvy::dotenv() { // 讀取env檔案
Ok(_) => println!(".env read successfully "),
Err(e) => println!("Could not load .env file: {e}"),
};
}
const
是常數,慣例是大寫加底線_
,遇到可能會常用的字串請設為常數,不要變成magic number(?)。在main裡加上init初始化:
#[tokio::main]
async fn main() {
+ config::init();
let file_appender = tracing_appender::rolling::daily("./logs", "log");
加.env
檔及example.env
檔
# .env
APP_ENV=dev
LOG_LEVEL=debug
# example.env
APP_ENV=dev
LOG_LEVEL=debug
記得要把.env
先加到.gitignore
裡,避免不小心把連線字串推上git,就哭哭了。通常因為我們.env
可能會含敏感資訊不應上版控,所以會再附上example.env
,提供設定的範例(記得未來加連線字串的時候在example要放假資料,別貼錯)。
@@ .gitignore @@
+.env
環境變數設好了,那麼來改我們的logger吧:
use crate::config::ENV_LOG_LEVEL;
pub fn tracing_filter() -> EnvFilter {
let level = std::env::var(ENV_LOG_LEVEL).unwrap_or_else(|_| "info".to_string());
let dir1 = format!("warp={}", level);
let dir2 = format!("web={}", level);
let dir3 = format!("tracing={}", level);
EnvFilter::from_default_env()
.add_directive(dir1.parse().unwrap())
.add_directive(dir2.parse().unwrap())
.add_directive(dir3.parse().unwrap())
}
format!
也是巨集,跟之前用的println!
很像,差別在於format!
是把字串組合的結果回傳,而非打印在命令列。試一下改變.env檔案的設定再重啟,不同的層級會顯示不同的訊息:
Info層級訊息:
Trace層級訊息:
好了,沒想到準備工作就這麼漫長,原本想要寫REST API的部分只能留待下一篇了。
本系列原始碼:https://github.com/kenstt/demo-app