Zum Inhalt

Rust-Datenmodellierung: Structs, Enums & Robustes Error Handling

Erstellt: 06.11.2026 Zuletzt aktualisiert: 07.11.2026

Executive Summary: Klassische objektorientierte Programmiersprachen stützen sich auf Vererbungshierarchien, Klassen und ungeprüfte Laufzeit-Exceptions – was Kontrollflüsse oft verschleiert und zu allgegenwärtigen NullPointerExceptions führt. Rust bricht radikal mit diesem Paradigma. Statt Vererbung setzt Rust auf Komposition über Vererbung auf Basis Algebraischer Datentypen (ADTs): Structs (Produkttypen), die zusammengehörige Felder mit deterministischem Speicherlayout bündeln, und Enums (Summentypen), die sich gegenseitig ausschließende Zustände mit reichhaltigen Nutzdaten (Payloads) abbilden. Zudem verbannt Rust Null-Referenzen durch Option<T> und modelliert fehlschlagbare Operationen explizit über Result<T, E>. Dieses Kapitel bildet Teil 1.2 (Grundlagen): Analyse von Struct-Varianten und Byte-Alignment, Implementierung von Methoden und Konstruktoren, meisterhafter Musterabgleich (Pattern Matching), robuste Fehlerfortpflanzung mit dem ?-Operator sowie die Realisierung eines absturzsicheren Webserver-Access-Logparsers.

  • Structs & Speicherlayout


    Konstruiere klassisch benannte Structs, Tupel-Structs (Newtype-Pattern) und speicherlose Unit-Structs mit optimiertem CPU-Cache-Alignment.

  • Methoden & impl-Blöcke


    Implementiere Instanzmethoden mit &self, &mut self und konsumierendem self sowie assoziierte Konstruktor-Funktionen (Self::new).

  • Algebraische Enums & Pattern Matching


    Modelliere komplexe Zustandsautomaten mit nutzlasttragenden Summentypen, erschöpfenden match-Ausdrücken, Match Guards und moderner let else-Syntax.

  • Option<T>, Result<T, E> & ?


    Eliminiere Null-Pointer-Bugs für immer, trenne behebbare Fehler von unrettbaren Panics und leite Fehler elegant mit dem ?-Operator weiter.


1. Structs: Produkttypen im Systementwurf

In der Typentheorie ist ein Produkttyp ein zusammengesetzter Datentyp, dessen Zustandsraum dem kartesischen Produkt seiner Einzelfelder entspricht. In Rust werden Produkttypen über struct realisiert. Die Sprache bietet drei Struct-Varianten für unterschiedliche Architekturmuster:

  1. Klassisch benannte Structs: Explizite Feldnamen für maximale Lesbarkeit und selbstdokumentierende Schnittstellen.
  2. Tupel-Structs: Positionsbasierte Felder ohne Namen, ideal für schlanke Wrapper und das typsichere Newtype-Pattern.
  3. Unit-Like Structs: Structs gänzlich ohne Felder, die 0 Byte Speicher auf dem Stack belegen (Zero-Sized Types / ZST), prädestiniert für Trait-Marker und Compile-Time-Zustandsmaschinen.
Deklaration der drei Struct-Varianten
// 1. Klassisch benanntes Struct
#[derive(Debug, Clone, PartialEq)]
pub struct NetworkInterface {
    pub name: String,
    pub mtu: u32,
    pub is_up: bool,
}

// 2. Tupel-Struct (Newtype-Pattern für strikte Domain-Typsicherheit)
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Port(pub u16);

#[derive(Debug, Copy, Clone, PartialEq)]
pub struct Megabytes(pub f64);

// 3. Unit-Like Struct (Zero-Sized Type: belegt exakt 0 Byte im RAM!)
#[derive(Debug, Copy, Clone)]
pub struct PromiscuousModeMarker;

Speicher-Alignment und Struct-Padding

Standardmäßig verwendet der Rust-Compiler (rustc) das Repräsentationsmodell repr(Rust). Im Gegensatz zu C (wo Felder strikt in Deklarationsreihenfolge im Speicher liegen) sortiert rustc Struct-Felder automatisch um, um CPU-Padding-Lücken zu minimieren und Speicherzugriffe optimal an Cache-Lines auszurichten.

Speicherlayout-Optimierung (Vermeidung von Alignment-Löchern)
Unoptimierte C-Deklarationsreihenfolge (40 Bytes Gesamtgröße durch Padding):
[ u8: 1 Byte ] [ 7 Bytes Padding ] [ u64: 8 Bytes ] [ bool: 1 Byte ] [ 7 Bytes Padding ] [ String: 24 Bytes ]

Optimierte rustc-Feldanordnung (32 Bytes Gesamtgröße):
[ String: 24 Bytes ] [ u64: 8 Bytes ] [ u8: 1 Byte ] [ bool: 1 Byte ] [ 6 Bytes trailing Padding ]

[!TIP] Für direkte C-FFI-Kompatibilität oder speicherabgebildete Hardware-Register (MMIO) erzwingt #[repr(C)] oder #[repr(packed)] eine feste, nicht umgeordnete Bytedarstellung exakt nach C-Standard.


2. Methoden und assoziierte Funktionen (impl)

In Rust sind Datenstruktur und Verhalten strikt entkoppelt. Methoden und Konstruktoren werden innerhalb von impl-Blöcken definiert.

Die Empfänger-Semantik: &self, &mut self und self

Der erste Parameter einer Funktionssignatur bestimmt die Eigentums- und Zugriffsrechte:

| Empfänger-Signatur | Ownership- / Borrow-Semantik | Typischer Anwendungsfall | +| :--- | :--- | :--- | | &self | Geteilte Lese-Referenz: Beliebig viele parallele Aufrufer erlaubt. | Getter, Statusabfragen, Validierung, Serialisierung. | | &mut self | Exklusive Schreib-Referenz: Genau ein exklusiver Aufrufer hat Schreibzugriff. | Modifikation interner Puffer, Zähler, Zustandsänderungen. | | self | Ownership-Transfer (Konsumierung): Das Ursprungsobjekt wird zerstört/übertragen. | Builder-Pattern-Finalizer, Zustandsübergänge (State Machine). |

Umfassender impl-Block in der Praxis
pub struct LinuxService {
    name: String,
    pid: Option<u32>,
    restart_count: u32,
}

impl LinuxService {
    /// Assoziierte Konstruktor-Funktion (Kein self-Parameter, Aufruf via LinuxService::new)
    pub fn new(name: impl Into<String>) -> Self {
        Self {
            name: name.into(),
            pid: None,
            restart_count: 0,
        }
    }

    /// Shared Borrow (&self): Reine Leseabfrage
    pub fn status(&self) -> (&str, bool) {
        (&self.name, self.pid.is_some())
    }

    /// Mutable Borrow (&mut self): Verändert den internen Zustand
    pub fn start(&mut self, assigned_pid: u32) {
        self.pid = Some(assigned_pid);
        self.restart_count += 1;
    }

    /// Konsumierender Empfänger (self): Wandelt das Objekt um und zerstört das Original
    pub fn decommission(self) -> String {
        format!("Dienst '{}' wurde endgültig außer Betrieb genommen.", self.name)
        // 'self' wird hier am Ende des Gültigkeitsbereichs via Drop deallokiert
    }
}

3. Algebraische Enums: Modellierung exklusiver Zustände

In traditionellen Programmiersprachen sind Enums meist bloße Integer-Konstanten. In Rust sind Enums vollwertige Summentypen (Tagged Unions / Discriminated Unions). Jede Variante kann völlig eigenständige Datentypen als Nutzlast tragen.

Algebraisches Enum mit vielseitigen Payloads
#[derive(Debug, Clone)]
pub enum NetworkEvent {
    /// Unit-Variante: Keine Nutzdaten
    LinkDown,
    /// Tupel-Variante: Trägt IP-Adresse und Port
    ConnectionEstablished(std::net::IpAddr, u16),
    /// Struct-Variante: Benannte Felder für komplexe Telemetrie
    PacketReceived {
        interface: String,
        bytes_transferred: usize,
        checksum_valid: bool,
    },
    /// Gekapselter Fehlerzustand
    FatalError(String),
}
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph DataModeling["Rust Algebraische Datentypen (ADT)"]
        direction TB
        subgraph ProductType["Produkttyp: Struct"]
            direction TB
            ST1["struct ServerConfig<br/>Alle Felder existieren zeitgleich"]
            ST2["host: String (24 Byte auf Stack)<br/>port: u16 (2 Byte + 6B Padding)<br/>tls: bool (1 Byte + 7B Padding)"]
            ST3["Gesamtspeichergröße =<br/>Summe der Felder + Padding (32 Byte)"]
            ST1 --> ST2 --> ST3
        end
        subgraph SumType["Summentyp: Enum"]
            direction TB
            EN1["enum NetworkEvent<br/>Exakt EINE Variante ist aktiv"]
            EN2["LinkDown: Unit<br/>ConnectionEstablished: Tupel<br/>PacketReceived: Struct-Payload"]
            EN3["Total Memory Size =<br/>Tag-Diskriminante (1-8B) +<br/>Größte Varianten-Nutzlast + Padding"]
            EN1 --> EN2 --> EN3
        end
    end

Niche Filling und Null Pointer Optimization (NPO)

Eine herausragende Compiler-Optimierung von rustc ist die Null Pointer Optimization (NPO). Für Typen, bei denen Bitmuster 0x0 ungültig ist (z. B. Referenzen &T, Box<T> oder std::num::NonZeroU64), nutzt der Compiler den Null-Pointer 0x0, um die None-Variante von Option<T> darzustellen.

Nachweis der Zero-Cost Option-Optimierung
use std::mem::size_of;

// Beide belegen auf einer 64-Bit-Architektur exakt 16 Byte!
assert_eq!(size_of::<&str>(), 16);
assert_eq!(size_of::<Option<&str>>(), 16); // Null Overhead! Kein zusätzliches Tag-Byte!

4. Pattern Matching: Präziser Kontrollfluss

Rusts match-Ausdruck erzwingt zur Compile-Zeit Vollständigkeit (Exhaustiveness): Jeder denkbare Fall muss behandelt werden, wodurch unvorhergesehene Randfälle mathematisch unmöglich werden.

Vollständiges match, Guards und Destrukturierung

Fortgeschrittenes Pattern Matching mit Guards
fn process_event(event: &NetworkEvent) -> String {
    match event {
        NetworkEvent::LinkDown => "Physische Netzwerkverbindung unterbrochen.".to_string(),
        NetworkEvent::ConnectionEstablished(ip, port) if ip.is_loopback() => {
            format!("Lokale Loopback-Verbindung auf Port {port}")
        }
        NetworkEvent::ConnectionEstablished(ip, port) => {
            format!("Externer Peer verbunden: {ip}:{port}")
        }
        NetworkEvent::PacketReceived { bytes_transferred, checksum_valid: true, .. } => {
            format!("{bytes_transferred} authentifizierte Bytes verarbeitet.")
        }
        NetworkEvent::PacketReceived { checksum_valid: false, .. } => {
            "Beschädigter Frame erkannt; Paket verworfen.".to_string()
        }
        NetworkEvent::FatalError(err) => format!("Kernel-Panic-Alarm: {err}"),
    }
}

Ergonomische Syntax: if let, while let und let else

Wenn nur eine einzelne Variante von Interesse ist, bietet Rust schlanke Konstrukte:

Ergonomische Matching-Konstrukte
let raw_config: Option<u16> = Some(8080);

// 1. if let: Ausführung nur bei Übereinstimmung
if let Some(port) = raw_config {
    println!("Dienst wird an Port {port} gebunden");
}

// 2. let else (Rust 1.65+): Idiomatischer Guard mit zwingendem Abbruch
fn parse_port_or_bail(opt: Option<u16>) -> u16 {
    let Some(port) = opt else {
        // Der else-Block MUSS divergieren (return, break, continue oder panic!)
        println!("Port-Konfiguration fehlt; verwende Standardwert 80");
        return 80;
    };
    port
}

5. Beseitigung von Null: Das Option<T>-Enum

Der britische Informatiker Sir Tony Hoare bezeichnete seine Erfindung der null-Referenz im Jahr 1965 rückblickend als seinen „Billion-Dollar Mistake“. Rust verhindert Null-Pointer-Dereferenzierungsfehler, indem es schlicht kein null-Schlüsselwort besitzt. Abwesende Werte werden explizit über Option<T> modelliert:

pub enum Option<T> {
    None,
    Some(T),
}

Funktionale Kombinatoren: Schluss mit unwrap()

In Produktionscode sollte der direkte Aufruf von .unwrap() vermieden werden. Stattdessen verketten wir funktionale Kombinatoren:

Idiomatische Option-Kombinatoren
let raw_timeout: Option<&str> = Some("4500");

// Wandle Some(&str) -> Some(u64) -> addiere 500ms Puffer
let calculated_timeout: u64 = raw_timeout
    .and_then(|s| s.parse::<u64>().ok()) // Parst String zu Result, konvertiert zu Option
    .map(|ms| ms + 500)                  // Transformiert inneren Wert, falls Some
    .unwrap_or(3000);                    // Fallback-Standardwert, falls None

assert_eq!(calculated_timeout, 5000);

6. Robuste Fehlerarchitektur: Result<T, E> und ?

Rust unterscheidet Laufzeitprobleme strikt in zwei Kategorien:

  1. Nicht behebbare Fehler (panic!): Invarianten-Verletzungen, Array-Zugriffe außerhalb der Grenzen oder interne Softwarefehler. Löst Stack-Unwinding (oder sofortigen Abbruch) aus und bereinigt den Speicher deterministisch via RAII Drop.
  2. Behebbare Fehler (Result<T, E>): Vorhersehbare betriebliche Ereignisse (Netzwerk-Timeouts, fehlende Dateien, ungültige Benutzereingaben, fehlerhafte Protokoll-Header).
pub enum Result<T, E> {
    Ok(T),
    Err(E),
}
%%{init: {'flowchart': {'wrappingWidth': 600}}}%%
flowchart TB
    subgraph ErrorHandling["Rust Duale Fehlerarchitektur"]
        direction TB
        E1["Operation oder Syscall-Ausführung"]
        E1 --> C1{"Fehler aufgetreten?"}

        C1 -- Nein --> S1["Ok(T) oder Some(T)<br/>Reguläre Pipeline wird fortgesetzt"]

        C1 -- Ja --> C2{"Behebbarer Fehler?"}

        C2 -- Ja --> R1["Result::Err(E)<br/>Expliziter Fehlerwert"]
        R1 --> R2["Weiterleitung via ?-Operator<br/>Musterabgleich oder lokale Reparatur"]
        R2 --> R3["Graceful Degradation oder<br/>Strukturierter Logeintrag"]

        C2 -- Nein --> U1["Unrettbarer Systemfehler<br/>Bug, Out of Bounds, Invariantenbruch"]
        U1 --> U2["panic! wird ausgelöst"]
        U2 --> U3["Stack Unwinding &amp; RAII Drop<br/>oder Abbruch ohne Speicherkorruption"]
    end

Der ?-Operator (Try Operator)

Der ?-Operator entpackt Ok(T) im Erfolgsfall oder führt ein sofortiges return Err(From::from(e)) aus der aufrufenden Funktion durch:

Elegante Fehlerweiterleitung mit dem ?-Operator
use std::fs::File;
use std::io::{self, Read};

fn read_kernel_hostname() -> Result<String, io::Error> {
    let mut file = File::open("/proc/sys/kernel/hostname")?; // Gibt io::Error bei Fehlschlag zurück
    let mut hostname = String::new();
    file.read_to_string(&mut hostname)?;                    // Gibt io::Error bei Fehlschlag zurück
    Ok(hostname.trim().to_string())
}

7. Entwurf professioneller Domain-Fehlertypen

In professionellen Systemen sind reine Textfehler (Err("failed")) ein Anti-Pattern. Stabile Software erfordert stark typisierte Fehler-Enums:

Eigener typsicherer Fehler-Enum (Ohne externe Abhängigkeiten)
use std::fmt;
use std::error::Error;

#[derive(Debug, PartialEq, Eq)]
pub enum ServerConfigError {
    IoFailure(String),
    InvalidPort { requested: u32, max: u16 },
    MissingMandatoryKey(&'static str),
}

impl fmt::Display for ServerConfigError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::IoFailure(msg) => write!(f, "I/O-Subsystemfehler: {msg}"),
            Self::InvalidPort { requested, max } => {
                write!(f, "Port {requested} überschreitet erlaubtes Maximum {max}")
            }
            Self::MissingMandatoryKey(key) => write!(f, "Erforderlicher Konfigurationsschlüssel fehlt: {key}"),
        }
    }
}

impl Error for ServerConfigError {}

[!NOTE] In größeren Cargo-Projekten automatisiert das Standard-Crate thiserror die Implementierung von Display und Error über prozedurale Makros (#[derive(Error)]), während anyhow in Anwendungs-Binaries für unkomplizierte Fehleraggregation mit Kontext-Traces genutzt wird.


8. Hands-On Systems Lab: web_log_parser.rs

Wir implementieren einen produktionsreifen, absturzsicheren Apache/Nginx Webserver-Logparser. Er nutzt klassische Structs, Summentyp-Enums, eigene Domain-Fehler und lückenloses Pattern Matching.

web_log_parser.rs (Eigenständiges ausführbares Systemlabor)
use std::fmt;
use std::error::Error;
use std::str::FromStr;

/// HTTP-Anfragemethoden (Summentyp)
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Head,
    Options,
}

impl FromStr for HttpMethod {
    type Err = ParseError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s {
            "GET" => Ok(HttpMethod::Get),
            "POST" => Ok(HttpMethod::Post),
            "PUT" => Ok(HttpMethod::Put),
            "DELETE" => Ok(HttpMethod::Delete),
            "PATCH" => Ok(HttpMethod::Patch),
            "HEAD" => Ok(HttpMethod::Head),
            "OPTIONS" => Ok(HttpMethod::Options),
            other => Err(ParseError::UnknownHttpMethod(other.to_string())),
        }
    }
}

/// HTTP-Status-Kategorien
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum HttpStatusCategory {
    Success,      // 2xx
    Redirection,  // 3xx
    ClientError,  // 4xx
    ServerError,  // 5xx
    Unknown,
}

/// Strukturierter Web-Logdatensatz (Produkttyp)
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogRecord {
    pub client_ip: String,
    pub timestamp: String,
    pub method: HttpMethod,
    pub path: String,
    pub status_code: u16,
    pub response_bytes: u64,
}

impl LogRecord {
    /// Bestimmt Statuskategorie über Bereichsabgleich (Range Matching)
    pub fn status_category(&self) -> HttpStatusCategory {
        match self.status_code {
            200..=299 => HttpStatusCategory::Success,
            300..=399 => HttpStatusCategory::Redirection,
            400..=499 => HttpStatusCategory::ClientError,
            500..=599 => HttpStatusCategory::ServerError,
            _ => HttpStatusCategory::Unknown,
        }
    }
}

/// Stark typisierter Domain-Fehler
#[derive(Debug, PartialEq, Eq)]
pub enum ParseError {
    EmptyLine,
    InsufficientFields { found: usize, expected: usize },
    MalformedRequestSection,
    UnknownHttpMethod(String),
    InvalidStatusCode(String),
    InvalidByteCount(String),
}

impl fmt::Display for ParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::EmptyLine => write!(f, "Logzeile war leer oder enthielt nur Whitespace"),
            Self::InsufficientFields { found, expected } => {
                write!(f, "Unzureichende Spaltenanzahl: {found} gefunden, mindestens {expected} erwartet")
            }
            Self::MalformedRequestSection => write!(f, "Format des HTTP-Anfrageabschnitts ist ungültig"),
            Self::UnknownHttpMethod(m) => write!(f, "Unbekannte HTTP-Methode: '{m}'"),
            Self::InvalidStatusCode(s) => write!(f, "HTTP-Statuscode kann nicht als Zahl geparst werden: '{s}'"),
            Self::InvalidByteCount(b) => write!(f, "Byte-Größe der Antwort kann nicht geparst werden: '{b}'"),
        }
    }
}

impl Error for ParseError {}

/// Reiner Parser für kombinierte Apache/Nginx-Logzeilen
pub fn parse_log_line(line: &str) -> Result<LogRecord, ParseError> {
    let trimmed = line.trim();
    if trimmed.is_empty() {
        return Err(ParseError::EmptyLine);
    }

    // Anführungszeichen trennen Prefix, HTTP-Request und Suffix
    let parts: Vec<&str> = trimmed.split('"').collect();
    if parts.len() < 3 {
        return Err(ParseError::MalformedRequestSection);
    }

    // 1. Client-IP und Timestamp aus Prefix extrahieren
    let prefix_tokens: Vec<&str> = parts[0].split_whitespace().collect();
    if prefix_tokens.is_empty() {
        return Err(ParseError::InsufficientFields { found: 0, expected: 1 });
    }
    let client_ip = prefix_tokens[0].to_string();

    let timestamp = parts[0]
        .find('[')
        .and_then(|start| parts[0].find(']').map(|end| &parts[0][start + 1..end]))
        .unwrap_or("UNBEKANNTER_ZEITSTEMPEL")
        .to_string();

    // 2. Request-Abschnitt parsen: "GET /api/v1/telemetry HTTP/1.1"
    let request_tokens: Vec<&str> = parts[1].split_whitespace().collect();
    if request_tokens.len() < 2 {
        return Err(ParseError::MalformedRequestSection);
    }
    let method = HttpMethod::from_str(request_tokens[0])?;
    let path = request_tokens[1].to_string();

    // 3. Statuscode und Bytegröße aus Suffix parsen
    let suffix_tokens: Vec<&str> = parts[2].split_whitespace().collect();
    if suffix_tokens.len() < 2 {
        return Err(ParseError::InsufficientFields {
            found: suffix_tokens.len(),
            expected: 2,
        });
    }

    let status_code = suffix_tokens[0]
        .parse::<u16>()
        .map_err(|_| ParseError::InvalidStatusCode(suffix_tokens[0].to_string()))?;

    let response_bytes = match suffix_tokens[1] {
        "-" => 0, // HTTP 304 oder leerer Antwortkörper
        raw => raw
            .parse::<u64>()
            .map_err(|_| ParseError::InvalidByteCount(raw.to_string()))?,
    };

    Ok(LogRecord {
        client_ip,
        timestamp,
        method,
        path,
        status_code,
        response_bytes,
    })
}

fn main() {
    println!("=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITEKTUR) ===\n");

    let raw_logs = [
        r#"192.168.1.105 - - [07/Nov/2026:12:30:45 +0000] "GET /api/v1/telemetry HTTP/1.1" 200 4826"#,
        r#"10.0.0.12 - - [07/Nov/2026:12:30:46 +0000] "POST /api/v1/auth HTTP/1.1" 401 128"#,
        r#"172.16.0.5 - - [07/Nov/2026:12:30:47 +0000] "DELETE /admin/config HTTP/1.1" 403 89"#,
        r#"192.168.1.200 - - [07/Nov/2026:12:30:48 +0000] "GET /static/bundle.js HTTP/1.1" 304 -"#,
        r#"10.0.0.99 - - [07/Nov/2026:12:30:49 +0000] "PROPFIND /webdav/docs HTTP/1.1" 405 210"#,
        r#"192.168.1.105 - - [07/Nov/2026:12:30:50 +0000] "GET /api/v2/metrics HTTP/1.1" 500 512"#,
        r#"FEHLERHAFTE_ZEILE_OHNE_ANFUEHRUNGSZEICHEN"#,
    ];

    let mut total_requests = 0;
    let mut total_bytes = 0;
    let mut client_errors = 0;
    let mut server_errors = 0;
    let mut rejected_lines = 0;

    for (idx, line) in raw_logs.iter().enumerate() {
        match parse_log_line(line) {
            Ok(record) => {
                total_requests += 1;
                total_bytes += record.response_bytes;

                match record.status_category() {
                    HttpStatusCategory::ClientError => client_errors += 1,
                    HttpStatusCategory::ServerError => server_errors += 1,
                    _ => {}
                }

                println!(
                    "[{:>2}] OK  | {:<6?} | {:<3} ({:<11?}) | {:<20} | {:>5} B",
                    idx + 1,
                    record.method,
                    record.status_code,
                    record.status_category(),
                    record.path,
                    record.response_bytes
                );
            }
            Err(err) => {
                rejected_lines += 1;
                eprintln!("[{:>2}] ERR | Zeile abgewiesen: {err}", idx + 1);
            }
        }
    }

    println!("\n--- TELEMETRIE-AGGREGATIONS-ZUSAMMENFASSUNG ---");
    println!("Gültige Anfragen gesamt: {total_requests}");
    println!("Gesamtes Datenvolumen:   {total_bytes} Bytes");
    println!("Client 4xx-Fehler:       {client_errors}");
    println!("Server 5xx-Fehler:       {server_errors}");
    println!("Verworfene Zeilen:       {rejected_lines}");
}

Kompilierung & Ausführung des Labs

Kompiliere und starte das eigenständige Labor direkt mit rustc:

Kompilierung und Ausführung des Systemlabors
rustc -O web_log_parser.rs
./web_log_parser

Beispielhafte Terminal-Ausgabe:

=== LINUX NGINX ACCESS LOG PARSER (ZERO-PANIC ARCHITEKTUR) ===

[ 1] OK  | Get    | 200 (Success    ) | /api/v1/telemetry    |  4826 B
[ 2] OK  | Post   | 401 (ClientError) | /api/v1/auth         |   128 B
[ 3] OK  | Delete | 403 (ClientError) | /admin/config        |    89 B
[ 4] OK  | Get    | 304 (Redirection) | /static/bundle.js    |     0 B
[ 5] ERR | Zeile abgewiesen: Unbekannte HTTP-Methode: 'PROPFIND'
[ 6] OK  | Get    | 500 (ServerError) | /api/v2/metrics      |   512 B
[ 7] ERR | Zeile abgewiesen: Format des HTTP-Anfrageabschnitts ist ungültig

--- TELEMETRIE-AGGREGATIONS-ZUSAMMENFASSUNG ---
Gültige Anfragen gesamt: 5
Gesamtes Datenvolumen:   5555 Bytes
Client 4xx-Fehler:       2
Server 5xx-Fehler:       1
Verworfene Zeilen:       2


Nächste Schritte

Nachdem zusammengesetzte Typen, algebraische Enums und das Fehlermodell gefestigt sind, widmen wir uns im nächsten Kapitel dem dynamischen Speicher, zusammenhängenden Heap-Strukturen und UTF-8-Zeichenketten:

  • Teil 2.1: Collections, Strings & Slices


    Meistere Vec<T>, String-Slices &str, Heap-Reallokationskosten, BTreeMap vs. HashMap und Zero-Cost-Iterator-Pipelines.

    Weiter zu Teil 2.1