Published on

Log analyzer ultraveloce con Rust + Tokio (e confronto con Bash)

Authors
  • avatar
    Name
    Alessandro Iannacone
    Twitter

Log analyzer ultraveloce con Rust + Tokio (e confronto "finto ma realistico" con Bash)

In questo articolo costruiamo un parser di log HTTP in Rust sfruttando Tokio per pipeline asincrona e parallelismo sul parsing. A fine guida trovi un confronto plausibile con uno script Bash/awk su un file da 1 GB, giusto per capire gli ordini di grandezza.


Obiettivi

  • Leggere velocemente file di log stile Nginx/Apache (Combined Log Format).
  • Estrarre per ogni riga: status code, path, timestamp (minuto).
  • Calcolare:
    • conteggio per status code,
    • top 10 path per richieste,
    • richieste per minuto (rate).
  • Pipeline asincrona: reader → workers di parsing → aggregatore.
  • Output in JSON e tabellare.

Prerequisiti

  • Rust 1.75+ (o recente) con cargo

  • File di log in formato tipo:

    127.0.0.1 - - [12/Oct/2025:10:15:42 +0000] "GET /api/items?id=7 HTTP/1.1" 200 123 "-" "curl/8.1.0"

  • Conoscenza base di regex.


Struttura del progetto

cargo new fastlog
cd fastlog

Cargo.toml

[package]
name = "fastlog"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
regex = "1"
clap = { version = "4", features = ["derive"] }
anyhow = "1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Implementazione

Strategia

  • Reader (1 task): legge il file riga per riga (I/O asincrono) e immette le righe in un canale MPSC con backpressure (buffer limitato).
  • Workers (N task): consumano le righe e applicano una regex compilata per estrarre campi; inviano risultati (strutturati) a un secondo canale.
  • Aggregatore (1 task): consuma i risultati e aggiorna mappe in memoria (niente lock condivisi tra i worker; l'aggregazione è centralizzata e quindi lock-free per i worker).

Questo design massimizza throughput quando: - l'I/O è "abbastanza veloce", - il parsing/regex è la parte "costosa" parallelizzabile.

src/main.rs

// Codice completo come nell'articolo precedente...

Confronto (simulato) con Bash/awk

Script Bash (baseline)

#!/usr/bin/env bash
# Estrae status, path e minuto; produce top path e conteggi per status/minuto

LOG=${1:-/var/log/nginx/access.log}

awk '
{
  match($0, /\[([^]]+)\]/, ts)
  split(ts[1], a, ":"); minute=a[1] ":" a[2]
  match($0, /"(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS) ([^ "]+)/, req)
  path=req[2]
  tail=substr($0, RSTART+RLENGTH)
  match(tail, / ([0-9]{3}) /, st)
  status=st[1]

  by_status[status]++
  by_path[path]++
  by_minute[minute]++
}
END {
  print "== by_status =="
  for (s in by_status) print s, by_status[s]
}' "$LOG"

Benchmark indicativo

Implementazione Tempo medio Throughput stimato


Rust + Tokio (8 worker) ~1.8 s ~550 MB/s Bash + awk ~18 s ~55 MB/s


Conclusioni

Rust + Tokio offrono un'architettura moderna per processare grandi quantità di log con massimo parallelismo, basso overhead e controllo della memoria.
Il confronto con Bash/awk è simbolico, ma evidenzia la differenza tra un approccio scripting e uno nativo, compilato e asincrono.