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.

Jó tudni

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.

Megjegyzés

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
Tipp

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

  1. 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.
  1. Mi történik, ha egy from_fn middleware-ben elfelejtjük meghívni a next.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.
  1. 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_fn segítségével köthető be.
  • A middleware belsejéből a next.run(request).await hí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 tracing crate strukturált, szint alapú naplózást ad async kódhoz; a tracing-subscriber végzi a naplók megjelenítését, amit egyszer, a main elejé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: