Az előző leckében megtanultad, hogyan jutnak adatok a handlerekbe extractorokkal és hogyan osztható meg állapot State-tel és Arc-cal. Egy valódi webszervernél azonban nem csak az egyes route-ok logikája számít: szükség van olyan közös rétegekre is, amelyek minden kérésen átfutnak – ez a middleware.
A middleware mint kérés/válasz lánc
Axum a tower ökoszisztéma middleware-fogalmára épül, még ha most nem is adunk hozzá közvetlenül tower függőséget – az Axum saját, egyszerűsített API-t ad ehhez: az axum::middleware::from_fn függvényt. A middleware lényege, hogy egy kérés megérkezésekor köréfut a handlernek: futhat kód a handler meghívása előtt, majd a handler választ ad vissza, és futhat kód azután is.
use axum::{
extract::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router,
};
async fn logging_middleware(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
// itt fut a handler (és minden "beljebb" lévő middleware)
let response = next.run(request).await;
println!("{method} {uri} -> {}", response.status());
response
}
async fn hello() -> &'static str {
"Szia, világ!"
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(hello))
.layer(middleware::from_fn(logging_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
A .layer() hívás csomagolja be a Router-t a middleware-rel. A next: Next paraméter az a "következő lépés" a láncban – lehet egy másik middleware, vagy végül a handler maga. A next.run(request).await hívás futtatja le a további láncot, és visszaadja a Response-t, amit a middleware még módosíthat, mielőtt visszaküldi.
Ha egy middleware elfelejti meghívni a next.run(...)-t, a kérés soha nem éri el a handlert – a kliens válasz nélkül, vagy egy korábban visszaadott válasszal marad. Ez gyakori kezdő hiba.
Middleware-típusok és felhasználási esetek
A middleware-ek tipikus felhasználási területei:
- Naplózás: minden kérésről és válaszról rögzítünk információt (metódus, útvonal, státuszkód, futásidő).
- Timeout: ha egy kérés túl sokáig futna, a middleware megszakíthatja és hibaválaszt adhat.
- Fejlécek hozzáadása: például CORS-fejlécek vagy biztonsági fejlécek minden válaszhoz.
- Kérés-korlátozás (rate limiting): túl sok kérés esetén elutasítás.
Ezek közül most a naplózásra fókuszálunk részletesen, mert ez az, amivel a legtöbb szolgáltatásban azonnal találkozol. A timeout és a fejléc-middleware-ek működési elve hasonló: mindegyik a bemutatott from_fn mintát, vagy egy erre épülő réteget használ.
Előretekintés: nagyobb projektekben gyakran a tower-http crate előre elkészített middleware-eit (pl. timeout, tömörítés) is bevetik – ezt egy későbbi leckében érintjük majd, most a saját middleware írására fókuszálunk.
A tracing crate: strukturált naplózás async kódban
A println! jó a kísérletezéshez, de éles rendszerben nem elég: nem tudsz szűrni súlyosság szerint, nem látod, melyik tokio::spawn-nal indított feladatból jött egy üzenet, és nincs strukturált formátum. Erre való a tracing crate.
A tracing event-eket (egyszeri naplóbejegyzéseket) és span-eket (egy időbeli szakaszt, például egy kérés teljes feldolgozását) tud kezelni. Az eventeknek szintjük van: trace, debug, info, warn, error.
use tracing::info;
async fn hello() -> &'static str {
info!("kiszolgáltuk a / útvonalat");
"Szia, világ!"
}
Ez önmagában semmit nem ír ki, amíg nincs beállítva egy subscriber, amely felfogja és megjeleníti (vagy elmenti) az eseményeket. Erre való a tracing-subscriber crate.
fn setup_tracing() {
tracing_subscriber::fmt()
.with_target(false)
.init();
}
Ezt egyszer, a main elején kell meghívni. Ezután minden info!, warn! stb. hívás szép, időbélyeggel ellátott sorként jelenik meg a terminálban.
2024-05-11T10:03:21.001Z INFO kiszolgáltuk a / útvonalat
A RUST_LOG környezeti változóval szűrheted a naplózás szintjét futásidőben, például RUST_LOG=info vagy RUST_LOG=debug. Ehhez a tracing_subscriber::fmt() helyett az EnvFilter-rel kombinált beállítást használhatod – ez a részletesebb konfiguráció egy haladóbb leckében kerül elő.
Middleware és tracing összekapcsolása
A két fogalom természetesen összekapcsolódik: a naplózó middleware-ben println! helyett tracing-et használunk, így strukturált, szűrhető naplókat kapunk minden kérésről.
use axum::{
extract::Request,
middleware::{self, Next},
response::Response,
routing::get,
Router,
};
use std::time::Instant;
use tracing::info;
async fn tracing_middleware(request: Request, next: Next) -> Response {
let method = request.method().clone();
let uri = request.uri().clone();
let start = Instant::now();
let response = next.run(request).await;
let elapsed = start.elapsed();
info!(%method, %uri, status = %response.status(), ?elapsed, "kérés feldolgozva");
response
}
async fn hello() -> &'static str {
"Szia, világ!"
}
#[tokio::main]
async fn main() {
tracing_subscriber::fmt().with_target(false).init();
let app = Router::new()
.route("/", get(hello))
.layer(middleware::from_fn(tracing_middleware));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
A %method és %uri szintaxis azt jelzi a tracing makrónak, hogy a Display-implementációt használja a mező kiírásához, a ?elapsed pedig a Debug-ot. Ez a strukturált formátum később, ha JSON-naplózásra váltasz, kulcs-érték páronként is exportálható lesz.
Próbáld ki!
Vegyél egy meglévő (vagy a fenti) Axum alkalmazást, és adj hozzá egy második route-ot, például /status. Módosítsd a tracing_middleware-t úgy, hogy warn!-t hívjon, ha a válasz státuszkódja 400 vagy nagyobb. Futtasd a szervert, kérj le egy nem létező útvonalat (ami 404-et ad), és figyeld meg a különbséget a naplóban az info! és a warn! sorok között.
Önellenőrzés
- Miért nem elég a
println!egy éles, több egyidejű kérést kezelő async webszerverben?
Megoldás
A `println!` nem strukturált: nem lehet szint szerint szűrni (info/warn/error), nem ad időbélyeget vagy kontextust automatikusan, és nagy terhelés alatt a kimenete átláthatatlanná válik. A `tracing` ezzel szemben szinteket, strukturált mezőket és – span-ekkel – a kérések közti kontextust is kezeli, ami async, sok párhuzamos feladatot futtató rendszerben elengedhetetlen.- Mi történik, ha egy
from_fnmiddleware-ben elfelejtjük meghívni anext.run(request)-et?
Megoldás
A lánc megszakad: a handler (és minden mögötte lévő réteg) soha nem fut le. A kliens vagy egy alapértelmezett, gyakran hibás vagy üres választ kap, vagy a middleware-nek magának kell egy `Response`-t visszaadnia. Emiatt mindig ellenőrizni kell, hogy minden ágon (pl. korai visszatérés esetén is) helyesen kezeljük a láncot.- Miért érdemes a middleware-t a
Router-hez.layer()-rel hozzáadni, és nem közvetlenül minden handlerbe beleírni a naplózó kódot?
Megoldás
Egy `.layer()`-rel hozzáadott middleware minden route-ra automatikusan érvényes lesz, így nem kell minden egyes handlerbe bemásolni ugyanazt a logikát. Ez csökkenti az ismétlődő kódot, egy helyen tartja a keresztmetszeti feladatokat (naplózás, timeout, fejlécek), és ha később módosítani kell a viselkedést, elég egyetlen helyen javítani.Összefoglalás
- A middleware olyan réteg, amely a kérés és a válasz közé illeszkedik, és
Router::layer+axum::middleware::from_fnsegítségével köthető be. - A middleware belsejéből a
next.run(request).awaithívással engedjük tovább a kérést a láncban; ha ez kimarad, a handler sosem fut le. - Tipikus middleware-feladatok: naplózás, timeout, fejlécek hozzáadása, kérés-korlátozás.
- A
tracingcrate strukturált, szint alapú naplózást ad async kódhoz; atracing-subscribervégzi a naplók megjelenítését, amit egyszer, amainelején kell inicializálni. - A middleware és a tracing kombinációja adja azt az alapot, amire éles Axum szolgáltatásokban a megfigyelhetőség épül.
A következő leckében, a "JSON API projekt"-ben mindezt összerakjuk: egy teljes, JSON-alapú REST API-t építünk, amely route-okat, extractorokat, state-et és a most megismert middleware/tracing páros gyakorlati alkalmazását egyesíti egy összefüggő projektben.
További olvasmányok: