v0.1.4-pre.016
This commit is contained in:
@@ -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" }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/README.md -->
|
||||
<!-- version: 18 -->
|
||||
<!-- version: 19 -->
|
||||
|
||||
# `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
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<!-- file: crates/ksp-app-config-desk/TODO.md -->
|
||||
<!-- version: 15 -->
|
||||
<!-- version: 16 -->
|
||||
|
||||
# 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.<start-timestamp>.<ext>`) 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.
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<!-- file: crates/ksp-app-config-desk/USAGE.md -->
|
||||
<!-- version: 18 -->
|
||||
<!-- version: 19 -->
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-app-config-desk/frontend/main.html -->
|
||||
<!-- version: 13 -->
|
||||
<!-- version: 14 -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
|
||||
@@ -310,7 +310,7 @@
|
||||
</button>
|
||||
<button id="saveLoggingDocument" class="btn btn-primary btn-sm" type="button" disabled>
|
||||
<i class="fa-solid fa-floppy-disk me-1" aria-hidden="true"></i>
|
||||
Sauvegarder
|
||||
Sauvegarder et appliquer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -324,6 +324,42 @@
|
||||
<dd id="loggingFormatVersion" class="col-lg-9 mb-0 font-monospace">—</dd>
|
||||
</dl>
|
||||
|
||||
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Runtime Logging actif">
|
||||
<div class="d-flex flex-wrap gap-2 align-items-start justify-content-between mb-3">
|
||||
<div>
|
||||
<h3 class="h6 mb-1">Runtime actif</h3>
|
||||
<p class="text-body-secondary small mb-0">Le profil runtime peut être appliqué explicitement sans modifier <code>default_profile</code>. L'identité de lancement reste stable pendant tous les hot reloads du processus.</p>
|
||||
</div>
|
||||
<button id="refreshLoggingRuntime" class="btn btn-outline-secondary btn-sm" type="button">Rafraîchir runtime</button>
|
||||
</div>
|
||||
<dl class="row small mb-3 app-profile-metadata">
|
||||
<dt class="col-xl-3">Profil actif</dt><dd id="loggingRuntimeActiveProfile" class="col-xl-3 font-monospace">—</dd>
|
||||
<dt class="col-xl-3">Source sélection</dt><dd id="loggingRuntimeSelectionSource" class="col-xl-3 font-monospace">—</dd>
|
||||
<dt class="col-xl-3">Génération</dt><dd id="loggingRuntimeGeneration" class="col-xl-3 font-monospace">—</dd>
|
||||
<dt class="col-xl-3">Console</dt><dd id="loggingRuntimeConsole" class="col-xl-3">—</dd>
|
||||
<dt class="col-xl-3">Fallback</dt><dd id="loggingRuntimeFallback" class="col-xl-3">—</dd>
|
||||
<dt class="col-xl-3">Dropped lines</dt><dd id="loggingRuntimeDropped" class="col-xl-3 font-monospace">—</dd>
|
||||
<dt class="col-xl-3">Application</dt><dd id="loggingRuntimeApplicationId" class="col-xl-3 font-monospace">—</dd>
|
||||
<dt class="col-xl-3">Launch timestamp</dt><dd id="loggingRuntimeLaunchTimestamp" class="col-xl-3 font-monospace">—</dd>
|
||||
</dl>
|
||||
<div class="row g-2 align-items-end mb-3">
|
||||
<div class="col-xl-8">
|
||||
<label class="form-label" for="loggingRuntimeProfileSelect">Profil persisté à appliquer explicitement</label>
|
||||
<select id="loggingRuntimeProfileSelect" class="form-select" disabled></select>
|
||||
<div class="form-text">Disponible uniquement lorsque le brouillon est synchronisé avec la source persistée.</div>
|
||||
</div>
|
||||
<div class="col-xl-4">
|
||||
<button id="applyLoggingRuntimeProfile" class="btn btn-outline-primary w-100" type="button" disabled>Appliquer ce profil au runtime</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table id="loggingRuntimeFilesTable" class="table table-sm align-middle mb-0 w-100">
|
||||
<thead><tr><th>output_id</th><th>Répertoire</th><th>Préfixe fichier effectif</th><th>Rotation</th></tr></thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="border rounded p-3 mb-4 bg-body-tertiary" aria-label="Globals Logging">
|
||||
<h3 class="h6 mb-3">Globals</h3>
|
||||
<div class="row g-3">
|
||||
@@ -438,7 +474,7 @@
|
||||
</section>
|
||||
|
||||
<div class="alert alert-warning small py-2 mb-2" role="note">
|
||||
<strong>Runtime actif :</strong> <strong>Sauvegarder et appliquer</strong> 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. <strong>Recharger le document</strong> resynchronise uniquement le brouillon.
|
||||
<strong>Runtime actif :</strong> <strong>Sauvegarder et appliquer</strong> persiste le candidat puis applique son <code>default_profile</code>. <strong>Appliquer ce profil au runtime</strong> sélectionne un profil persisté sans modifier le document. En cas d'échec, l'ancien runtime et sa génération restent actifs. <strong>Recharger le document</strong> resynchronise uniquement le brouillon.
|
||||
</div>
|
||||
<div id="loggingSaveReport" class="small text-body-secondary mb-2">Aucune sauvegarde/application effectuée pendant ce lancement.</div>
|
||||
<div id="loggingStatus" class="alert alert-primary mb-0" role="status" aria-live="polite">Chargement du document Logging typé...</div>
|
||||
|
||||
@@ -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<HTMLSelectElement>("#loggingRuntimeProfileSelect");
|
||||
const apply = document.querySelector<HTMLButtonElement>("#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<HTMLSelectElement>("#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<HTMLTableSectionElement>("#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<HTMLSelectElement>("#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<void> {
|
||||
if (source === "user") {
|
||||
frontendDebug("main", "Logging runtime metadata refresh requested");
|
||||
}
|
||||
try {
|
||||
const status = await invokeKsp<LoggingRuntimeStatusDto>("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<void> {
|
||||
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<HTMLButtonElement>("#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<LoggingRuntimeStatusDto>("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<void> {
|
||||
try {
|
||||
const result = await invokeKsp<LoggingDocumentSaveResultDto>("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<void>
|
||||
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<HTMLButtonElement>("#refreshLoggingDocument")?.addEventListener("click", requestReload);
|
||||
document.querySelector<HTMLButtonElement>("#saveLoggingDocument")?.addEventListener("click", () => void saveLoggingDocument());
|
||||
document.querySelector<HTMLButtonElement>("#refreshLoggingRuntime")?.addEventListener("click", () => void refreshRuntimeStatus("user"));
|
||||
document.querySelector<HTMLButtonElement>("#applyLoggingRuntimeProfile")?.addEventListener("click", () => void applyRuntimeProfile());
|
||||
document.querySelector<HTMLSelectElement>("#loggingRuntimeProfileSelect")?.addEventListener("change", updateRuntimeActionState);
|
||||
document.querySelector<HTMLSelectElement>("#loggingProfileSelect")?.addEventListener("change", event => {
|
||||
syncStaticProfileFields();
|
||||
selectedProfileId = (event.currentTarget as HTMLSelectElement).value;
|
||||
|
||||
@@ -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<void> {
|
||||
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<void> {
|
||||
const windowLabel = getCurrentWindow().label;
|
||||
frontendInfo("main", "Config Desk main frontend loaded", { windowLabel });
|
||||
bindNavigation();
|
||||
bindRuntimeSnapshotRefresh();
|
||||
initializeDocumentsPanel();
|
||||
initializeProfilesPanel();
|
||||
initializeEnvironmentPanel();
|
||||
|
||||
@@ -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<String>,
|
||||
selection_source: String,
|
||||
generation: u32,
|
||||
fallback_active: bool,
|
||||
startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
@@ -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<u32> {
|
||||
pub(crate) fn reinitialize_logging_runtime(
|
||||
&self,
|
||||
profile_id: &str,
|
||||
selection_source: &str,
|
||||
settings: &ksp_logging_lib::LoggingSettings,
|
||||
) -> ksp_core_lib::Result<u32> {
|
||||
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<crate::LoggingRuntimeStatusDto> {
|
||||
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::<crate::LoggingRuntimeFileDto>::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 {
|
||||
|
||||
@@ -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<String>,
|
||||
pub(crate) selection_source: String,
|
||||
pub(crate) fallback_active: bool,
|
||||
pub(crate) startup_diagnostic: std::option::Option<crate::CommandErrorDto>,
|
||||
}
|
||||
@@ -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<LoggingStartup> {
|
||||
pub(crate) fn initialize_logging(
|
||||
management: &ksp_config_lib::ConfigManagement,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<LoggingStartup> {
|
||||
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<LoggingStartup> {
|
||||
fn initialize_fallback_logging(
|
||||
initial_error: ksp_core_lib::Error,
|
||||
runtime_identity: &ksp_logging_lib::LoggingRuntimeIdentity,
|
||||
) -> ksp_core_lib::Result<LoggingStartup> {
|
||||
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),
|
||||
});
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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),
|
||||
|
||||
130
crates/ksp-app-config-desk/src/logging_runtime.rs
Normal file
130
crates/ksp-app-config-desk/src/logging_runtime.rs
Normal file
@@ -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<String>,
|
||||
/// 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<LoggingRuntimeFileDto>,
|
||||
}
|
||||
|
||||
/// Creates the stable runtime identity for this Config Desk process launch.
|
||||
pub(crate) fn launch_identity() -> ksp_core_lib::Result<ksp_logging_lib::LoggingRuntimeIdentity> {
|
||||
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<LoggingRuntimeStatusDto> {
|
||||
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<LoggingRuntimeStatusDto> {
|
||||
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;
|
||||
@@ -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::Wry>) -> tauri::Builder<tau
|
||||
remove_environment_value,
|
||||
reveal_environment_value,
|
||||
get_logging_document,
|
||||
get_logging_runtime_status,
|
||||
apply_logging_profile,
|
||||
save_logging_document,
|
||||
emit_frontend_log,
|
||||
splash_frontend_ready
|
||||
@@ -181,6 +183,27 @@ fn get_logging_document(state: tauri::State<'_, crate::AppState>) -> std::result
|
||||
};
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn get_logging_runtime_status(state: tauri::State<'_, crate::AppState>) -> std::result::Result<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
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<crate::LoggingRuntimeStatusDto, crate::CommandErrorDto> {
|
||||
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,
|
||||
|
||||
26
crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
Normal file
26
crates/ksp-app-config-desk/unit_tests/logging_runtime.rs
Normal file
@@ -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);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/README.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
<!-- file: crates/ksp-logging-lib/TODO.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# 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.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-logging-lib/USAGE.md -->
|
||||
<!-- version: 5 -->
|
||||
<!-- version: 6 -->
|
||||
|
||||
# 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 :
|
||||
|
||||
@@ -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");
|
||||
|
||||
71
crates/ksp-logging-lib/src/identity.rs
Normal file
71
crates/ksp-logging-lib/src/identity.rs
Normal file
@@ -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<std::string::String>,
|
||||
launch_timestamp: impl std::convert::Into<std::string::String>,
|
||||
) -> ksp_core_lib::Result<Self> {
|
||||
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;
|
||||
@@ -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.
|
||||
|
||||
@@ -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<crate::LoggingRuntimeIdentity>,
|
||||
outputs: RuntimeOutputs,
|
||||
retired_dropped_lines: crate::DroppedLines,
|
||||
retired_file_dropped_lines: std::collections::HashMap<std::string::String, usize>,
|
||||
@@ -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<crate::RuntimeFileMetadata> {
|
||||
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<std::string::String, usize>) {
|
||||
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<crate::LoggingGuard> {
|
||||
return prepare_runtime(settings).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
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<crate::LoggingGuard> {
|
||||
return initialize_runtime(settings, std::option::Option::Some(identity.clone()));
|
||||
}
|
||||
|
||||
fn initialize_runtime(
|
||||
settings: &crate::LoggingSettings,
|
||||
runtime_identity: std::option::Option<crate::LoggingRuntimeIdentity>,
|
||||
) -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
return prepare_runtime_with_identity(settings, runtime_identity.as_ref()).and_then(|prepared| -> ksp_core_lib::Result<crate::LoggingGuard> {
|
||||
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<cra
|
||||
std::result::Result::Ok(()) => 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<cra
|
||||
/// configuration remains unchanged. After a successful layer swap, dropped-line counters from the retired outputs are retained cumulatively. Retired
|
||||
/// layers are then dropped before their worker guards so all retired `NonBlocking` senders are released before shutdown asks the workers to drain/flush.
|
||||
pub fn reinitialize(guard: &mut crate::LoggingGuard, settings: &crate::LoggingSettings) -> 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<PreparedRuntime> {
|
||||
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<PreparedRuntime> {
|
||||
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<Pr
|
||||
outputs.console = std::option::Option::Some(prepared_console.output);
|
||||
}
|
||||
for file in enabled_files {
|
||||
let prepared_file_result = build_file_output(file, settings);
|
||||
let prepared_file_result = build_file_output(file, settings, runtime_identity);
|
||||
let prepared_file = match prepared_file_result {
|
||||
std::result::Result::Ok(output) => 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<PreparedFileOutput> {
|
||||
fn build_file_output(
|
||||
file: &crate::FileSettings,
|
||||
settings: &crate::LoggingSettings,
|
||||
runtime_identity: std::option::Option<&crate::LoggingRuntimeIdentity>,
|
||||
) -> ksp_core_lib::Result<PreparedFileOutput> {
|
||||
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<W>(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
23
crates/ksp-logging-lib/unit_tests/identity.rs
Normal file
23
crates/ksp-logging-lib/unit_tests/identity.rs
Normal file
@@ -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());
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
145
deltas/0.1.4/pre.016.md
Normal file
145
deltas/0.1.4/pre.016.md
Normal file
@@ -0,0 +1,145 @@
|
||||
<!-- file: deltas/0.1.4/pre.016.md -->
|
||||
<!-- version: 1 -->
|
||||
|
||||
# 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
|
||||
<application_id>.<launch_timestamp>.<configured_file_name_prefix>
|
||||
```
|
||||
|
||||
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.
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: docs/plans/006-V0_1_4_CONFIG_DESKTOP_PLAN.md -->
|
||||
<!-- version: 19 -->
|
||||
<!-- version: 20 -->
|
||||
|
||||
# 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
|
||||
|
||||
Reference in New Issue
Block a user