Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Spec couche 1 — air-base-lib (services : logging, identifiants)

Spécification technique — Version 1.1 (2026-06-25). Couche 1 « Primitives système ». Fait suite à air-base-lib-core.md (erreurs, chaînes/chemins, temps).

Périmètre air-base-lib services = logging (§1) + identifiants (§2). La §3 Configuration est SUPERSEDED (ADR-033/040/041) : la config devient la crate dédiée air-config (compilateur + artefact Cap’n Proto), hors air-base-lib. Voir l’encadré en tête de §3.

Position et méthode

Ce document complète air-base-lib avec ses services : journalisation structurée (AirLog), identifiants (AirUuid, AirId128, AirMonotonicId), configuration (AirConfig). Mêmes conventions que le cœur (Rust idiomatique, aucun unsafe exposé, couverture 100 %, nommage ADR-029, arithmétique et encodages défensifs).

API Rust d’abord ; la surface ABI C (libair-base.so, ADR-012/027) reste différée à une passe dédiée. Méthode doc-d’abord : ce contrat est validé avant implémentation.

C’est ici qu’air-base-lib consomme réellement la couche 0 (le cœur ne la touchait qu’aux horloges). Carte de consommation (utile pour le journal de dette doc lors de l’implémentation) :

  • AirLogair-sys-syscall::net (socket AF_UNIX/SOCK_DGRAM, sendmsg
    • SCM_RIGHTS), ::mem (memfd_create pour les grandes entrées), fcntl (scellement du memfd) ;
  • AirUuid::system::getrandom + horloge (AirInstant/AirDateTime du cœur) ;
  • AirId128::fs (lecture de /etc/machine-id). Chaque fois que l’implémentation devra descendre dans le code source d’une de ces primitives faute de doc suffisante, on le consigne : c’est le signal de dette doc couche 0 à amender.

Dépendances assumées (règle des 80 %, ADR-024)

  • Aucune dépendance systemd/libsystemd : AirLog parle le protocole socket natif de journald ; AirId128 lit /etc/machine-id comme un fichier. La couche 1 reste pure (pas de dépendance montante vers la couche 2).
  • serde + toml : nécessaires à AirConfig. Isolés derrière une feature cargo config-toml (cf. décision §3) pour que la fondation reste minimale et C-ABI-friendly ; audit 80 % à mener à l’ajout (serde/toml sont consommés largement → vraisemblablement conformes ou exception justifiée, à documenter dans DEPENDENCIES.md/EXCEPTIONS.md).
  • tracing : seulement si le pont tracing est activé (feature tracing-bridge, cf. §1.4), sinon absent.
  • Pas de proc-macro Air (cohérent cœur).

Section 1 — Journalisation : AirLog

1.1 Décision de couche (rappel, gravée)

AirLog écrit vers journald via le protocole socket natif documenté (/run/systemd/journal/socket, AF_UNIX/SOCK_DGRAM), sans dépendre d’une bibliothèque systemd. La couche 1 reste pure ; journald reste un détail kernel/IPC consommé via la couche 0, pas une dépendance vers air-systemd (couche 2). Cohérent ADR-005 (intégration journald) sans inversion de couche.

1.2 API

#![allow(unused)]
fn main() {
/// Niveaux alignés sur les priorités syslog/journald (0..7).
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum AirLogLevel {
    Emergency = 0, Alert = 1, Critical = 2, Error = 3,
    Warning = 4, Notice = 5, Info = 6, Debug = 7,
}

/// Logger structuré. Cible journald (socket natif). `Clone` bon marché (handle partagé).
#[derive(Clone)]
pub struct AirLog { /* socket + namespace + champs par défaut */ }

impl AirLog {
    /// Ouvre un logger vers journald, dans un **namespace** donné (`None` = défaut).
    /// # Errors `AirError` si le socket journald est indisponible.
    pub fn open(namespace: Option<&str>) -> AirResult<Self>;

    /// Émet une entrée : message + champs structurés. Le champ `PRIORITY` est
    /// dérivé du `level`, `MESSAGE` du message ; les champs custom complètent.
    pub fn log(&self, level: AirLogLevel, message: &str, fields: &AirLogFields);

    /// Raccourcis ergonomiques.
    pub fn info(&self, message: &str);
    pub fn warning(&self, message: &str);
    pub fn error(&self, message: &str);
    pub fn debug(&self, message: &str);

    /// Retourne un logger enfant avec des **champs par défaut** attachés à toutes
    /// les entrées (contexte de service, identifiants de requête…).
    pub fn with_default_fields(&self, fields: AirLogFields) -> AirLog;
}

/// Champs structurés (clé/valeur). Clés validées au format journald.
#[derive(Debug, Clone, Default)]
pub struct AirLogFields { /* … */ }

impl AirLogFields {
    pub fn new() -> Self;
    /// Ajoute un champ texte. La clé doit être `[A-Z][A-Z0-9_]*` (convention
    /// journald) ; sinon le champ est **rejeté silencieusement** et l'incident
    /// noté (un log ne doit jamais faire échouer le chemin appelant).
    pub fn with(self, key: &str, value: &str) -> Self;
    /// Champ binaire (valeurs avec retours-ligne/octets bruts → format binaire
    /// journald : `KEY\n` + longueur LE 64 bits + données).
    pub fn with_bytes(self, key: &str, value: &[u8]) -> Self;
}
}

1.3 Protocole journald (ce que l’implémentation doit faire)

  • Petites entrées : un datagramme AF_UNIX vers le socket journal, corps = suite de lignes CLÉ=valeur\n ; valeurs multi-lignes/binaires → forme CLÉ\n + longueur little-endian 64 bits + octets. Champs obligatoires : MESSAGE=, PRIORITY= (0..7).
  • Grandes entrées (datagramme trop gros, EMSGSIZE) : repli memfd — créer un memfd_create (couche 0 mem), y écrire l’entrée, le sceller (fcntl F_SEAL), puis l’envoyer en SCM_RIGHTS via sendmsg (couche 0 net). C’est le protocole documenté de journald pour les entrées volumineuses.
  • Non-bloquant et non-fatal : journald injoignable ⇒ l’entrée est perdue silencieusement (compteur de pertes), jamais d’erreur propagée à l’appelant — logger ne doit pas casser la logique métier. open() peut échouer (diagnostic initial), mais log() n’échoue jamais.

1.4 Pont tracing (optionnel, feature tracing-bridge)

Décision. Le cœur d’AirLog (API native + sink journald) est le primaire et n’impose aucune dépendance tracing. La macro-architecture veut une intégration avec tracing standard Rust : on la fournit comme adaptateur optionnel (un tracing_subscriber::Layer qui réémet vers AirLog) derrière la feature tracing-bridge, pour ne pas imposer tracing à tous les consommateurs (notamment C-ABI). Spécifié ici dans sa forme ; activable au besoin.

1.5 Tests / décisions

  • Intégration : un journald de test (ou un socket AF_UNIX factice qui capture les datagrammes) reçoit MESSAGE/PRIORITY/champs ; vérifier l’encodage texte et binaire (valeur multi-lignes), le repli memfd + SCM_RIGHTS sur grande entrée, la validation des clés, les champs par défaut hérités, et que log() n’échoue jamais (socket fermé → perte comptée).
  • Fuzzing : l’encodeur d’entrée (clé/valeur arbitraires → trame journald) ne panique jamais et n’émet pas de trame malformée.
  • Décisions : niveaux = priorités syslog ; clés invalides rejetées sans paniquer ; logger non-fatal ; tracing optionnel ; zéro dépendance systemd.

Section 2 — Identifiants

#![allow(unused)]
fn main() {
/// UUID 128 bits. v7 (ordonné dans le temps) préféré (ADR — observabilité/tri).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AirUuid([u8; 16]);

impl AirUuid {
    /// Génère un UUID **v7** : timestamp ms (horloge mur, cœur) + aléa
    /// (`getrandom`, couche 0). Monotone-friendly pour les index.
    /// # Errors `AirError` si l'entropie ou l'horloge échoue.
    pub fn new_v7() -> AirResult<Self>;
    /// Génère un UUID **v4** (purement aléatoire) si l'ordre temporel n'est pas voulu.
    pub fn new_v4() -> AirResult<Self>;
    pub fn as_bytes(&self) -> &[u8; 16];
    pub fn to_hyphenated(&self) -> String;          // 8-4-4-4-12
    /// # Errors `InvalidData` si la chaîne n'est pas un UUID valide.
    pub fn parse(s: &str) -> AirResult<Self>;
}

/// Identifiant 128 bits générique (concept `sd-id128`), **sans dépendance systemd**.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AirId128([u8; 16]);

impl AirId128 {
    /// L'ID machine, lu depuis `/etc/machine-id` (fichier, couche 0 `fs`).
    /// # Errors `AirError` (NotFound/InvalidData) si absent ou mal formé.
    pub fn machine_id() -> AirResult<Self>;
    pub fn as_bytes(&self) -> &[u8; 16];
    pub fn to_hex(&self) -> String;                 // 32 hex, comme machine-id
}

/// Compteur monotone **local au processus** (pas global), pour corréler des
/// événements dans un même run. Atomique, jamais réutilisé dans le processus.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct AirMonotonicId(u64);

impl AirMonotonicId {
    pub fn next() -> Self;                           // incrément atomique global au processus
    pub fn value(&self) -> u64;
}
}

Décisions.

  1. Pas de sd-id128 : AirId128::machine_id lit /etc/machine-id directement (pureté de couche). 2. La dérivation « app-specific » d’un ID machine (équivalent sd_id128_get_machine_app_specific, qui repose sur un HMAC) est différée à un module consommant air-crypto (sibling couche 1) — elle n’a pas sa place dans la fondation sans crypto. 3. L’aléa vient toujours de getrandom (couche 0), jamais d’un PRNG userspace (cohérent ADR-016/sécurité). 4. AirMonotonicId est local au processus, pas un identifiant distribué.

Tests : v7 monotone-croissant sur la milliseconde + format correct (version 7, variant) ; v4 (version 4) ; round-trip to_hyphenated/parse ; rejet de chaînes invalides ; machine_id (fichier de test) + rejet d’un contenu mal formé ; AirMonotonicId::next strictement croissant, thread-safe (property-based concurrent). Fuzzing : AirUuid::parse (données externes).


Section 3 — Configuration : AirConfig — ⚠️ SUPERSEDED (ne plus implémenter ici)

⚠️ SECTION OBSOLÈTE (2026-06-25). Cette section (v1.0, antérieure aux ADRs de configuration) décrit un modèle abandonné : un AirConfig TOML/serde derrière une feature, dans air-base-lib. Elle est superseded par :

  • ADR-033 — la config a pour source de vérité une source typée compilée en artefact binaire reproductible ; le texte n’est qu’une projection. Ce n’est PAS un parseur TOML chargé au runtime.
  • ADR-040 — l’artefact est en Cap’n Proto, dans une crate dédiée air-config (PAS dans air-base-lib, pour ne pas l’alourdir) ; TOML/JSON ne survivent que comme projections d’import/export. (Addendum : capnpc exige le tool C++ au build → politique « code généré committé ».)
  • ADR-041 — coexistence /etc (projection générée, lecture seule sélective, air-config seul écrivain).

Conséquence pratique : air-base-lib services = logging (§1) + identifiants (§2) UNIQUEMENT. La configuration est le chantier air-config (crate couche 1 dédiée, à spécifier). Ni serde ni toml ne sont ajoutés à air-base-lib. Le contenu ci-dessous est conservé pour mémoire historique.

3.1 Décision (OBSOLÈTE — cf. encadré ci-dessus) : API dans la fondation, backend sérialisation feature-gated

Tension à trancher : la macro-architecture place AirConfig dans air-base-lib, mais la fondation se veut minimale et C-ABI-friendly (le cœur n’a que icu4x). Or AirConfig veut serde + toml (et serde derive est une proc-macro). Décision retenue : l’API AirConfig + la résolution de chemins XDG vivent dans air-base-lib (purement logiques) ; le backend de (dé)sérialisation TOML/serde est derrière la feature cargo config-toml (activée par défaut pour les consommateurs Rust, désactivable pour un profil minimal/C). Ainsi la fondation reste légère par défaut de compilation, sans renoncer à l’API AirConfig annoncée. (Si tu préfères un crate air-config séparé, c’est l’alternative — à arbitrer ; je recommande la feature, plus simple et cohérent avec « AirConfig ∈ air-base-lib ».)

3.2 API

#![allow(unused)]
fn main() {
/// Résout les emplacements de config selon XDG + conventions Air.
pub struct AirConfigPaths { /* … */ }

impl AirConfigPaths {
    /// Pour un composant nommé : `$XDG_CONFIG_HOME/<name>/`, repli `~/.config/<name>/`,
    /// puis répertoires système (`$XDG_CONFIG_DIRS`, `/etc/xdg/<name>/`).
    pub fn for_component(name: &str) -> Self;
    /// Liste ordonnée (la plus prioritaire d'abord) des chemins candidats d'un fichier.
    pub fn config_file_candidates(&self, file: &AirPath) -> Vec<AirPath>;
}

/// Config typée d'un composant. `T: Deserialize` (feature `config-toml`).
pub struct AirConfig;

impl AirConfig {
    /// Charge et **désérialise** la config d'un composant en `T` validé par le
    /// typage Rust + serde. Fusionne selon la cascade XDG (système < utilisateur).
    /// # Errors `AirError` (NotFound si aucun fichier ; InvalidData si TOML/typage invalide).
    #[cfg(feature = "config-toml")]
    pub fn load<T: serde::de::DeserializeOwned>(component: &str) -> AirResult<T>;

    /// Variante depuis une source TOML explicite (tests, override).
    #[cfg(feature = "config-toml")]
    pub fn from_toml_str<T: serde::de::DeserializeOwned>(s: &str) -> AirResult<T>;
}
}

3.3 Décisions / tests

  • TOML par défaut (macro-architecture). Format unique en v1 ; pas de YAML/JSON (simplicité, déterminisme).
  • Validation par schéma = typage Rust + serde : un champ manquant/du mauvais type → AirError { InvalidData } avec le chemin du champ fautif (contexte).
  • Cascade XDG : système < utilisateur (l’utilisateur surcharge). La résolution est pure (construction de chemins) ; la lecture effective passe par air-filesystem (sibling) ou la couche 0 fsAirConfig::load consomme l’un des deux (à préciser à l’implémentation : air-filesystem si disponible, sinon fs couche 0).
  • Tests : résolution XDG (avec XDG_CONFIG_HOME posé/absent, XDG_CONFIG_DIRS) ; désérialisation TOML valide → T ; TOML invalide / champ manquant → erreur contextualisée ; cascade système/utilisateur (fusion). Fuzzing : parsing TOML (données externes) ne panique jamais (sous feature config-toml).

Récapitulatif des services

DomaineTypes / API principaux
LoggingAirLog, AirLogLevel, AirLogFields (journald socket natif ; pont tracing optionnel)
IdentifiantsAirUuid (v7/v4), AirId128 (machine-id via fichier), AirMonotonicId
ConfigurationAirConfig, AirConfigPaths (TOML/serde derrière config-toml, XDG)

Différé : surface ABI C ; dérivation app-specific de l’ID machine (→ via air-crypto) ; formats de config autres que TOML.

Consommation de la couche 0 (récap pour la dette doc)

ServicePrimitives couche 0 consommées
AirLognet (socket AF_UNIX, sendmsg+SCM_RIGHTS), mem (memfd_create), fcntl (seals)
AirUuidsystem::getrandom, horloge (cœur)
AirId128fs (lecture /etc/machine-id)
AirConfigfs / air-filesystem (lecture des fichiers de config)

Stratégie de tests (services)

  • Couverture 100 % lignes + branches (couche fondatrice).
  • Unitaires + property-based : niveaux/priorités, validation des clés journald, AirUuid v7 monotone, AirMonotonicId concurrent, résolution XDG.
  • Intégration : journald (socket de capture), machine_id (fichier), AirConfig::load (fixtures TOML + variables XDG).
  • Fuzzing : encodeur d’entrée journald, AirUuid::parse, parsing TOML.
  • Doctests : exemples compilent et passent.

Décisions de fond (services)

  1. AirLog → journald via socket natif, zéro dépendance systemd ; non-fatal (un log ne casse jamais l’appelant) ; pont tracing optionnel (feature).
  2. AirId128 lit /etc/machine-id (pas sd-id128) ; dérivation app-specific différée à air-crypto.
  3. Aléa toujours via getrandom (couche 0), jamais de PRNG userspace.
  4. AirConfig : API + XDG dans la fondation, backend serde/toml feature-gated config-toml (fondation minimale par défaut). Alternative notée : crate air-config séparé.
  5. TOML seul en v1 (déterminisme, simplicité).

Travail à reprendre

  • Surface ABI C d’air-base-lib (cœur + services), une fois l’API Rust stabilisée (ADR-012/027).
  • Crates couche 1 suivantes : air-filesystem, air-process, air-socket, air-crypto (dont la dérivation app-specific de l’ID machine), air-device, air-thread, air-memory.

Licence du document : MPL 2.0 Statut : Spécification technique des services de air-base-lib (couche 1). API Rust ; ABI C différée.