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-libservices = logging (§1) + identifiants (§2). La §3 Configuration est SUPERSEDED (ADR-033/040/041) : la config devient la crate dédiéeair-config(compilateur + artefact Cap’n Proto), horsair-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-libconsomme 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) :
AirLog→air-sys-syscall::net(socketAF_UNIX/SOCK_DGRAM,sendmsg
SCM_RIGHTS),::mem(memfd_createpour les grandes entrées),fcntl(scellement du memfd) ;AirUuid→::system::getrandom+ horloge (AirInstant/AirDateTimedu 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:AirLogparle le protocole socket natif de journald ;AirId128lit/etc/machine-idcomme 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 cargoconfig-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 dansDEPENDENCIES.md/EXCEPTIONS.md).tracing: seulement si le ponttracingest activé (featuretracing-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 versair-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_UNIXvers le socket journal, corps = suite de lignesCLÉ=valeur\n; valeurs multi-lignes/binaires → formeCLÉ\n+ longueur little-endian 64 bits + octets. Champs obligatoires :MESSAGE=,PRIORITY=(0..7). - Grandes entrées (datagramme trop gros,
EMSGSIZE) : repli memfd — créer unmemfd_create(couche 0mem), y écrire l’entrée, le sceller (fcntlF_SEAL), puis l’envoyer enSCM_RIGHTSviasendmsg(couche 0net). 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), maislog()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épendancetracing. La macro-architecture veut une intégration avectracingstandard Rust : on la fournit comme adaptateur optionnel (untracing_subscriber::Layerqui réémet versAirLog) derrière la featuretracing-bridge, pour ne pas imposertracingà 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_UNIXfactice qui capture les datagrammes) reçoitMESSAGE/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 quelog()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 ;
tracingoptionnel ; 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.
- Pas de
sd-id128:AirId128::machine_idlit/etc/machine-iddirectement (pureté de couche). 2. La dérivation « app-specific » d’un ID machine (équivalentsd_id128_get_machine_app_specific, qui repose sur un HMAC) est différée à un module consommantair-crypto(sibling couche 1) — elle n’a pas sa place dans la fondation sans crypto. 3. L’aléa vient toujours degetrandom(couche 0), jamais d’un PRNG userspace (cohérent ADR-016/sécurité). 4.AirMonotonicIdest 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
AirConfigTOML/serde derrière une feature, dansair-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 dansair-base-lib, pour ne pas l’alourdir) ; TOML/JSON ne survivent que comme projections d’import/export. (Addendum :capnpcexige 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-configseul écrivain).Conséquence pratique :
air-base-libservices = logging (§1) + identifiants (§2) UNIQUEMENT. La configuration est le chantierair-config(crate couche 1 dédiée, à spécifier). Niserdenitomlne 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
AirConfigdansair-base-lib, mais la fondation se veut minimale et C-ABI-friendly (le cœur n’a queicu4x). OrAirConfigveutserde+toml(etserdederive est une proc-macro). Décision retenue : l’APIAirConfig+ la résolution de chemins XDG vivent dansair-base-lib(purement logiques) ; le backend de (dé)sérialisation TOML/serde est derrière la feature cargoconfig-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’APIAirConfigannoncée. (Si tu préfères un crateair-configsé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 0fs—AirConfig::loadconsomme l’un des deux (à préciser à l’implémentation :air-filesystemsi disponible, sinonfscouche 0). - Tests : résolution XDG (avec
XDG_CONFIG_HOMEposé/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 featureconfig-toml).
Récapitulatif des services
| Domaine | Types / API principaux |
|---|---|
| Logging | AirLog, AirLogLevel, AirLogFields (journald socket natif ; pont tracing optionnel) |
| Identifiants | AirUuid (v7/v4), AirId128 (machine-id via fichier), AirMonotonicId |
| Configuration | AirConfig, 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)
| Service | Primitives couche 0 consommées |
|---|---|
AirLog | net (socket AF_UNIX, sendmsg+SCM_RIGHTS), mem (memfd_create), fcntl (seals) |
AirUuid | system::getrandom, horloge (cœur) |
AirId128 | fs (lecture /etc/machine-id) |
AirConfig | fs / 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,
AirUuidv7 monotone,AirMonotonicIdconcurrent, 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)
AirLog→ journald via socket natif, zéro dépendance systemd ; non-fatal (un log ne casse jamais l’appelant) ; ponttracingoptionnel (feature).AirId128lit/etc/machine-id(passd-id128) ; dérivation app-specific différée àair-crypto.- Aléa toujours via
getrandom(couche 0), jamais de PRNG userspace. AirConfig: API + XDG dans la fondation, backendserde/tomlfeature-gatedconfig-toml(fondation minimale par défaut). Alternative notée : crateair-configséparé.- 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.