From 5aa538ed5d20ea773addf3f29e4418ca0ff5c872 Mon Sep 17 00:00:00 2001 From: SinuS Von SifriduS Date: Sun, 16 Aug 2026 19:00:49 +0200 Subject: [PATCH] v0.1.4-pre.016 --- Cargo.toml | 5 +- crates/ksp-app-config-desk/Cargo.toml | 3 +- crates/ksp-app-config-desk/README.md | 10 +- crates/ksp-app-config-desk/TODO.md | 7 +- crates/ksp-app-config-desk/USAGE.md | 52 ++++++- crates/ksp-app-config-desk/frontend/main.html | 42 ++++- .../frontend/ts/logging.ts | 137 ++++++++++++++++- .../ksp-app-config-desk/frontend/ts/main.ts | 10 +- crates/ksp-app-config-desk/src/app_state.rs | 65 +++++++- crates/ksp-app-config-desk/src/bootstrap.rs | 25 ++- crates/ksp-app-config-desk/src/constants.rs | 4 +- crates/ksp-app-config-desk/src/lib.rs | 5 +- .../ksp-app-config-desk/src/logging_editor.rs | 4 +- .../src/logging_runtime.rs | 130 ++++++++++++++++ crates/ksp-app-config-desk/src/tauri.rs | 25 ++- .../unit_tests/logging_runtime.rs | 26 ++++ crates/ksp-logging-lib/README.md | 6 +- crates/ksp-logging-lib/TODO.md | 24 ++- crates/ksp-logging-lib/USAGE.md | 48 +++++- crates/ksp-logging-lib/src/error.rs | 4 +- crates/ksp-logging-lib/src/identity.rs | 71 +++++++++ crates/ksp-logging-lib/src/lib.rs | 17 +- crates/ksp-logging-lib/src/runtime.rs | 115 ++++++++++++-- crates/ksp-logging-lib/tests/public_api.rs | 8 +- crates/ksp-logging-lib/tests/runtime.rs | 16 +- crates/ksp-logging-lib/unit_tests/identity.rs | 23 +++ crates/ksp-logging-lib/unit_tests/runtime.rs | 34 +++- deltas/0.1.4/pre.016.md | 145 ++++++++++++++++++ docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md | 6 +- 29 files changed, 994 insertions(+), 73 deletions(-) create mode 100644 crates/ksp-app-config-desk/src/logging_runtime.rs create mode 100644 crates/ksp-app-config-desk/unit_tests/logging_runtime.rs create mode 100644 crates/ksp-logging-lib/src/identity.rs create mode 100644 crates/ksp-logging-lib/unit_tests/identity.rs create mode 100644 deltas/0.1.4/pre.016.md diff --git a/Cargo.toml b/Cargo.toml index ad185cd..a19bddd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ # file: Cargo.toml -# version: 85 +# version: 86 [workspace] resolver = "3" members = ["crates/ksp-app-config-desk", "crates/ksp-config-lib", "crates/ksp-core-lib", "crates/ksp-logging-lib"] [workspace.package] -version = "0.1.4-pre.15.fix.3" +version = "0.1.4-pre.16" edition = "2024" license = "MIT" repository = "https://git.sasedev.com/Sasedev/khadhroony-solana-project" @@ -23,6 +23,7 @@ tracing = { version = "^0.1", default-features = false, features = ["std"] } tracing-subscriber = { version = "^0.3", default-features = false, features = ["fmt", "json", "ansi"] } tracing-appender = { version = "^0.2", default-features = false } tokio = { version = "^1.53", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time"] } +time = { version = "^0.3", default-features = false, features = ["std"] } tauri = { version = "^2.11" } tauri-build = { version = "^2.6" } tauri-plugin-tracing = { version = "^0.3" } diff --git a/crates/ksp-app-config-desk/Cargo.toml b/crates/ksp-app-config-desk/Cargo.toml index 7ad8b5e..a2d27b3 100644 --- a/crates/ksp-app-config-desk/Cargo.toml +++ b/crates/ksp-app-config-desk/Cargo.toml @@ -1,5 +1,5 @@ # file: crates/ksp-app-config-desk/Cargo.toml -# version: 5 +# version: 6 [package] name = "ksp-app-config-desk" @@ -30,6 +30,7 @@ serde.workspace = true serde_json.workspace = true tauri.workspace = true tauri-plugin-tracing.workspace = true +time.workspace = true tokio.workspace = true ts-rs.workspace = true diff --git a/crates/ksp-app-config-desk/README.md b/crates/ksp-app-config-desk/README.md index 660b9e2..a43b564 100644 --- a/crates/ksp-app-config-desk/README.md +++ b/crates/ksp-app-config-desk/README.md @@ -1,5 +1,5 @@ - + # `ksp-app-config-desk` @@ -30,6 +30,8 @@ Le gabarit desktop actif fournit maintenant : - le panneau Profils générique pour `default_profile`, sélection explicite, vues global/profil/effective sûre et provenance ; - le panneau Environnement alimenté uniquement par `ConfigManagement`, avec rapport desired/effective sûr, source/shadowing, management atomique `.env` et reveal Secret privilégié/transitoire ; - le panneau Logging typé, alimenté par `ConfigManagement::load_logging_document()` / `save_logging_document()`, avec brouillon éditable, profils, console, fichiers, filtres, targets et domains sans parsing JSON frontend ; +- l’observabilité du runtime Logging actif : profil, source de sélection, génération, console, compteurs de drop, identité de lancement et prefixes fichiers effectifs ; +- l’application explicite d’un profil Logging persisté sans modifier `default_profile` ni le document source ; - `tauri-plugin-tracing` côté Rust et `@fltsci/tauri-plugin-tracing` côté frontend ; - les ports dédiés `1430` pour Vite HTTP et `1431` pour le WebSocket de développement ; - la destination frontend externe commune à Tauri et Vite; @@ -145,7 +147,11 @@ L'inspection expose le profil par défaut ou une sélection explicite, les vues La vue **Logging** charge le document standard exclusivement avec `ConfigManagement::load_logging_document()`. Rust mappe ensuite les types publics `LoggingConfigDocument`, `LoggingProfileConfig`, `LoggingConsoleConfig`, `LoggingFileConfig`, `LoggingOutputFilterConfig` et `LoggingTargetFilterConfig` vers des DTO TS-RS dédiés. Le frontend ne parse donc ni le JSON source ni son schema. -Le panneau expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. En `pre.015`, le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée. +Le panneau expose `format_version`, `logs_directory`, `default_profile`, tous les profils, la console, les fichiers persistants, les filtres locaux, les target overrides et les listes de targets/domains. Le frontend maintient un brouillon typé : create/clone/rename/delete de profils, 0/1/N file sinks et target filters restent locaux jusqu'à **Sauvegarder et appliquer**. Le backend reconstruit les types publics Config et appelle `ConfigManagement::save_logging_document()`, qui valide la totalité du candidat avant remplacement atomique. Après persistence, Config Desk recharge un `ConfigEnvironment` frais, résout le `default_profile`, puis appelle `ksp_logging_lib::reinitialize()` sur le `LoggingGuard` actif. Le hot reload est immédiat et `logging_generation` avance uniquement après succès. Si l'application runtime échoue, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** ne modifie que le brouillon/source persistée. + +`pre.016` distingue en plus le **profil default persistant** du **profil runtime actif**. La section **Runtime actif** expose le profil actuellement appliqué, `selection_source` (`default_profile`, `explicit` ou `fallback`), la génération, l'état console, les compteurs de lignes abandonnées et les file sinks réellement actifs. Un profil déjà persisté peut être appliqué explicitement sans modifier `default_profile` ni écrire le document ; cette action est désactivée tant que le brouillon contient des changements non sauvegardés. + +Chaque lancement de Config Desk crée aussi une `LoggingRuntimeIdentity` stable : `application_id` + timestamp UTC de démarrage + PID. `ksp-logging-lib` utilise cette identité pour préfixer les noms des fichiers actifs et la conserve pendant tous les hot reloads du même processus. Deux lancements distincts ne partagent donc plus le même fichier persistant, même avec une rotation `daily`. Exemple d'un prefix effectif : `ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log`. Le path Config reste inchangé ; l'identité appartient au runtime, pas au document source. ## Traçabilité frontend diff --git a/crates/ksp-app-config-desk/TODO.md b/crates/ksp-app-config-desk/TODO.md index a603f91..03fe2f1 100644 --- a/crates/ksp-app-config-desk/TODO.md +++ b/crates/ksp-app-config-desk/TODO.md @@ -1,13 +1,12 @@ - + # TODO `ksp-app-config-desk` -Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging et son hot reload transactionnel sont maintenant présents. +Le gabarit frontend, le bootstrap Config/Logging, `AppState`, le `LoggingGuard` durable, les premiers DTO TS-RS, le bridge frontend logging KSP, le lifecycle `splash -> main`, les panneaux Documents, Profils/provenance et Environnement avec management `.env`/reveal Secret privilégié, ainsi que l'édition typée/persistence atomique du document Logging, son hot reload transactionnel, la sélection explicite du profil runtime et les fichiers isolés par identité de lancement sont maintenant présents. Les prochaines tranches doivent encore : -- compléter le runtime Logging par les fichiers uniques par lancement (`app-name..`) et la sélection explicite du profil à appliquer ; - ramener le target applicatif de `debug` à `info` ou `warn` avant clôture de la release ; -- démontrer le hot reload Logging avec le panneau de test de routage ; +- démontrer le routing `trace/debug/info/warn/error`, targets et domains avant/après hot reload avec le panneau Test Logging ; - étudier puis intégrer le retour Rust -> console WebKit via `tauri_plugin_tracing::WebviewLayer` dans le subscriber possédé par `ksp-logging-lib`, afin de pouvoir utiliser `attachConsole()` sans second subscriber, double émission ni boucle avec le bridge KSP. diff --git a/crates/ksp-app-config-desk/USAGE.md b/crates/ksp-app-config-desk/USAGE.md index 318e72b..32f707d 100644 --- a/crates/ksp-app-config-desk/USAGE.md +++ b/crates/ksp-app-config-desk/USAGE.md @@ -1,11 +1,11 @@ - + # Utilisation de `ksp-app-config-desk` ## État actuel -Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique et un hot reload runtime sans redémarrage. +Le gabarit Rust/Tauri et le frontend Vite/TypeScript/SCSS sont présents. Le backend initialise Config, le runtime Logging et `AppState`. Les panneaux Documents, Profils et Environnement/`.env` sont fonctionnels, y compris les mutations `.env` create/update/remove et les reveals Secrets privilégiés. Logging expose maintenant un brouillon typé éditable, une persistence atomique, un hot reload sans redémarrage, un profil runtime sélectionnable indépendamment du `default_profile` et une identité de lancement observable pour isoler les fichiers persistants. La fenêtre `splash` est visible au démarrage. Après readiness du frontend et temporisation résolue par Config, l'application effectue le fade-out, affiche/focalise `main` puis détruit `splash`. La fenêtre principale expose immédiatement la navigation monofenêtre de référence. @@ -212,3 +212,51 @@ La vue **Logging** appelle `get_logging_document`. Le backend charge le document Le sélecteur **Profil édité** travaille sur un brouillon local. `logs_directory`, `default_profile`, profils, console, file sinks, filtres et target overrides sont modifiables. **Créer**, **Cloner**, **Renommer** et **Supprimer** agissent d'abord sur le brouillon ; la suppression de profil est confirmée par modal Bootstrap. **Sauvegarder et appliquer** envoie un candidat typé à `save_logging_document`, qui reconstruit les contrats Config, persiste atomiquement après validation, recharge un `ConfigEnvironment` frais, résout le `default_profile` et hot-reload le `LoggingGuard`. Si le runtime ne peut pas être préparé/rechargé, l'ancien runtime reste actif et la source précédente est restaurée. **Recharger le document** resynchronise uniquement le brouillon depuis la source persistée et demande confirmation si des changements non sauvegardés existent. Les messages Cargo/Tauri/Vite affichés par `cargo tauri dev` sont externes au runtime Logging KSP et ne dépendent pas de `console.enabled`. + + +## Runtime Logging — profil actif, génération et fichiers par lancement + +La section **Runtime actif** du panneau Logging ne décrit pas le brouillon : elle interroge l'état réellement installé dans `ksp-logging-lib`. + +Elle expose : + +- `active_profile` ; +- `selection_source` : `default_profile`, `explicit` ou `fallback` ; +- `generation` ; +- console active/inactive ; +- compteurs cumulés de lignes abandonnées ; +- `application_id` ; +- `launch_timestamp` ; +- chaque file sink actif avec son directory, son prefix effectif et sa rotation. + +Au démarrage de Config Desk, l'identité de lancement est construite une seule fois avec un token de la forme : + +```text +20260816-182519.123Z-p4242 +``` + +Pour un fichier source configuré comme `debug/ksp-debug.log`, le runtime peut donc exposer un prefix comme : + +```text +ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log +``` + +Le même `launch_timestamp` doit rester affiché après chaque hot reload du processus courant. Après fermeture puis nouveau lancement de l'application, il doit changer. La rotation `daily` ou `hourly` peut ajouter sa composante de rotation, mais deux lancements ne doivent plus partager le même prefix applicatif. + +### Sélection explicite du profil runtime + +`default_profile` reste une propriété persistée de `std.logging.json`. Le sélecteur **Profil runtime à appliquer** travaille uniquement avec les profils déjà persistés et ne modifie pas ce champ. + +Scénario attendu avec deux profils persistés `local_dev` et `local_test` : + +1. garder `default_profile=local_dev` ; +2. sélectionner `local_test` dans **Profil runtime à appliquer** ; +3. cliquer **Appliquer le profil** ; +4. vérifier `active_profile=local_test`, `selection_source=explicit` et `generation + 1` ; +5. cliquer **Recharger le document** : `default_profile` doit toujours être `local_dev` ; +6. sélectionner/appliquer `local_dev` explicitement si souhaité ; +7. **Sauvegarder et appliquer** un document valide : le runtime revient au `default_profile` du document et `selection_source=default_profile`. + +Le bouton d'application explicite est désactivé lorsque le brouillon est sale afin qu'un profil affiché mais non persisté ne soit jamais confondu avec un profil réellement chargeable par Config. + +Si la résolution ou la préparation du profil explicite échoue, `ksp_logging_lib::reinitialize()` ne remplace pas les layers actifs et la génération ne doit pas avancer. Une sauvegarde de document qui échoue pendant l'application runtime conserve également l'ancien runtime et restaure la source précédente lorsqu'elle avait été modifiée. diff --git a/crates/ksp-app-config-desk/frontend/main.html b/crates/ksp-app-config-desk/frontend/main.html index 6d2397e..d4284f8 100644 --- a/crates/ksp-app-config-desk/frontend/main.html +++ b/crates/ksp-app-config-desk/frontend/main.html @@ -1,5 +1,5 @@ - + @@ -310,7 +310,7 @@ @@ -324,6 +324,42 @@
+
+
+
+

Runtime actif

+

Le profil runtime peut être appliqué explicitement sans modifier default_profile. L'identité de lancement reste stable pendant tous les hot reloads du processus.

+
+ +
+ +
+
+ + +
Disponible uniquement lorsque le brouillon est synchronisé avec la source persistée.
+
+
+ +
+
+
+ + + +
output_idRépertoirePréfixe fichier effectifRotation
+
+
+

Globals

@@ -438,7 +474,7 @@
- Runtime actif : Sauvegarder et appliquer persiste le candidat puis hot-reload immédiatement le subscriber KSP avec un environnement frais. En cas d'échec d'application, l'ancien runtime reste actif et la source précédente est restaurée. Recharger le document resynchronise uniquement le brouillon. + Runtime actif : Sauvegarder et appliquer persiste le candidat puis applique son default_profile. Appliquer ce profil au runtime sélectionne un profil persisté sans modifier le document. En cas d'échec, l'ancien runtime et sa génération restent actifs. Recharger le document resynchronise uniquement le brouillon.
Aucune sauvegarde/application effectuée pendant ce lancement.
Chargement du document Logging typé...
diff --git a/crates/ksp-app-config-desk/frontend/ts/logging.ts b/crates/ksp-app-config-desk/frontend/ts/logging.ts index 36f7fa0..5f209b0 100644 --- a/crates/ksp-app-config-desk/frontend/ts/logging.ts +++ b/crates/ksp-app-config-desk/frontend/ts/logging.ts @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/frontend/ts/logging.ts -// version: 4 +// version: 5 //! Typed Logging editor backed by ConfigManagement persistence and KSP-owned runtime hot reload. @@ -10,6 +10,7 @@ import type { LoggingDocumentSaveResultDto } from "./bindings/ksp_app_config_des import type { LoggingFileDto } from "./bindings/ksp_app_config_desk/logging/LoggingFileDto"; import type { LoggingProfileDto } from "./bindings/ksp_app_config_desk/logging/LoggingProfileDto"; import type { LoggingTargetFilterDto } from "./bindings/ksp_app_config_desk/logging/LoggingTargetFilterDto"; +import type { LoggingRuntimeStatusDto } from "./bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto"; import { frontendDebug, frontendTrace } from "./frontend_log"; import { invokeKsp } from "./invoke"; @@ -18,6 +19,7 @@ const ROTATIONS = ["never", "hourly", "daily"] as const; const FORMATS = ["human", "compact", "pretty", "json"] as const; let draft: LoggingDocumentCandidateDto | null = null; +let persistedDocument: LoggingDocumentDto | null = null; let selectedProfileId = ""; let dirty = false; @@ -104,6 +106,7 @@ function markDirty(reason: string): void { saveButton.disabled = false; } setLoggingStatus("Brouillon modifié. Sauvegarde nécessaire pour persister std.logging.json.", "warning"); + updateRuntimeActionState(); frontendTrace("main", "Logging draft changed", { reason, selectedProfileId }); } @@ -295,7 +298,130 @@ function renderSelectedProfile(source: "load" | "selection" | "mutation"): void }); } +function updateRuntimeActionState(): void { + const select = document.querySelector("#loggingRuntimeProfileSelect"); + const apply = document.querySelector("#applyLoggingRuntimeProfile"); + if (select) { + select.disabled = persistedDocument === null || dirty; + } + if (apply) { + apply.disabled = persistedDocument === null || dirty || !select || select.value.length === 0; + } +} + +function renderRuntimeProfileSelect(): void { + const select = document.querySelector("#loggingRuntimeProfileSelect"); + if (!select || !persistedDocument) { + updateRuntimeActionState(); + return; + } + const previous = select.value; + select.replaceChildren(); + for (const profile of persistedDocument.profiles) { + const option = document.createElement("option"); + option.value = profile.profileId; + option.textContent = profile.profileId === persistedDocument.defaultProfile ? `${profile.profileId} (default)` : profile.profileId; + select.append(option); + } + if (persistedDocument.profiles.some(profile => profile.profileId === previous)) { + select.value = previous; + } else { + select.value = persistedDocument.defaultProfile; + } + updateRuntimeActionState(); +} + +function renderRuntimeFiles(status: LoggingRuntimeStatusDto): void { + const body = document.querySelector("#loggingRuntimeFilesTable tbody"); + if (!body) { + return; + } + body.replaceChildren(); + for (const file of status.files) { + const row = document.createElement("tr"); + const values: Array<{ value: string; monospace: boolean }> = [ + { value: file.outputId, monospace: true }, + { value: file.directory, monospace: true }, + { value: file.fileNamePrefix, monospace: true }, + { value: file.rotation, monospace: false }, + ]; + for (const item of values) { + const cell = document.createElement("td"); + cell.textContent = item.value; + if (item.monospace) { + cell.classList.add("font-monospace"); + } + row.append(cell); + } + body.append(row); + } +} + +function renderRuntimeStatus(status: LoggingRuntimeStatusDto): void { + setText("#loggingRuntimeActiveProfile", status.activeProfile ?? "fallback transitoire"); + setText("#loggingRuntimeSelectionSource", status.selectionSource); + setText("#loggingRuntimeGeneration", status.generation.toString()); + setText("#loggingRuntimeFallback", status.fallbackActive ? "oui" : "non"); + setText("#loggingRuntimeApplicationId", status.applicationId); + setText("#loggingRuntimeLaunchTimestamp", status.launchTimestamp); + setText("#loggingRuntimeConsole", status.consoleEnabled ? "active" : "désactivée"); + setText("#loggingRuntimeDropped", `${status.droppedTotalLines} total · console=${status.droppedConsoleLines} · fichiers=${status.droppedFileLines}`); + renderRuntimeFiles(status); + const select = document.querySelector("#loggingRuntimeProfileSelect"); + if (select && status.activeProfile && persistedDocument?.profiles.some(profile => profile.profileId === status.activeProfile)) { + select.value = status.activeProfile; + } + updateRuntimeActionState(); + frontendTrace("main", "Logging runtime metadata rendered", { + generation: status.generation, + selectionSource: status.selectionSource, + fileCount: status.files.length, + consoleEnabled: status.consoleEnabled, + }); +} + +async function refreshRuntimeStatus(source: "startup" | "save" | "explicit" | "user"): Promise { + if (source === "user") { + frontendDebug("main", "Logging runtime metadata refresh requested"); + } + try { + const status = await invokeKsp("main", "get_logging_runtime_status"); + renderRuntimeStatus(status); + frontendDebug("main", "Logging runtime metadata refresh completed", { generation: status.generation, activeProfile: status.activeProfile }); + } catch { + setText("#loggingRuntimeActiveProfile", "indisponible"); + } +} + +async function applyRuntimeProfile(): Promise { + if (dirty) { + setLoggingStatus("Sauvegarde ou abandonne d'abord le brouillon avant d'appliquer un profil runtime explicite.", "warning"); + return; + } + const profileId = selectedValue("#loggingRuntimeProfileSelect"); + if (profileId.length === 0) { + return; + } + const apply = document.querySelector("#applyLoggingRuntimeProfile"); + if (apply) { + apply.disabled = true; + } + setLoggingStatus(`Application explicite du profil runtime ${profileId}...`); + frontendDebug("main", "Explicit Logging runtime profile apply requested", { profileId }); + try { + const status = await invokeKsp("main", "apply_logging_profile", { profileId }); + renderRuntimeStatus(status); + window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated")); + setLoggingStatus(`Profil ${profileId} appliqué au runtime sans modifier default_profile. Génération ${status.generation}.`, "success"); + frontendDebug("main", "Explicit Logging runtime profile apply completed", { profileId, generation: status.generation }); + } catch { + updateRuntimeActionState(); + setLoggingStatus("Application explicite refusée. Le runtime précédent et sa génération sont conservés.", "danger"); + } +} + function renderDocument(documentDto: LoggingDocumentDto): void { + persistedDocument = structuredClone(documentDto); draft = { logsDirectory: documentDto.logsDirectory, defaultProfile: documentDto.defaultProfile, @@ -311,6 +437,7 @@ function renderDocument(documentDto: LoggingDocumentDto): void { setText("#loggingFormatVersion", documentDto.formatVersion.toString()); setInputValue("#loggingLogsDirectoryInput", draft.logsDirectory); renderProfileSelects(); + renderRuntimeProfileSelect(); renderSelectedProfile("load"); setText("#loggingSaveReport", "Aucune sauvegarde effectuée depuis le dernier chargement."); frontendTrace("main", "Logging document editor rendered", { profileCount: documentDto.profiles.length }); @@ -512,6 +639,8 @@ async function saveLoggingDocument(): Promise { try { const result = await invokeKsp("main", "save_logging_document", { candidate: draft }); renderDocument(result.document); + await refreshRuntimeStatus("save"); + window.dispatchEvent(new CustomEvent("ksp:logging-runtime-updated")); setText( "#loggingSaveReport", `source_changed=${result.sourceChanged} · reload_required=${result.reloadRequired} · runtime_applied=${result.runtimeApplied} · generation=${result.loggingGeneration} · active_profile=${result.activeProfile}`, @@ -576,9 +705,12 @@ async function refreshLoggingDocument(source: "startup" | "user"): Promise profileCount: documentDto.profiles.length, defaultProfile: documentDto.defaultProfile, }); + await refreshRuntimeStatus(source === "startup" ? "startup" : "user"); } catch { draft = null; + persistedDocument = null; setEditorEnabled(false); + updateRuntimeActionState(); setLoggingStatus("Le document Logging typé n'a pas pu être chargé. Consulte les diagnostics backend.", "danger"); } } @@ -623,6 +755,9 @@ export function initializeLoggingPanel(): void { bindStaticDraftFields(); document.querySelector("#refreshLoggingDocument")?.addEventListener("click", requestReload); document.querySelector("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument()); + document.querySelector("#refreshLoggingRuntime")?.addEventListener("click", () => void refreshRuntimeStatus("user")); + document.querySelector("#applyLoggingRuntimeProfile")?.addEventListener("click", () => void applyRuntimeProfile()); + document.querySelector("#loggingRuntimeProfileSelect")?.addEventListener("change", updateRuntimeActionState); document.querySelector("#loggingProfileSelect")?.addEventListener("change", event => { syncStaticProfileFields(); selectedProfileId = (event.currentTarget as HTMLSelectElement).value; diff --git a/crates/ksp-app-config-desk/frontend/ts/main.ts b/crates/ksp-app-config-desk/frontend/ts/main.ts index 7298726..5ffe28d 100644 --- a/crates/ksp-app-config-desk/frontend/ts/main.ts +++ b/crates/ksp-app-config-desk/frontend/ts/main.ts @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/frontend/ts/main.ts -// version: 10 +// version: 11 import "bootstrap"; import ResizeObserver from "resize-observer-polyfill"; @@ -146,10 +146,18 @@ async function loadSnapshot(): Promise { frontendTrace("main", "Main shell status replaced", { status: "ready" }); } +function bindRuntimeSnapshotRefresh(): void { + window.addEventListener("ksp:logging-runtime-updated", () => { + frontendDebug("main", "Application snapshot refresh requested after Logging runtime update"); + void loadSnapshot(); + }); +} + async function initializeMain(): Promise { const windowLabel = getCurrentWindow().label; frontendInfo("main", "Config Desk main frontend loaded", { windowLabel }); bindNavigation(); + bindRuntimeSnapshotRefresh(); initializeDocumentsPanel(); initializeProfilesPanel(); initializeEnvironmentPanel(); diff --git a/crates/ksp-app-config-desk/src/app_state.rs b/crates/ksp-app-config-desk/src/app_state.rs index 20b5272..1e88efe 100644 --- a/crates/ksp-app-config-desk/src/app_state.rs +++ b/crates/ksp-app-config-desk/src/app_state.rs @@ -1,11 +1,12 @@ // file: crates/ksp-app-config-desk/src/app_state.rs -// version: 5 +// version: 6 //! Shared backend state owned by the Tauri application. struct LoggingRuntimeState { guard: ksp_logging_lib::LoggingGuard, active_profile_id: std::option::Option, + selection_source: String, generation: u32, fallback_active: bool, startup_diagnostic: std::option::Option, @@ -27,7 +28,12 @@ impl AppState { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), }; - let logging_startup = crate::initialize_logging(&config_management); + let runtime_identity = crate::logging_runtime::launch_identity(); + let runtime_identity = match runtime_identity { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let logging_startup = crate::initialize_logging(&config_management, &runtime_identity); let logging_startup = match logging_startup { std::result::Result::Ok(value) => value, std::result::Result::Err(error) => return std::result::Result::Err(error), @@ -61,6 +67,7 @@ impl AppState { logging_runtime: std::sync::Mutex::new(LoggingRuntimeState { guard: logging_startup.guard, active_profile_id: logging_startup.active_profile_id, + selection_source: logging_startup.selection_source, generation: 1, fallback_active: logging_startup.fallback_active, startup_diagnostic: logging_startup.startup_diagnostic, @@ -111,7 +118,12 @@ impl AppState { } /// Replaces the active Logging runtime with freshly resolved settings and advances the runtime generation only after success. - pub(crate) fn reinitialize_logging_runtime(&self, profile_id: &str, settings: &ksp_logging_lib::LoggingSettings) -> ksp_core_lib::Result { + pub(crate) fn reinitialize_logging_runtime( + &self, + profile_id: &str, + selection_source: &str, + settings: &ksp_logging_lib::LoggingSettings, + ) -> ksp_core_lib::Result { let runtime = self.logging_runtime.lock(); let mut runtime = match runtime { std::result::Result::Ok(value) => value, @@ -127,12 +139,59 @@ impl AppState { return std::result::Result::Err(error); } runtime.active_profile_id = std::option::Option::Some(profile_id.to_owned()); + runtime.selection_source = selection_source.to_owned(); runtime.generation = runtime.generation.saturating_add(1); runtime.fallback_active = false; runtime.startup_diagnostic = std::option::Option::None; return std::result::Result::Ok(runtime.generation); } + /// Returns a safe observable snapshot of the currently active Logging runtime. + pub(crate) fn logging_runtime_status(&self) -> ksp_core_lib::Result { + let runtime = self.logging_runtime.lock(); + let runtime = match runtime { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => { + return std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_APP_STATE_LOCK_FAILED, + "Config Desk Logging runtime state lock is poisoned", + )); + }, + }; + let identity = runtime.guard.runtime_identity(); + let identity = match identity { + std::option::Option::Some(value) => value, + std::option::Option::None => { + return std::result::Result::Err(ksp_core_lib::Error::new( + crate::ERROR_CODE_APP_STATE_INVALID, + "Config Desk Logging runtime is missing its application launch identity", + )); + }, + }; + let console_enabled = match runtime.guard.settings().console() { + std::option::Option::Some(console) => console.enabled(), + std::option::Option::None => false, + }; + let dropped = runtime.guard.dropped_lines(); + let mut files = std::vec::Vec::::new(); + for metadata in runtime.guard.active_file_outputs() { + files.push(crate::logging_runtime::project_file(&metadata)); + } + return std::result::Result::Ok(crate::LoggingRuntimeStatusDto { + active_profile: runtime.active_profile_id.clone(), + selection_source: runtime.selection_source.clone(), + generation: runtime.generation, + fallback_active: runtime.fallback_active, + application_id: identity.application_id().to_owned(), + launch_timestamp: identity.launch_timestamp().to_owned(), + console_enabled, + dropped_console_lines: crate::logging_runtime::count_to_u64(dropped.console()), + dropped_file_lines: crate::logging_runtime::count_to_u64(dropped.file()), + dropped_total_lines: crate::logging_runtime::count_to_u64(dropped.total()), + files, + }); + } + /// Returns the resolved splash timings captured during application bootstrap. #[must_use] pub(crate) const fn splash_settings(&self) -> crate::SplashSettings { diff --git a/crates/ksp-app-config-desk/src/bootstrap.rs b/crates/ksp-app-config-desk/src/bootstrap.rs index 0e5095d..21d64c7 100644 --- a/crates/ksp-app-config-desk/src/bootstrap.rs +++ b/crates/ksp-app-config-desk/src/bootstrap.rs @@ -1,11 +1,12 @@ // file: crates/ksp-app-config-desk/src/bootstrap.rs -// version: 1 +// version: 2 //! Config and Logging bootstrap for the desktop application. pub(crate) struct LoggingStartup { pub(crate) guard: ksp_logging_lib::LoggingGuard, pub(crate) active_profile_id: std::option::Option, + pub(crate) selection_source: String, pub(crate) fallback_active: bool, pub(crate) startup_diagnostic: std::option::Option, } @@ -25,20 +26,23 @@ pub(crate) fn config_management(arguments: &[std::ffi::OsString]) -> ksp_core_li return std::result::Result::Ok(ksp_config_lib::ConfigManagement::new(engine)); } -pub(crate) fn initialize_logging(management: &ksp_config_lib::ConfigManagement) -> ksp_core_lib::Result { +pub(crate) fn initialize_logging( + management: &ksp_config_lib::ConfigManagement, + runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, +) -> ksp_core_lib::Result { let environment = ksp_config_lib::ConfigEnvironment::load(); let environment = match environment { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return initialize_fallback_logging(error), + std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity), }; let resolved = management.engine().load_resolved_logging_config(std::option::Option::None, &environment); let resolved = match resolved { std::result::Result::Ok(value) => value, - std::result::Result::Err(error) => return initialize_fallback_logging(error), + std::result::Result::Err(error) => return initialize_fallback_logging(error, runtime_identity), }; let active_profile_id = resolved.profile_id().to_owned(); let settings = resolved.into_settings(); - let guard = ksp_logging_lib::initialize(&settings); + let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); return match guard { std::result::Result::Ok(guard) => { ksp_logging_lib::info!( @@ -50,18 +54,22 @@ pub(crate) fn initialize_logging(management: &ksp_config_lib::ConfigManagement) std::result::Result::Ok(LoggingStartup { guard, active_profile_id: std::option::Option::Some(active_profile_id), + selection_source: "default_profile".to_owned(), fallback_active: false, startup_diagnostic: std::option::Option::None, }) }, - std::result::Result::Err(error) => initialize_fallback_logging(error), + std::result::Result::Err(error) => initialize_fallback_logging(error, runtime_identity), }; } -fn initialize_fallback_logging(initial_error: ksp_core_lib::Error) -> ksp_core_lib::Result { +fn initialize_fallback_logging( + initial_error: ksp_core_lib::Error, + runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity, +) -> ksp_core_lib::Result { let diagnostic = crate::CommandErrorDto::from_error(&initial_error); let settings = fallback_logging_settings(); - let guard = ksp_logging_lib::initialize(&settings); + let guard = ksp_logging_lib::initialize_with_identity(&settings, runtime_identity); let guard = match guard { std::result::Result::Ok(value) => value, std::result::Result::Err(fallback_error) => { @@ -83,6 +91,7 @@ fn initialize_fallback_logging(initial_error: ksp_core_lib::Error) -> ksp_core_l return std::result::Result::Ok(LoggingStartup { guard, active_profile_id: std::option::Option::None, + selection_source: "fallback".to_owned(), fallback_active: true, startup_diagnostic: std::option::Option::Some(diagnostic), }); diff --git a/crates/ksp-app-config-desk/src/constants.rs b/crates/ksp-app-config-desk/src/constants.rs index 6521dad..111d2b1 100644 --- a/crates/ksp-app-config-desk/src/constants.rs +++ b/crates/ksp-app-config-desk/src/constants.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/constants.rs -// version: 7 +// version: 8 //! Application-owned tracing targets and domains. @@ -27,3 +27,5 @@ pub(crate) const TRACING_DOMAIN_ENVIRONMENT: &str = "config.environment"; pub(crate) const TRACING_DOMAIN_SECRETS: &str = "config.secrets"; /// Structured domain for typed Logging editor inspection. pub(crate) const TRACING_DOMAIN_LOGGING_EDITOR: &str = "config.logging_editor"; +/// Structured domain for active Logging runtime metadata and explicit profile application. +pub(crate) const TRACING_DOMAIN_LOGGING_RUNTIME: &str = "config.logging_runtime"; diff --git a/crates/ksp-app-config-desk/src/lib.rs b/crates/ksp-app-config-desk/src/lib.rs index ea443b3..df6aa88 100644 --- a/crates/ksp-app-config-desk/src/lib.rs +++ b/crates/ksp-app-config-desk/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/lib.rs -// version: 12 +// version: 13 //! Tauri desktop application for managing and validating KSP configuration. @@ -16,6 +16,7 @@ mod environment; mod errors; mod frontend_logging; mod logging_editor; +mod logging_runtime; mod profiles; mod secrets; mod splash; @@ -34,6 +35,7 @@ pub(crate) use self::constants::TRACING_DOMAIN_DOCUMENTS; pub(crate) use self::constants::TRACING_DOMAIN_ENVIRONMENT; pub(crate) use self::constants::TRACING_DOMAIN_FRONTEND; pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_EDITOR; +pub(crate) use self::constants::TRACING_DOMAIN_LOGGING_RUNTIME; pub(crate) use self::constants::TRACING_DOMAIN_PROFILES; pub(crate) use self::constants::TRACING_DOMAIN_SECRETS; pub(crate) use self::constants::TRACING_DOMAIN_WINDOWS; @@ -70,6 +72,7 @@ pub(crate) use self::frontend_logging::emit_frontend_log_event; pub(crate) use self::logging_editor::LoggingDocumentCandidateDto; pub(crate) use self::logging_editor::LoggingDocumentDto; pub(crate) use self::logging_editor::LoggingDocumentSaveResultDto; +pub(crate) use self::logging_runtime::{LoggingRuntimeFileDto, LoggingRuntimeStatusDto}; pub(crate) use self::profiles::ConfigProfileDetailDto; pub(crate) use self::profiles::ConfigProfileDocumentDto; pub(crate) use self::secrets::SecretRevealRequestDto; diff --git a/crates/ksp-app-config-desk/src/logging_editor.rs b/crates/ksp-app-config-desk/src/logging_editor.rs index 169f48f..d16e820 100644 --- a/crates/ksp-app-config-desk/src/logging_editor.rs +++ b/crates/ksp-app-config-desk/src/logging_editor.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/logging_editor.rs -// version: 3 +// version: 4 //! Typed Logging document projection and validated persistence for the Config Desk Logging editor. @@ -207,7 +207,7 @@ fn apply_persisted_runtime(state: &crate::AppState) -> ksp_core_lib::Result<(Str }; let profile_id = resolved.profile_id().to_owned(); let settings = resolved.into_settings(); - let generation = state.reinitialize_logging_runtime(profile_id.as_str(), &settings); + let generation = state.reinitialize_logging_runtime(profile_id.as_str(), "default_profile", &settings); return match generation { std::result::Result::Ok(value) => std::result::Result::Ok((profile_id, value)), std::result::Result::Err(error) => std::result::Result::Err(error), diff --git a/crates/ksp-app-config-desk/src/logging_runtime.rs b/crates/ksp-app-config-desk/src/logging_runtime.rs new file mode 100644 index 0000000..1efce31 --- /dev/null +++ b/crates/ksp-app-config-desk/src/logging_runtime.rs @@ -0,0 +1,130 @@ +// file: crates/ksp-app-config-desk/src/logging_runtime.rs +// version: 1 + +//! Runtime Logging metadata, launch identity and explicit profile application for Config Desk. + +use ts_rs::TS; // rust-rules: derive-import + +/// Metadata for one currently active persistent Logging file output. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeFileDto.ts")] +pub(crate) struct LoggingRuntimeFileDto { + /// Stable configured output identifier. + pub(crate) output_id: String, + /// Resolved runtime directory. + pub(crate) directory: String, + /// Effective per-launch filename prefix passed to the rolling appender. + pub(crate) file_name_prefix: String, + /// Effective file rotation cadence. + pub(crate) rotation: String, +} + +/// Safe observable state of the currently active KSP Logging runtime. +#[derive(Clone, Debug, serde::Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export, export_to = "../frontend/ts/bindings/ksp_app_config_desk/logging_runtime/LoggingRuntimeStatusDto.ts")] +pub(crate) struct LoggingRuntimeStatusDto { + /// Active profile identifier, or `None` while fallback Logging is active. + pub(crate) active_profile: std::option::Option, + /// Source that selected the active runtime profile. + pub(crate) selection_source: String, + /// Monotonic runtime generation. + pub(crate) generation: u32, + /// Whether transient fallback Logging is active. + pub(crate) fallback_active: bool, + /// Stable application identifier embedded in launch-specific file names. + pub(crate) application_id: String, + /// Stable launch timestamp token reused by all hot reloads in this process. + pub(crate) launch_timestamp: String, + /// Whether the currently active console sink is enabled. + pub(crate) console_enabled: bool, + /// Cumulative console lines dropped by non-blocking outputs. + pub(crate) dropped_console_lines: u64, + /// Cumulative file lines dropped by non-blocking outputs. + pub(crate) dropped_file_lines: u64, + /// Cumulative lines dropped across all outputs. + pub(crate) dropped_total_lines: u64, + /// Active persistent file outputs and their launch-specific prefixes. + pub(crate) files: std::vec::Vec, +} + +/// Creates the stable runtime identity for this Config Desk process launch. +pub(crate) fn launch_identity() -> ksp_core_lib::Result { + let now = time::OffsetDateTime::now_utc(); + let milliseconds = now.nanosecond() / 1_000_000; + let timestamp = format!( + "{:04}{:02}{:02}-{:02}{:02}{:02}.{:03}Z-p{}", + now.year(), + now.month() as u8, + now.day(), + now.hour(), + now.minute(), + now.second(), + milliseconds, + std::process::id(), + ); + return ksp_logging_lib::LoggingRuntimeIdentity::new(crate::TRACING_TARGET, timestamp); +} + +/// Returns safe metadata for the currently active Logging runtime. +pub(crate) fn status(state: &crate::AppState) -> ksp_core_lib::Result { + return state.logging_runtime_status(); +} + +/// Applies one persisted Logging profile explicitly without changing `default_profile` or the source document. +pub(crate) fn apply_profile(state: &crate::AppState, profile_id: &str) -> ksp_core_lib::Result { + let environment = ksp_config_lib::ConfigEnvironment::load(); + let environment = match environment { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let resolved = state.config_management().engine().load_resolved_logging_config(std::option::Option::Some(profile_id), &environment); + let resolved = match resolved { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), + }; + let active_profile = resolved.profile_id().to_owned(); + let settings = resolved.into_settings(); + let generation = state.reinitialize_logging_runtime(active_profile.as_str(), "explicit", &settings); + if let std::result::Result::Err(error) = generation { + return std::result::Result::Err(error); + } + ksp_logging_lib::debug!( + target: crate::TRACING_TARGET, + domain = crate::TRACING_DOMAIN_LOGGING_RUNTIME, + active_profile = active_profile.as_str(), + selection_source = "explicit", + "persisted Logging profile applied explicitly to runtime" + ); + return state.logging_runtime_status(); +} + +pub(crate) fn project_file(metadata: &ksp_logging_lib::RuntimeFileMetadata) -> LoggingRuntimeFileDto { + return LoggingRuntimeFileDto { + output_id: metadata.output_id().to_owned(), + directory: metadata.directory().display().to_string(), + file_name_prefix: metadata.file_name_prefix().to_owned(), + rotation: rotation_label(metadata.rotation()).to_owned(), + }; +} + +pub(crate) const fn rotation_label(rotation: ksp_logging_lib::FileRotation) -> &'static str { + return match rotation { + ksp_logging_lib::FileRotation::Never => "never", + ksp_logging_lib::FileRotation::Hourly => "hourly", + ksp_logging_lib::FileRotation::Daily => "daily", + }; +} + +pub(crate) fn count_to_u64(value: usize) -> u64 { + let converted = u64::try_from(value); + return match converted { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => u64::MAX, + }; +} + +#[cfg(test)] +#[path = "../unit_tests/logging_runtime.rs"] +mod tests; diff --git a/crates/ksp-app-config-desk/src/tauri.rs b/crates/ksp-app-config-desk/src/tauri.rs index b70c700..35fef0e 100644 --- a/crates/ksp-app-config-desk/src/tauri.rs +++ b/crates/ksp-app-config-desk/src/tauri.rs @@ -1,5 +1,5 @@ // file: crates/ksp-app-config-desk/src/tauri.rs -// version: 13 +// version: 14 //! Tauri runtime assembly for the KSP configuration desktop application. @@ -49,6 +49,8 @@ fn configure_commands(builder: tauri::Builder) -> tauri::Builder) -> std::result }; } +#[tauri::command] +fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result { + let result = crate::logging_runtime::status(&state); + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), + }; +} + +#[tauri::command] +fn apply_logging_profile( + profile_id: String, + state: tauri::State<'_, crate::AppState>, +) -> std::result::Result { + let result = crate::logging_runtime::apply_profile(&state, profile_id.as_str()); + return match result { + std::result::Result::Ok(value) => std::result::Result::Ok(value), + std::result::Result::Err(error) => std::result::Result::Err(crate::CommandErrorDto::from_error(&error)), + }; +} + #[tauri::command] fn save_logging_document( candidate: crate::LoggingDocumentCandidateDto, diff --git a/crates/ksp-app-config-desk/unit_tests/logging_runtime.rs b/crates/ksp-app-config-desk/unit_tests/logging_runtime.rs new file mode 100644 index 0000000..7bdd5d0 --- /dev/null +++ b/crates/ksp-app-config-desk/unit_tests/logging_runtime.rs @@ -0,0 +1,26 @@ +// file: crates/ksp-app-config-desk/unit_tests/logging_runtime.rs +// version: 1 + +#[test] +fn launch_identity_is_safe_and_contains_application_and_process_identity() { + let identity = super::launch_identity(); + assert!(identity.is_ok()); + if let std::result::Result::Ok(identity) = identity { + assert_eq!(identity.application_id(), crate::TRACING_TARGET); + assert!(identity.launch_timestamp().contains("Z-p")); + assert!(identity.launch_timestamp().ends_with(std::process::id().to_string().as_str())); + } +} + +#[test] +fn runtime_rotation_labels_are_stable() { + assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Never), "never"); + assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Hourly), "hourly"); + assert_eq!(super::rotation_label(ksp_logging_lib::FileRotation::Daily), "daily"); +} + +#[test] +fn runtime_count_projection_is_non_lossy_for_normal_values() { + assert_eq!(super::count_to_u64(0), 0); + assert_eq!(super::count_to_u64(42), 42); +} diff --git a/crates/ksp-logging-lib/README.md b/crates/ksp-logging-lib/README.md index 37a5ea7..c0ec351 100644 --- a/crates/ksp-logging-lib/README.md +++ b/crates/ksp-logging-lib/README.md @@ -1,5 +1,5 @@ - + # ksp-logging-lib @@ -18,6 +18,8 @@ La crate possède : - l'héritage du `domain` effectif à travers les spans, avec possibilité pour un event ou un span enfant de le remplacer explicitement ; - l'installation unique du subscriber global ; - le hot reload via `reinitialize` sans second subscriber global ; +- `LoggingRuntimeIdentity` pour isoler les fichiers persistants par lancement sans muter les `FileSettings` source ; +- l'observabilité des file sinks actifs via `RuntimeFileMetadata`, avec maintien de la même identité à travers les hot reloads ; - le takeover des logs : les targets externes sont silencieux par défaut ; - les writers non bloquants console/fichier et leurs `WorkerGuard` ; - les compteurs agrégés de lignes abandonnées et le compteur cumulatif par `output_id` fichier ; @@ -54,7 +56,7 @@ Une crate KSP comportementale qui journalise son activité dépend de `ksp-loggi Les événements utiles issus d'une dépendance externe ne sont pas renommés : la crate KSP propriétaire de l'opération réémet explicitement l'information utile sous son propre target KSP. -`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config pourra construire un `LoggingSettings` puis appeler `initialize` ou `reinitialize`. +`ksp-logging-lib` ne dépend pas de `ksp-config-lib`. Config peut construire un `LoggingSettings` puis une application appelle `initialize`, `initialize_with_identity` et `reinitialize` selon son besoin. L'identité de lancement reste une responsabilité de la couche application : Logging la valide, la conserve dans le `LoggingGuard` et l'applique aux noms de fichiers actifs. ## Documentation diff --git a/crates/ksp-logging-lib/TODO.md b/crates/ksp-logging-lib/TODO.md index 1f311f6..4f875b1 100644 --- a/crates/ksp-logging-lib/TODO.md +++ b/crates/ksp-logging-lib/TODO.md @@ -1,19 +1,13 @@ - + # TODO ksp-logging-lib -## À fermer pendant `0.1.3` +## État courant -Les trois tranches Logging nécessaires à la fondation Config sont maintenant couvertes fonctionnellement : +La fondation Logging `0.1.2` et ses compléments nécessaires à Config `0.1.3` sont stables. `0.1.4-pre.016` complète le contrat runtime pour les applications desk avec une identité stable par lancement et l'observabilité des file sinks actifs, sans déplacer l'ownership du subscriber hors de `ksp-logging-lib`. -- `pre.004` : contrats/settings multi-output ; -- `pre.005` : runtime multi-sink, formats et routing level/target ; -- `pre.006` : routing structuré `domain`, héritage de spans et lifecycle. - -Avant le gel de `std.logging.schema.json`, il reste uniquement à faire valider `pre.006` par les commandes workspace usuelles. Les travaux suivants de `0.1.3` reviennent ensuite à `ksp-config-lib`. - -## Capacités désormais actives +## Capacités actives Le runtime supporte maintenant : @@ -25,13 +19,17 @@ Le runtime supporte maintenant : - guards non bloquants indépendants ; - compteurs agrégés et compteurs cumulatifs par `output_id` fichier ; - hot reload transactionnel du groupe de sinks ; -- héritage du `domain` effectif pour les events/spans et lifecycle de spans. +- héritage du `domain` effectif pour les events/spans et lifecycle de spans ; +- `LoggingRuntimeIdentity` pour isoler les fichiers persistants entre lancements ; +- maintien automatique de la même identité lors des hot reloads d'un processus ; +- `RuntimeFileMetadata` pour observer les file sinks réellement actifs sans exposer leurs writers. ## Capacités différées -Ces éléments ne sont pas requis par la fondation Config `0.1.3` : +Ces éléments ne sont pas requis pour la clôture de `0.1.4` : - rotation par taille, rétention/compression et symlink `latest` ; - OpenTelemetry/export réseau ; - watcher de fichiers de configuration, qui appartient à Config ou à une couche supérieure ; -- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence. +- benchmark/profiling de précision destiné aux chemins de trading sensibles à la latence ; +- éventuelle couche générique supplémentaire pour transporter des événements runtime vers une WebView, à concevoir sans second subscriber et sans boucle de réémission. diff --git a/crates/ksp-logging-lib/USAGE.md b/crates/ksp-logging-lib/USAGE.md index 5eede8f..d46d1fd 100644 --- a/crates/ksp-logging-lib/USAGE.md +++ b/crates/ksp-logging-lib/USAGE.md @@ -1,5 +1,5 @@ - + # Utilisation de ksp-logging-lib @@ -92,6 +92,52 @@ let mut logging_guard = match initialize_result { Une configuration sans output actif est valide et installe une infrastructure initialement silencieuse qui pourra être activée plus tard par hot reload. + + +## Identité de lancement et isolation des fichiers persistants + +Une application qui utilise des sorties fichiers persistantes peut attacher une identité stable au runtime : + +```rust +let identity = ksp_logging_lib::LoggingRuntimeIdentity::new( + "ksp-app-config-desk", + "20260816-182519.123Z-p4242", +); +let identity = match identity { + std::result::Result::Ok(value) => value, + std::result::Result::Err(error) => return std::result::Result::Err(error), +}; + +let initialize_result = ksp_logging_lib::initialize_with_identity(&settings, &identity); +let mut logging_guard = match initialize_result { + std::result::Result::Ok(guard) => guard, + std::result::Result::Err(error) => return std::result::Result::Err(error), +}; +``` + +Les composants de l'identité n'acceptent que des caractères ASCII alphanumériques et `._-`; les séparateurs de path et espaces sont refusés. + +Si un `FileSettings` configure `ksp-debug.log`, l'identité ci-dessus produit un prefix runtime : + +```text +ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log +``` + +Le `FileSettings` d'origine reste `ksp-debug.log`. `reinitialize(&mut logging_guard, ...)` réutilise automatiquement l'identité stockée dans le guard : un hot reload ne crée donc pas une fausse nouvelle identité de lancement. + +Les outputs réellement actifs sont inspectables sans exposer les writers : + +```rust +for file in logging_guard.active_file_outputs() { + let output_id = file.output_id(); + let directory = file.directory(); + let prefix = file.file_name_prefix(); + let rotation = file.rotation(); +} +``` + +`initialize()` reste disponible pour les consommateurs qui n'ont pas besoin d'identité de lancement. `initialize_with_identity()` est la forme attendue pour les applications KSP qui activent des logs persistants et doivent empêcher la fusion de plusieurs lancements dans le même fichier. + ## Construction d'un runtime multi-output Le runtime peut activer plusieurs sorties ayant des formats et filtres niveau/target/domain distincts : diff --git a/crates/ksp-logging-lib/src/error.rs b/crates/ksp-logging-lib/src/error.rs index c440309..c78a44c 100644 --- a/crates/ksp-logging-lib/src/error.rs +++ b/crates/ksp-logging-lib/src/error.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/error.rs -// version: 3 +// version: 4 /// Error code used when runtime logging settings are invalid. pub const ERROR_CODE_INVALID_SETTINGS: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_settings"); @@ -9,3 +9,5 @@ pub const ERROR_CODE_ALREADY_INITIALIZED: ksp_core_lib::ErrorCode = ksp_core_lib pub const ERROR_CODE_RELOAD_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "reload_failed"); /// Error code used when the rolling file output cannot be initialized. pub const ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "file_output_initialization_failed"); +/// Error code used when an application Logging runtime identity is invalid. +pub const ERROR_CODE_INVALID_RUNTIME_IDENTITY: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("logging", "invalid_runtime_identity"); diff --git a/crates/ksp-logging-lib/src/identity.rs b/crates/ksp-logging-lib/src/identity.rs new file mode 100644 index 0000000..6f79ad1 --- /dev/null +++ b/crates/ksp-logging-lib/src/identity.rs @@ -0,0 +1,71 @@ +// file: crates/ksp-logging-lib/src/identity.rs +// version: 1 + +//! Stable runtime identity used to separate persistent file outputs between application launches. + +/// Identity attached to one installed KSP Logging runtime for the lifetime of an application launch. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct LoggingRuntimeIdentity { + application_id: std::string::String, + launch_timestamp: std::string::String, +} + +impl LoggingRuntimeIdentity { + /// Creates a validated runtime identity from an application identifier and launch timestamp token. + pub fn new( + application_id: impl std::convert::Into, + launch_timestamp: impl std::convert::Into, + ) -> ksp_core_lib::Result { + let application_id = application_id.into(); + let launch_timestamp = launch_timestamp.into(); + let application_validation = validate_identity_component(application_id.as_str(), "application_id"); + if let std::result::Result::Err(error) = application_validation { + return std::result::Result::Err(error); + } + let timestamp_validation = validate_identity_component(launch_timestamp.as_str(), "launch_timestamp"); + if let std::result::Result::Err(error) = timestamp_validation { + return std::result::Result::Err(error); + } + return std::result::Result::Ok(Self { application_id, launch_timestamp }); + } + + /// Returns the application identifier embedded in persistent runtime file names. + #[must_use] + pub fn application_id(&self) -> &str { + return self.application_id.as_str(); + } + + /// Returns the stable launch timestamp token embedded in persistent runtime file names. + #[must_use] + pub fn launch_timestamp(&self) -> &str { + return self.launch_timestamp.as_str(); + } + + pub(crate) fn file_name_prefix(&self, configured_prefix: &str) -> std::string::String { + return format!("{}.{}.{}", self.application_id, self.launch_timestamp, configured_prefix); + } +} + +fn validate_identity_component(value: &str, field: &'static str) -> ksp_core_lib::Result<()> { + if value.is_empty() || value.len() > 160 { + return invalid_identity(field); + } + for byte in value.bytes() { + let accepted = byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-'); + if !accepted { + return invalid_identity(field); + } + } + return std::result::Result::Ok(()); +} + +fn invalid_identity(field: &'static str) -> ksp_core_lib::Result<()> { + return std::result::Result::Err( + ksp_core_lib::Error::new(crate::ERROR_CODE_INVALID_RUNTIME_IDENTITY, "KSP Logging runtime identity contains an invalid component") + .with_context("field", field), + ); +} + +#[cfg(test)] +#[path = "../unit_tests/identity.rs"] +mod tests; diff --git a/crates/ksp-logging-lib/src/lib.rs b/crates/ksp-logging-lib/src/lib.rs index 4a323c2..39cbb3e 100644 --- a/crates/ksp-logging-lib/src/lib.rs +++ b/crates/ksp-logging-lib/src/lib.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/lib.rs -// version: 7 +// version: 8 #![warn(missing_docs)] #![deny(unreachable_pub)] #![forbid(unsafe_code)] @@ -7,12 +7,13 @@ //! KSP-owned logging and tracing facade. //! //! This crate owns the KSP runtime logging contract. Behavioral KSP crates emit events and spans through this facade rather than depending directly on the -//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, hot reload and non-blocking outputs. `0.1.3-pre.006` supports -//! multiple simultaneous outputs with per-output level/target/domain routing, selectable formats, console ANSI and per-file dropped-line accounting. Structured -//! `domain` routing remains distinct from targets and follows explicit event domains or inherited span domains. +//! `tracing` stack. The crate owns the single global subscriber, KSP takeover filtering, transactional hot reload, non-blocking outputs, structured +//! level/target/domain routing and optional per-launch identities used to isolate persistent file outputs. Structured `domain` routing remains distinct from +//! targets and follows explicit event domains or inherited span domains. mod domain; mod error; +mod identity; mod macros; mod runtime; mod settings; @@ -23,16 +24,24 @@ mod writer; pub use self::error::ERROR_CODE_ALREADY_INITIALIZED; /// Error code used when the rolling file output cannot be initialized. pub use self::error::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED; +/// Error code used when an application Logging runtime identity is invalid. +pub use self::error::ERROR_CODE_INVALID_RUNTIME_IDENTITY; /// Error code used when runtime logging settings are invalid. pub use self::error::ERROR_CODE_INVALID_SETTINGS; /// Error code used when a hot reload cannot replace the active runtime layers. pub use self::error::ERROR_CODE_RELOAD_FAILED; +/// Stable application/launch identity used to isolate persistent Logging file outputs. +pub use self::identity::LoggingRuntimeIdentity; /// Cumulative number of log lines dropped by non-blocking KSP outputs. pub use self::runtime::DroppedLines; /// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber. pub use self::runtime::LoggingGuard; +/// Metadata for one active persistent Logging file output. +pub use self::runtime::RuntimeFileMetadata; /// Installs the global KSP tracing subscriber. pub use self::runtime::initialize; +/// Installs the global KSP tracing subscriber with a stable per-launch runtime identity. +pub use self::runtime::initialize_with_identity; /// Replaces the active KSP logging settings without reinstalling the global subscriber. pub use self::runtime::reinitialize; /// Console stream selected for human-readable logs. diff --git a/crates/ksp-logging-lib/src/runtime.rs b/crates/ksp-logging-lib/src/runtime.rs index e9af54a..cb5a52e 100644 --- a/crates/ksp-logging-lib/src/runtime.rs +++ b/crates/ksp-logging-lib/src/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/src/runtime.rs -// version: 11 +// version: 12 use tracing_subscriber::Layer; // rust-rules: trait-import use tracing_subscriber::layer::SubscriberExt; // rust-rules: trait-import @@ -41,10 +41,46 @@ impl DroppedLines { } } +/// Metadata for one active persistent Logging file output. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct RuntimeFileMetadata { + output_id: std::string::String, + directory: std::path::PathBuf, + file_name_prefix: std::string::String, + rotation: crate::FileRotation, +} + +impl RuntimeFileMetadata { + /// Returns the stable output identifier. + #[must_use] + pub fn output_id(&self) -> &str { + return self.output_id.as_str(); + } + + /// Returns the resolved directory containing this launch's files. + #[must_use] + pub fn directory(&self) -> &std::path::Path { + return self.directory.as_path(); + } + + /// Returns the effective launch-specific filename prefix passed to the rolling appender. + #[must_use] + pub fn file_name_prefix(&self) -> &str { + return self.file_name_prefix.as_str(); + } + + /// Returns the configured rotation cadence. + #[must_use] + pub const fn rotation(&self) -> crate::FileRotation { + return self.rotation; + } +} + /// Guard owning the mutable runtime state and non-blocking writers of the installed KSP logging subscriber. pub struct LoggingGuard { reload_handle: RuntimeReloadHandle, settings: crate::LoggingSettings, + runtime_identity: std::option::Option, outputs: RuntimeOutputs, retired_dropped_lines: crate::DroppedLines, retired_file_dropped_lines: std::collections::HashMap, @@ -57,6 +93,25 @@ impl LoggingGuard { return &self.settings; } + /// Returns the stable application/launch identity attached to this runtime, when one was supplied at initialization. + #[must_use] + pub const fn runtime_identity(&self) -> std::option::Option<&crate::LoggingRuntimeIdentity> { + return self.runtime_identity.as_ref(); + } + + /// Returns metadata for the currently active persistent file outputs. + #[must_use] + pub fn active_file_outputs(&self) -> std::vec::Vec { + return self + .outputs + .files + .iter() + .map(|output| -> crate::RuntimeFileMetadata { + return output.metadata.clone(); + }) + .collect(); + } + /// Returns cumulative dropped-line counters across active and previously reloaded outputs. #[must_use] pub fn dropped_lines(&self) -> crate::DroppedLines { @@ -110,7 +165,7 @@ impl RuntimeOutputs { .files .iter() .find(|output| -> bool { - return output.output_id == output_id; + return output.metadata.output_id == output_id; }) .map(|output| -> usize { return output.output.dropped_lines(); @@ -120,7 +175,7 @@ impl RuntimeOutputs { fn accumulate_file_dropped_lines(&self, destination: &mut std::collections::HashMap) { for output in &self.files { let dropped = output.output.dropped_lines(); - match destination.entry(output.output_id.clone()) { + match destination.entry(output.metadata.output_id.clone()) { std::collections::hash_map::Entry::Occupied(mut entry) => { let cumulative = entry.get().saturating_add(dropped); *entry.get_mut() = cumulative; @@ -134,7 +189,7 @@ impl RuntimeOutputs { } struct RuntimeFileOutput { - output_id: std::string::String, + metadata: crate::RuntimeFileMetadata, output: RuntimeOutput, } @@ -155,7 +210,7 @@ struct PreparedOutput { } struct PreparedFileOutput { - output_id: std::string::String, + metadata: crate::RuntimeFileMetadata, layer: BoxedRuntimeLayer, output: RuntimeOutput, } @@ -165,7 +220,19 @@ struct PreparedFileOutput { /// This function may succeed only once for the lifetime of the process. The returned guard owns all non-blocking writer guards and is then used by /// [`crate::reinitialize`] to replace the active KSP logging configuration without installing a second global subscriber. pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result { - return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result { + return initialize_runtime(settings, std::option::Option::None); +} + +/// Installs the global KSP tracing subscriber and isolates persistent file outputs with one stable application launch identity. +pub fn initialize_with_identity(settings: &crate::LoggingSettings, identity: &crate::LoggingRuntimeIdentity) -> ksp_core_lib::Result { + return initialize_runtime(settings, std::option::Option::Some(identity.clone())); +} + +fn initialize_runtime( + settings: &crate::LoggingSettings, + runtime_identity: std::option::Option, +) -> ksp_core_lib::Result { + return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result { let PreparedRuntime { layers, outputs } = prepared; let (reload_layer, reload_handle) = tracing_subscriber::reload::Layer::new(layers); let subscriber = tracing_subscriber::registry().with(reload_layer); @@ -174,6 +241,7 @@ pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result std::result::Result::Ok(crate::LoggingGuard { reload_handle, settings: settings.clone(), + runtime_identity, outputs, retired_dropped_lines: crate::DroppedLines::zero(), retired_file_dropped_lines: std::collections::HashMap::new(), @@ -191,7 +259,7 @@ pub fn initialize(settings: &crate::LoggingSettings) -> ksp_core_lib::Result ksp_core_lib::Result<()> { - return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<()> { + return prepare_runtime_with_identity(settings, guard.runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<()> { let PreparedRuntime { layers, outputs } = prepared; let mut retired_layers = RuntimeLayers::new(); let reload_result = guard.reload_handle.modify(|active_layers| { @@ -215,6 +283,13 @@ pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSe } fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result { + return prepare_runtime_with_identity(settings, std::option::Option::None); +} + +fn prepare_runtime_with_identity( + settings: &crate::LoggingSettings, + runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>, +) -> ksp_core_lib::Result { let validation_error = settings.validate().err(); if let std::option::Option::Some(error) = validation_error { return std::result::Result::Err(error); @@ -233,13 +308,13 @@ fn prepare_runtime(settings: &crate::LoggingSettings) -> ksp_core_lib::Result output, std::result::Result::Err(error) => return std::result::Result::Err(error), }; output_layers.push(prepared_file.layer); - outputs.files.push(RuntimeFileOutput { output_id: prepared_file.output_id, output: prepared_file.output }); + outputs.files.push(RuntimeFileOutput { metadata: prepared_file.metadata, output: prepared_file.output }); } if output_layers.is_empty() { return std::result::Result::Ok(PreparedRuntime { layers: RuntimeLayers::new(), outputs }); @@ -272,10 +347,18 @@ fn build_console_output(console: &crate::ConsoleSettings, settings: &crate::Logg }; } -fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettings) -> ksp_core_lib::Result { +fn build_file_output( + file: &crate::FileSettings, + settings: &crate::LoggingSettings, + runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>, +) -> ksp_core_lib::Result { + let file_name_prefix = match runtime_identity { + std::option::Option::Some(identity) => identity.file_name_prefix(file.file_name_prefix()), + std::option::Option::None => file.file_name_prefix().to_owned(), + }; let appender_result = tracing_appender::rolling::RollingFileAppender::builder() .rotation(map_file_rotation(file.rotation())) - .filename_prefix(file.file_name_prefix()) + .filename_prefix(file_name_prefix.as_str()) .build(file.directory()); let appender = match appender_result { std::result::Result::Ok(appender) => appender, @@ -284,7 +367,7 @@ fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettin ksp_core_lib::Error::new(crate::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED, "unable to initialize the KSP rolling file appender") .with_context("output_id", file.output_id()) .with_context("directory", file.directory().display().to_string()) - .with_context("file_name_prefix", file.file_name_prefix()) + .with_context("file_name_prefix", file_name_prefix.as_str()) .with_source(error), ); }, @@ -292,7 +375,13 @@ fn build_file_output(file: &crate::FileSettings, settings: &crate::LoggingSettin let stripped_writer = crate::writer::StripAnsiWriter::new(appender); let thread_name = format!("ksp-logging-{}", file.output_id()); let prepared = build_non_blocking_output(stripped_writer, thread_name.as_str(), settings.span_events(), false, false, file.format(), file.filter()); - return std::result::Result::Ok(PreparedFileOutput { output_id: file.output_id().to_string(), layer: prepared.layer, output: prepared.output }); + let metadata = crate::RuntimeFileMetadata { + output_id: file.output_id().to_owned(), + directory: file.directory().to_path_buf(), + file_name_prefix, + rotation: file.rotation(), + }; + return std::result::Result::Ok(PreparedFileOutput { metadata, layer: prepared.layer, output: prepared.output }); } fn build_non_blocking_output( diff --git a/crates/ksp-logging-lib/tests/public_api.rs b/crates/ksp-logging-lib/tests/public_api.rs index 0c8cc2f..b542992 100644 --- a/crates/ksp-logging-lib/tests/public_api.rs +++ b/crates/ksp-logging-lib/tests/public_api.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/tests/public_api.rs -// version: 6 +// version: 7 //! Integration tests for the public crate-root surface of `ksp-logging-lib`. @@ -81,10 +81,16 @@ fn all_span_levels_are_usable() { #[test] fn public_runtime_surface_is_addressable_without_installing_it() { let _initialize = ksp_logging_lib::initialize; + let _initialize_with_identity = ksp_logging_lib::initialize_with_identity; let _reinitialize = ksp_logging_lib::reinitialize; + let identity = ksp_logging_lib::LoggingRuntimeIdentity::new("ksp-test", "20260816-182519.123Z-p4242"); + assert!(identity.is_ok()); let _already_initialized = ksp_logging_lib::ERROR_CODE_ALREADY_INITIALIZED; + let _invalid_runtime_identity = ksp_logging_lib::ERROR_CODE_INVALID_RUNTIME_IDENTITY; let _reload_failed = ksp_logging_lib::ERROR_CODE_RELOAD_FAILED; let _file_initialization_failed = ksp_logging_lib::ERROR_CODE_FILE_OUTPUT_INITIALIZATION_FAILED; + let _runtime_identity = ksp_logging_lib::LoggingGuard::runtime_identity; + let _active_file_outputs = ksp_logging_lib::LoggingGuard::active_file_outputs; let _per_file_counter = ksp_logging_lib::LoggingGuard::dropped_file_lines; let dropped = ksp_logging_lib::DroppedLines::zero(); assert_eq!(dropped.console(), 0); diff --git a/crates/ksp-logging-lib/tests/runtime.rs b/crates/ksp-logging-lib/tests/runtime.rs index a0bec33..709709b 100644 --- a/crates/ksp-logging-lib/tests/runtime.rs +++ b/crates/ksp-logging-lib/tests/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/tests/runtime.rs -// version: 8 +// version: 9 //! Integration tests for global initialization, takeover filtering, non-blocking outputs and hot reload. @@ -119,7 +119,13 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i std::option::Option::None, std::vec::Vec::new(), ); - let initialize_result = ksp_logging_lib::initialize(&disabled); + let identity = ksp_logging_lib::LoggingRuntimeIdentity::new("ksp-logging-runtime-test", "20260816-182519.123Z-p4242"); + assert!(identity.is_ok()); + let identity = match identity { + std::result::Result::Ok(value) => value, + std::result::Result::Err(_) => return, + }; + let initialize_result = ksp_logging_lib::initialize_with_identity(&disabled, &identity); assert!(initialize_result.is_ok()); let mut guard = match initialize_result { std::result::Result::Ok(guard) => guard, @@ -222,6 +228,12 @@ fn global_runtime_supports_takeover_non_blocking_outputs_hot_reload_and_single_i ); let file_reload = ksp_logging_lib::reinitialize(&mut guard, &files_enabled); assert!(file_reload.is_ok()); + assert_eq!(guard.runtime_identity(), std::option::Option::Some(&identity)); + let active_files = guard.active_file_outputs(); + assert_eq!(active_files.len(), 4); + for file in &active_files { + assert!(file.file_name_prefix().starts_with("ksp-logging-runtime-test.20260816-182519.123Z-p4242.")); + } ksp_logging_lib::info!(target: LOGGING_TARGET, "logging info \x1b[31mmarker\x1b[0m"); ksp_logging_lib::error!(target: LOGGING_TARGET, "logging error marker"); ksp_logging_lib::info!(target: JSON_KSP_TARGET, "json info marker"); diff --git a/crates/ksp-logging-lib/unit_tests/identity.rs b/crates/ksp-logging-lib/unit_tests/identity.rs new file mode 100644 index 0000000..dd97ab5 --- /dev/null +++ b/crates/ksp-logging-lib/unit_tests/identity.rs @@ -0,0 +1,23 @@ +// file: crates/ksp-logging-lib/unit_tests/identity.rs +// version: 1 + +#[test] +fn runtime_identity_accepts_safe_application_and_timestamp_tokens() { + let identity = crate::LoggingRuntimeIdentity::new("ksp-app-config-desk", "20260816-182519.123-p4242"); + assert!(identity.is_ok()); + if let std::result::Result::Ok(identity) = identity { + assert_eq!(identity.application_id(), "ksp-app-config-desk"); + assert_eq!(identity.launch_timestamp(), "20260816-182519.123-p4242"); + assert_eq!(identity.file_name_prefix("ksp-debug.log"), "ksp-app-config-desk.20260816-182519.123-p4242.ksp-debug.log"); + } +} + +#[test] +fn runtime_identity_rejects_paths_whitespace_and_empty_components() { + for application_id in ["", "ksp app", "../ksp-app", "ksp/app"] { + let result = crate::LoggingRuntimeIdentity::new(application_id, "20260816-182519.123-p4242"); + assert!(result.is_err()); + } + let result = crate::LoggingRuntimeIdentity::new("ksp-app-config-desk", "2026/08/16"); + assert!(result.is_err()); +} diff --git a/crates/ksp-logging-lib/unit_tests/runtime.rs b/crates/ksp-logging-lib/unit_tests/runtime.rs index cf63d8a..0b9fafb 100644 --- a/crates/ksp-logging-lib/unit_tests/runtime.rs +++ b/crates/ksp-logging-lib/unit_tests/runtime.rs @@ -1,5 +1,5 @@ // file: crates/ksp-logging-lib/unit_tests/runtime.rs -// version: 9 +// version: 10 #[test] fn level_mapping_covers_all_ksp_levels() { @@ -258,3 +258,35 @@ fn lossy_non_blocking_builder_drops_lines_instead_of_blocking_a_stalled_producer drop(non_blocking); drop(worker_guard); } + +#[test] +fn launch_identity_decorates_file_outputs_without_mutating_source_settings() { + let root = std::env::temp_dir().join(format!("ksp-pre016-runtime-{}", std::process::id())); + let _cleanup_before = std::fs::remove_dir_all(root.as_path()); + let file = crate::FileSettings::new( + "file.runtime", + true, + root.clone(), + "runtime.log", + crate::FileRotation::Never, + crate::LogFormat::Human, + crate::OutputFilter::unrestricted(), + ); + let settings = crate::LoggingSettings::new(crate::LogFilterLevel::Info, crate::SpanEvents::Off, std::option::Option::None, std::vec![file]); + let identity = crate::LoggingRuntimeIdentity::new("ksp-app-config-desk", "20260816-182519.123-p4242"); + assert!(identity.is_ok()); + if let std::result::Result::Ok(identity) = identity { + let prepared = super::prepare_runtime_with_identity(&settings, std::option::Option::Some(&identity)); + assert!(prepared.is_ok()); + if let std::result::Result::Ok(prepared) = prepared { + assert_eq!(prepared.outputs.files.len(), 1); + assert_eq!(prepared.outputs.files[0].metadata.output_id(), "file.runtime"); + assert_eq!(prepared.outputs.files[0].metadata.directory(), root.as_path()); + assert_eq!(prepared.outputs.files[0].metadata.file_name_prefix(), "ksp-app-config-desk.20260816-182519.123-p4242.runtime.log"); + assert_eq!(settings.files()[0].file_name_prefix(), "runtime.log"); + drop(prepared); + } + } + let cleanup_after = std::fs::remove_dir_all(root.as_path()); + assert!(cleanup_after.is_ok()); +} diff --git a/deltas/0.1.4/pre.016.md b/deltas/0.1.4/pre.016.md new file mode 100644 index 0000000..fb57fdb --- /dev/null +++ b/deltas/0.1.4/pre.016.md @@ -0,0 +1,145 @@ + + + +# Delta `0.1.4-pre.016` — Logging runtime : identité de lancement et profil actif + +## Base + +`0.1.4-pre.015-fix.003` est validée : `cargo fmt --all`, `cargo check --workspace`, `cargo clippy --workspace --all-targets`, 51 tests `ksp-app-config-desk`, 88 tests `ksp-config-lib`, 5 audits ownership et 12 tests d'API publique passent. Le hot reload persistence + runtime de `pre.015` a également été validé avec `runtime_applied=true` et une génération qui avance sans redémarrage. + +## Objectif + +Compléter le runtime Logging sans déplacer l'ownership : + +- `ksp-logging-lib` reste seul propriétaire du subscriber et des sinks ; +- `ksp-config-lib` reste seul propriétaire du document Logging et de sa résolution ; +- Config Desk compose ces contrats et expose leur état. + +Cette tranche ne crée pas encore le panneau manuel **Test Logging** prévu en `pre.017`. + +## Changements + +### Identité stable par lancement + +`ksp-logging-lib` ajoute `LoggingRuntimeIdentity` et `initialize_with_identity()`. + +L'identité contient : + +- un `application_id` ; +- un token `launch_timestamp` construit une seule fois par l'application. + +Config Desk utilise un token UTC de la forme : + +```text +20260816-182519.123Z-p4242 +``` + +Chaque file sink actif reçoit un prefix effectif de la forme : + +```text +.. +``` + +Exemple : + +```text +ksp-app-config-desk.20260816-182519.123Z-p4242.ksp-debug.log +``` + +Le `FileSettings` source n'est pas muté. `reinitialize()` réutilise automatiquement l'identité conservée dans `LoggingGuard`, donc tous les hot reloads d'un même processus gardent le même prefix de lancement. Un nouveau lancement produit une nouvelle identité et ne fusionne plus ses fichiers avec le précédent, même avec une rotation `daily`. + +`LoggingGuard::active_file_outputs()` expose des `RuntimeFileMetadata` sûres : `output_id`, directory résolu, prefix effectif et rotation. + +### Runtime Logging observable + +Config Desk ajoute : + +- `LoggingRuntimeStatusDto` ; +- `LoggingRuntimeFileDto` ; +- `get_logging_runtime_status`. + +La vue Logging affiche maintenant : + +- profil runtime actif ; +- source de sélection (`default_profile`, `explicit`, `fallback`) ; +- génération ; +- état console ; +- fallback ; +- dropped lines console/fichiers/total ; +- application id ; +- launch timestamp ; +- file sinks actifs et leur prefix effectif. + +L'overview est rafraîchi après un changement runtime afin de refléter immédiatement `active_logging_profile` et `logging_generation`. + +### Profil runtime explicite + +Config Desk ajoute `apply_logging_profile`. + +Cette commande : + +1. recharge un `ConfigEnvironment` frais ; +2. résout un profil **déjà persisté** avec `load_resolved_logging_config(Some(profile_id), ...)` ; +3. prépare et applique le runtime via `ksp_logging_lib::reinitialize()` ; +4. avance la génération uniquement après succès ; +5. marque `selection_source=explicit` ; +6. ne modifie ni `default_profile` ni le document source. + +Le frontend interdit l'application explicite lorsque le brouillon est sale, afin de ne pas confondre un profil local non persisté avec un profil réellement résolu par Config. + +Une future action **Sauvegarder et appliquer** continue au contraire d'appliquer le `default_profile` persisté et marque `selection_source=default_profile`. + +### Rollback/runtime + +Les garanties existantes sont conservées : + +- `reinitialize()` prépare les nouveaux sinks/layers avant le swap ; +- en cas d'échec, le runtime précédent reste actif ; +- pour une sauvegarde de document suivie d'un échec runtime, Config Desk restaure la source précédente ; +- une application explicite de profil n'écrit aucune source, donc un échec ne peut pas altérer le document. + +### Tests + +Les tests ajoutés couvrent notamment : + +- validation de l'identité de lancement ; +- rejet des composants contenant paths/espaces ; +- projection des rotations et compteurs runtime ; +- décoration des prefixes sans mutation des `FileSettings` ; +- maintien de l'identité dans le test d'intégration global à travers les hot reloads et plusieurs file sinks ; +- adressabilité de la nouvelle API publique Logging. + +## Version technique + +```text +workspace.package.version = 0.1.4-pre.16 +``` + +Une dépendance workspace `time ^0.3` avec feature `std` est ajoutée uniquement pour construire le token UTC lisible de lancement dans Config Desk. + +## Validation attendue + +```bash +cargo fmt --all +cargo check --workspace +cargo clippy --workspace --all-targets +cargo test -p ksp-app-config-desk +cargo test -p ksp-config-lib +cargo test -p ksp-logging-lib +cargo tree -p ksp-app-config-desk +cargo tauri dev -c crates/ksp-app-config-desk/tauri.conf.json +``` + +Contrôles fonctionnels : + +1. noter `application_id`, `launch_timestamp` et les prefixes fichiers dans **Runtime actif** ; +2. effectuer plusieurs hot reloads et vérifier que `launch_timestamp`/prefixes restent identiques tandis que `generation` avance ; +3. redémarrer réellement l'application et vérifier qu'un nouveau `launch_timestamp`/prefix est créé ; +4. persister au moins deux profils, laisser `default_profile=A`, puis appliquer explicitement `B` ; +5. vérifier `active_profile=B`, `selection_source=explicit`, génération avancée et `default_profile=A` inchangé après rechargement du document ; +6. sauvegarder/appliquer le document et vérifier que le runtime revient au `default_profile` avec `selection_source=default_profile` ; +7. vérifier sur disque que deux lancements distincts n'écrivent pas dans le même fichier persistant. + +## Suite + +`0.1.4-pre.017` ajoute le panneau **Test Logging** : message, niveaux, target contrôlé, domain et preuves de routing avant/après hot reload et via le bridge frontend. diff --git a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md index 29edc5f..0ecb4bd 100644 --- a/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md +++ b/docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md @@ -1,5 +1,5 @@ - + # Plan `0.1.4` — `ksp-app-config-desk` @@ -1238,7 +1238,7 @@ pre.014 Logging editor — lecture/DTO [réalisé] - targets/domains - mapping types Config -> DTO -pre.015 Logging editor — mutations/persistence [en cours] +pre.015 Logging editor — mutations/persistence [réalisé] - create/clone/rename/delete profils - default_profile - mono-fichier/multi-fichiers @@ -1247,7 +1247,7 @@ pre.015 Logging editor — mutations/persistence [en cours] - Recharger le document = resynchronisation du brouillon uniquement - rollback source + ancien runtime conservé si l’application échoue -pre.016 Logging runtime — compléments +pre.016 Logging runtime — compléments [en cours] - fichiers applicatifs uniques par lancement avec timestamp de démarrage - sélection explicite du profil à appliquer indépendamment du default_profile - observabilité metadata/génération du runtime