0.5.1-pre.005
This commit is contained in:
@@ -14,8 +14,9 @@ KS_SECRET_POSTGRES_DEVNET_URL=postgres://solana:solana@localhost:5432/khadhroony
|
|||||||
# PostgreSQL database reserved for automated integration tests.
|
# PostgreSQL database reserved for automated integration tests.
|
||||||
KS_SECRET_POSTGRES_TEST_URL=postgres://solana:solana@localhost:5432/khadhroony_test
|
KS_SECRET_POSTGRES_TEST_URL=postgres://solana:solana@localhost:5432/khadhroony_test
|
||||||
|
|
||||||
# Optional configuration-file override.
|
# Optional independent configuration-file overrides.
|
||||||
# KS_CONFIG_PATH=config/example.config.json
|
# KS_CONFIG_PATH=config/app.config.json
|
||||||
|
# KS_LOGGING_CONFIG_PATH=config/logging.config.json
|
||||||
|
|
||||||
# Optional Devnet profile selected by CLI and opt-in scenario tests.
|
# Optional Devnet profile selected by CLI and opt-in scenario tests.
|
||||||
# When absent, the first compatible Devnet profile is selected.
|
# When absent, the first compatible Devnet profile is selected.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: Cargo.toml
|
# file: Cargo.toml
|
||||||
# version: 48
|
# version: 49
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
@@ -18,7 +18,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.5.1-pre.4"
|
version = "0.5.1-pre.5"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
repository = "https://git.sasedev.com/Sasedev/khadhroony-bot3"
|
||||||
|
|||||||
121
config/README.md
121
config/README.md
@@ -1,29 +1,50 @@
|
|||||||
<!-- file: config/README.md -->
|
<!-- file: config/README.md -->
|
||||||
<!-- version: 19 -->
|
<!-- version: 20 -->
|
||||||
|
|
||||||
# Configuration locale
|
# Configuration locale
|
||||||
|
|
||||||
Ce dossier contient les exemples et le schéma de configuration JSON.
|
Ce dossier contient les deux documents de configuration runtime, leurs exemples et leurs schémas JSON indépendants.
|
||||||
|
|
||||||
## Règles
|
## Fichiers chargés par défaut
|
||||||
|
|
||||||
- Le fichier versionné doit rester sans secret.
|
- `app.config.json` : configuration générale Khadhroony Solana ;
|
||||||
- Les placeholders de clés restent directement dans les URLs.
|
- `logging.config.json` : configuration logging/tracing ;
|
||||||
- Le champ `active_profile` sélectionne un seul profil actif.
|
- `schemas/app.config.schema.json` : schéma du document général ;
|
||||||
- Un profil actif doit être présent et unique.
|
- `schemas/logging.config.schema.json` : schéma du document logging.
|
||||||
- Les profils non actifs peuvent rester dans le fichier pour les tests devnet, mainnet lecture seule, backfill ou future production.
|
|
||||||
- Le contrat courant couvre HTTP JSON-RPC Solana et WebSocket JSON-RPC générique.
|
|
||||||
- Helius `transactionSubscribe` et Yellowstone gRPC seront ajoutés plus tard dans `ks_onchain_transport`, avec évolution explicite du schéma quand les transports seront réellement utilisés.
|
|
||||||
- Les IDLs restent des artefacts de développement et ne font pas partie de la configuration runtime.
|
|
||||||
|
|
||||||
## Fichiers
|
Les chemins par défaut peuvent être remplacés indépendamment par :
|
||||||
|
|
||||||
- `example.config.json` : exemple complet avec profils `local_devnet`, `mainnet_research` et `mainnet`.
|
```text
|
||||||
- `schema.config.json` : JSON Schema de validation du fichier de configuration.
|
KS_CONFIG_PATH
|
||||||
|
KS_LOGGING_CONFIG_PATH
|
||||||
|
```
|
||||||
|
|
||||||
## Variables d’environnement de l’exemple
|
## Exemples conformes
|
||||||
|
|
||||||
`example.config.json` conserve la topologie des profils et référence les valeurs locales avec des placeholders :
|
- `example.app.config.json` : exemple général minimal conforme ;
|
||||||
|
- `example.logging.config.json` : exemple logging minimal conforme.
|
||||||
|
|
||||||
|
Les exemples servent de référence de structure. Ils ne sont pas sélectionnés automatiquement au démarrage.
|
||||||
|
|
||||||
|
## Sélection indépendante des profils
|
||||||
|
|
||||||
|
Chaque document possède son propre `active_profile` :
|
||||||
|
|
||||||
|
```text
|
||||||
|
app.config.json
|
||||||
|
active_profile = mainnet_research
|
||||||
|
|
||||||
|
logging.config.json
|
||||||
|
active_profile = mainnet_research
|
||||||
|
```
|
||||||
|
|
||||||
|
Ces valeurs sont indépendantes. Changer le profil applicatif ne change pas implicitement le profil logging, et inversement. Les noms peuvent être identiques pour faciliter l’exploitation sans créer de couplage contractuel.
|
||||||
|
|
||||||
|
Le document général ne contient plus de propriété `logging`. Les routes, niveaux, formats et filtres sont exclusivement possédés par `logging.config.json` et `ks-logging`.
|
||||||
|
|
||||||
|
## Variables d’environnement
|
||||||
|
|
||||||
|
`app.config.json` référence notamment :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
KS_SECRET_HELIUS_API_KEY
|
KS_SECRET_HELIUS_API_KEY
|
||||||
@@ -31,73 +52,35 @@ KS_SECRET_POSTGRES_MAINNET_URL
|
|||||||
KS_SECRET_POSTGRES_DEVNET_URL
|
KS_SECRET_POSTGRES_DEVNET_URL
|
||||||
```
|
```
|
||||||
|
|
||||||
`KS_SECRET_POSTGRES_TEST_URL` n’est pas utilisé par un profil runtime : il reste réservé aux tests d’intégration PostgreSQL. Le profil `local_devnet` utilise `KS_SECRET_POSTGRES_DEVNET_URL`; les profils `mainnet_research` et `mainnet` utilisent `KS_SECRET_POSTGRES_MAINNET_URL`. Les trois bases doivent être distinctes.
|
`KS_SECRET_POSTGRES_TEST_URL` reste réservé aux tests d’intégration PostgreSQL. Le fichier réel `.env` reste local et ignoré par Git ; `.env.example` est le modèle versionné.
|
||||||
|
|
||||||
Le fichier réel `.env` reste local et ignoré par Git. `.env.example` est le seul modèle dotenv versionné.
|
La classification `KS_SECRET_*` / `KS_PUBLIC_*` / `KS_*` et `KB_SECRET_*` / `KB_PUBLIC_*` / `KB_*` est normative. Le camouflage et la propagation de sensibilité dans les valeurs composées sont traités dans la prerelease suivante ; aucun secret ne doit être écrit en clair dans un fichier versionné.
|
||||||
|
|
||||||
## Phase `0.3.x`
|
## Schémas
|
||||||
|
|
||||||
`0.3.x` utilise les endpoints HTTP des profils de recherche existants pour :
|
Les schémas runtime sont localisés exclusivement sous `config/schemas/`.
|
||||||
|
|
||||||
```text
|
`ks-config` embarque `schemas/app.config.schema.json`. `ks-logging` embarque `schemas/logging.config.schema.json`. Les fichiers embarqués et leurs versions sur disque doivent rester identiques.
|
||||||
getSignaturesForAddress
|
|
||||||
getTransaction
|
|
||||||
getSignatureStatuses
|
|
||||||
```
|
|
||||||
|
|
||||||
Aucun profil Helius payant ou Yellowstone n’est activé à ce stade.
|
## Migration depuis le format combiné
|
||||||
|
|
||||||
Les sources futures devront déclarer séparément :
|
L’ancien `example.config.json` combinait configuration générale et logging dans chaque profil. La migration consiste à :
|
||||||
|
|
||||||
- provider ;
|
1. conserver dans `app.config.json` les sections `app`, `database`, `data`, `solana`, `wallet`, `execution` et `demo` ;
|
||||||
- protocole ;
|
2. extraire chaque ancien bloc `logging` vers le profil homonyme de `logging.config.json` ;
|
||||||
- rôle ;
|
3. sélectionner explicitement un `active_profile` dans chacun des deux documents ;
|
||||||
- limites de streams et filtres ;
|
4. supprimer l’ancien fichier combiné une fois la migration validée.
|
||||||
- authentification ;
|
|
||||||
- région éventuelle.
|
|
||||||
|
|
||||||
## Évolution du schéma
|
|
||||||
|
|
||||||
`schema.config.json` est le contrat de validation runtime. Il évolue seulement lorsqu’un champ est réellement utilisé par l’exécution.
|
|
||||||
|
|
||||||
## Logging de développement
|
## Logging de développement
|
||||||
|
|
||||||
L’exemple configure, pour chacun des trois profils, quatre fichiers globaux puis trois fichiers dédiés par crate opérationnelle utilisant `tracing` : `debug.log`, `info.log` et `error.jsonl`.
|
Le fichier `logging.config.json` conserve actuellement les routes globales ainsi que trois fichiers dédiés par crate opérationnelle utilisant `tracing` : `debug.log`, `info.log` et `error.jsonl`.
|
||||||
|
|
||||||
Les matérialisateurs natifs consolidés rejoignent la console et disposent de leurs routes propres sous :
|
Les matérialisateurs consolidés disposent de leurs routes sous les targets `ks-lib-materializer.*`. Les tests de `ks-logging` découvrent dynamiquement les crates déclarant `tracing.workspace = true` et vérifient les routes canoniques du document logging par défaut.
|
||||||
|
|
||||||
```text
|
Les logs ne doivent contenir ni secret, ni DSN non masqué, ni keypair, ni payload de configuration résolue complet.
|
||||||
logs/<profil>/ks-lib/materializer/admin/
|
|
||||||
logs/<profil>/ks-lib/materializer/compliance/audit/
|
|
||||||
logs/<profil>/ks-lib/materializer/lifecycle/
|
|
||||||
logs/<profil>/ks-lib/materializer/risk/
|
|
||||||
logs/<profil>/ks-lib/materializer/staking/
|
|
||||||
logs/<profil>/ks-lib/materializer/token/accounts/
|
|
||||||
logs/<profil>/ks-lib/materializer/transaction/annotations/
|
|
||||||
```
|
|
||||||
|
|
||||||
Le test de configuration découvre dynamiquement toutes les crates déclarant `tracing.workspace = true` et vérifie leurs trois routes dans chaque profil. Les logs ne doivent contenir ni secret, ni DSN non masqué, ni payload Config ou bytecode complet.
|
|
||||||
|
|
||||||
## Limites du profil public Devnet
|
## Limites du profil public Devnet
|
||||||
|
|
||||||
`api.devnet.solana.com` est un endpoint public partagé. Les limites configurées par rôle ne sont pas des quotas indépendants fournis par le serveur : leur débit cumulé doit rester sous la limite globale de l’endpoint, et chaque méthode RPC doit également rester sous sa propre limite.
|
`api.devnet.solana.com` est un endpoint public partagé. Les limites configurées par rôle ne sont pas des quotas indépendants fournis par le serveur : leur débit cumulé doit rester sous la limite globale de l’endpoint, et chaque méthode RPC doit également rester sous sa propre limite.
|
||||||
|
|
||||||
L’exemple `local_devnet` utilise donc volontairement des valeurs conservatrices :
|
Le profil `local_devnet` de `app.config.json` utilise donc volontairement des valeurs conservatrices pour les rôles HTTP. Un warning `retry_http_json_rpc_after_rate_limit` reste possible sur un service partagé ; il indique que le cooldown et le retry borné ont été activés.
|
||||||
|
|
||||||
```text
|
|
||||||
http_queries:
|
|
||||||
requests_per_second: 3
|
|
||||||
burst_capacity: 3
|
|
||||||
max_concurrent_requests: 2
|
|
||||||
pause_after_rate_limit_ms: 10000
|
|
||||||
|
|
||||||
http_transactions:
|
|
||||||
requests_per_second: 1
|
|
||||||
burst_capacity: 1
|
|
||||||
max_concurrent_requests: 1
|
|
||||||
pause_after_rate_limit_ms: 10000
|
|
||||||
```
|
|
||||||
|
|
||||||
Un fichier de configuration local déjà créé n’est pas modifié automatiquement lorsque `example.config.json` évolue. L’opérateur doit reporter explicitement ces valeurs dans son profil `local_devnet`.
|
|
||||||
|
|
||||||
Un warning `retry_http_json_rpc_after_rate_limit` reste possible sur un service public partagé. Il indique que le cooldown et le retry borné ont été activés ; il ne constitue un échec que si les tentatives finissent par être épuisées.
|
|
||||||
|
|||||||
1110
config/app.config.json
Normal file
1110
config/app.config.json
Normal file
File diff suppressed because it is too large
Load Diff
240
config/example.app.config.json
Normal file
240
config/example.app.config.json
Normal file
@@ -0,0 +1,240 @@
|
|||||||
|
{
|
||||||
|
"active_profile": "local_devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"name": "local_devnet",
|
||||||
|
"app": {
|
||||||
|
"name": "khadhroony-bot3",
|
||||||
|
"environment": "development",
|
||||||
|
"auto_reconnect_default": false
|
||||||
|
},
|
||||||
|
"database": {
|
||||||
|
"enabled": true,
|
||||||
|
"backend": "postgres",
|
||||||
|
"postgres": {
|
||||||
|
"url": "${KS_SECRET_POSTGRES_DEVNET_URL}",
|
||||||
|
"max_connections": 8,
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"auto_initialize_schema": true
|
||||||
|
},
|
||||||
|
"sqlite": {
|
||||||
|
"path": "data/local.sqlite",
|
||||||
|
"create_if_missing": true,
|
||||||
|
"busy_timeout_ms": 5000,
|
||||||
|
"max_connections": 1,
|
||||||
|
"auto_initialize_schema": true,
|
||||||
|
"use_wal": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"wallets_directory": "wallets",
|
||||||
|
"logs_directory": "logs"
|
||||||
|
},
|
||||||
|
"solana": {
|
||||||
|
"http_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "devnet_public_http",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "https://api.devnet.solana.com",
|
||||||
|
"connect_timeout_ms": 4000,
|
||||||
|
"request_timeout_ms": 10000,
|
||||||
|
"max_idle_connections_per_host": 4,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "http_queries",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"get_account_info",
|
||||||
|
"get_balance",
|
||||||
|
"get_block_commitment",
|
||||||
|
"get_block_height",
|
||||||
|
"get_block_production",
|
||||||
|
"get_blocks",
|
||||||
|
"get_blocks_with_limit",
|
||||||
|
"get_cluster_nodes",
|
||||||
|
"get_epoch_info",
|
||||||
|
"get_epoch_schedule",
|
||||||
|
"get_fee_for_message",
|
||||||
|
"get_first_available_block",
|
||||||
|
"get_genesis_hash",
|
||||||
|
"get_health",
|
||||||
|
"get_highest_snapshot_slot",
|
||||||
|
"get_identity",
|
||||||
|
"get_inflation_governor",
|
||||||
|
"get_inflation_rate",
|
||||||
|
"get_inflation_reward",
|
||||||
|
"get_largest_accounts",
|
||||||
|
"get_latest_blockhash",
|
||||||
|
"get_leader_schedule",
|
||||||
|
"get_max_retransmit_slot",
|
||||||
|
"get_max_shred_insert_slot",
|
||||||
|
"get_minimum_balance_for_rent_exemption",
|
||||||
|
"get_multiple_accounts",
|
||||||
|
"get_recent_performance_samples",
|
||||||
|
"get_recent_prioritization_fees",
|
||||||
|
"get_signature_statuses",
|
||||||
|
"get_slot",
|
||||||
|
"get_slot_leader",
|
||||||
|
"get_slot_leaders",
|
||||||
|
"get_stake_activation",
|
||||||
|
"get_stake_minimum_delegation",
|
||||||
|
"get_supply",
|
||||||
|
"get_token_account_balance",
|
||||||
|
"get_token_accounts_by_delegate",
|
||||||
|
"get_token_accounts_by_owner",
|
||||||
|
"get_token_largest_accounts",
|
||||||
|
"get_token_supply",
|
||||||
|
"get_transaction",
|
||||||
|
"get_transaction_count",
|
||||||
|
"get_version",
|
||||||
|
"get_vote_accounts",
|
||||||
|
"is_blockhash_valid",
|
||||||
|
"request_airdrop"
|
||||||
|
],
|
||||||
|
"priority": 10,
|
||||||
|
"requests_per_second": 3,
|
||||||
|
"burst_capacity": 3,
|
||||||
|
"max_concurrent_requests": 2,
|
||||||
|
"max_subscriptions": 0,
|
||||||
|
"pause_after_rate_limit_ms": 10000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "http_transactions",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"simulate_transaction",
|
||||||
|
"send_transaction",
|
||||||
|
"get_signature_statuses"
|
||||||
|
],
|
||||||
|
"priority": 20,
|
||||||
|
"requests_per_second": 1,
|
||||||
|
"burst_capacity": 1,
|
||||||
|
"max_concurrent_requests": 1,
|
||||||
|
"max_subscriptions": 0,
|
||||||
|
"pause_after_rate_limit_ms": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"ws_endpoints": [
|
||||||
|
{
|
||||||
|
"name": "devnet_public_ws",
|
||||||
|
"enabled": true,
|
||||||
|
"provider": "solana-public",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"url": "wss://api.devnet.solana.com",
|
||||||
|
"connect_timeout_ms": 5000,
|
||||||
|
"request_timeout_ms": 10000,
|
||||||
|
"unsubscribe_timeout_ms": 1500,
|
||||||
|
"write_channel_capacity": 256,
|
||||||
|
"event_channel_capacity": 1024,
|
||||||
|
"auto_reconnect": false,
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "slot_notifications",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"slot_subscribe",
|
||||||
|
"slots_updates_subscribe"
|
||||||
|
],
|
||||||
|
"priority": 10,
|
||||||
|
"requests_per_second": 4,
|
||||||
|
"burst_capacity": 8,
|
||||||
|
"max_concurrent_requests": 4,
|
||||||
|
"max_subscriptions": 16,
|
||||||
|
"pause_after_rate_limit_ms": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "program_logs",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"logs_subscribe_mentions",
|
||||||
|
"program_subscribe",
|
||||||
|
"account_subscribe",
|
||||||
|
"signature_subscribe",
|
||||||
|
"block_subscribe",
|
||||||
|
"vote_subscribe"
|
||||||
|
],
|
||||||
|
"priority": 20,
|
||||||
|
"requests_per_second": 2,
|
||||||
|
"burst_capacity": 4,
|
||||||
|
"max_concurrent_requests": 2,
|
||||||
|
"max_subscriptions": 64,
|
||||||
|
"pause_after_rate_limit_ms": 3000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "root_notifications",
|
||||||
|
"enabled": true,
|
||||||
|
"request_kinds": [
|
||||||
|
"root_subscribe"
|
||||||
|
],
|
||||||
|
"priority": 15,
|
||||||
|
"requests_per_second": 4,
|
||||||
|
"burst_capacity": 8,
|
||||||
|
"max_concurrent_requests": 4,
|
||||||
|
"max_subscriptions": 16,
|
||||||
|
"pause_after_rate_limit_ms": 3000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"listeners": {
|
||||||
|
"enabled": true,
|
||||||
|
"default_commitment": "confirmed",
|
||||||
|
"log_listeners": [
|
||||||
|
{
|
||||||
|
"name": "pump_swap_logs",
|
||||||
|
"enabled": false,
|
||||||
|
"endpoint_role": "program_logs",
|
||||||
|
"program_id": "pAMMBay6oceH9fJKBRHGP5D4bD4sWpmSwMn52FMfXEA",
|
||||||
|
"purpose": "Détecter les swaps, créations de pool et changements de liquidité Pump Swap."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"program_listeners": [
|
||||||
|
{
|
||||||
|
"name": "pump_fun_program_accounts",
|
||||||
|
"enabled": false,
|
||||||
|
"endpoint_role": "program_logs",
|
||||||
|
"program_id": "6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P",
|
||||||
|
"purpose": "Détecter les changements de comptes utiles à la création ou migration de tokens Pump Fun."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"account_listeners": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wallet": {
|
||||||
|
"wallet_dir": "wallets/temporary/local_devnet",
|
||||||
|
"cluster": "devnet",
|
||||||
|
"temporary_wallet_enabled": true,
|
||||||
|
"temporary_wallet_alias": "local-devnet-operator",
|
||||||
|
"temporary_wallet_persist": true,
|
||||||
|
"localnet_send_enabled": false,
|
||||||
|
"devnet_send_enabled": true,
|
||||||
|
"testnet_send_enabled": false,
|
||||||
|
"mainnet_send_enabled": false
|
||||||
|
},
|
||||||
|
"execution": {
|
||||||
|
"dry_run_default": true,
|
||||||
|
"require_simulation": true,
|
||||||
|
"require_operator_confirmation": true,
|
||||||
|
"localnet_max_spend_lamports": 0,
|
||||||
|
"devnet_max_spend_lamports": 100000000,
|
||||||
|
"testnet_max_spend_lamports": 0,
|
||||||
|
"mainnet_max_spend_lamports": 0,
|
||||||
|
"max_fee_lamports": 1000000,
|
||||||
|
"max_compute_unit_price_micro_lamports": 100000,
|
||||||
|
"recent_blockhash_max_age_slots": 150,
|
||||||
|
"send_max_retries": 3,
|
||||||
|
"confirmation_poll_interval_ms": 500,
|
||||||
|
"confirmation_max_attempts": 120,
|
||||||
|
"devnet_airdrop_max_lamports": 2000000000
|
||||||
|
},
|
||||||
|
"demo": {
|
||||||
|
"live_demo_enabled": true,
|
||||||
|
"trading_demo_enabled": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
55
config/example.logging.config.json
Normal file
55
config/example.logging.config.json
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"active_profile": "local_devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"name": "local_devnet",
|
||||||
|
"default_level": "warn",
|
||||||
|
"targets": [
|
||||||
|
{
|
||||||
|
"name": "console_local_devnet_compact",
|
||||||
|
"enabled": true,
|
||||||
|
"sink": "console",
|
||||||
|
"level": "debug",
|
||||||
|
"path": "",
|
||||||
|
"rotation": "none",
|
||||||
|
"format": "compact",
|
||||||
|
"ansi": true,
|
||||||
|
"targets": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "file_local_devnet_debug",
|
||||||
|
"enabled": true,
|
||||||
|
"sink": "file",
|
||||||
|
"level": "debug",
|
||||||
|
"path": "logs/devnet/debug.log",
|
||||||
|
"rotation": "daily",
|
||||||
|
"format": "human",
|
||||||
|
"ansi": false,
|
||||||
|
"targets": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"target_filters": [
|
||||||
|
{
|
||||||
|
"target": "hyper",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "reqwest",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "tao",
|
||||||
|
"level": "warn"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"target": "wry",
|
||||||
|
"level": "warn"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3557
config/logging.config.json
Normal file
3557
config/logging.config.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
"$id": "https://khadhroony.local/schema/config.schema.json",
|
"$id": "https://khadhroony.local/schema/app.config.schema.json",
|
||||||
"title": "Khadhroony Bot3 configuration",
|
"title": "Khadhroony Solana application configuration",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
@@ -25,24 +25,12 @@
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1
|
"minLength": 1
|
||||||
},
|
},
|
||||||
"log_level": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"trace",
|
|
||||||
"debug",
|
|
||||||
"info",
|
|
||||||
"warn",
|
|
||||||
"error",
|
|
||||||
"off"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"profile": {
|
"profile": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"required": [
|
"required": [
|
||||||
"name",
|
"name",
|
||||||
"app",
|
"app",
|
||||||
"logging",
|
|
||||||
"database",
|
"database",
|
||||||
"data",
|
"data",
|
||||||
"solana",
|
"solana",
|
||||||
@@ -57,9 +45,6 @@
|
|||||||
"app": {
|
"app": {
|
||||||
"$ref": "#/$defs/app_section"
|
"$ref": "#/$defs/app_section"
|
||||||
},
|
},
|
||||||
"logging": {
|
|
||||||
"$ref": "#/$defs/logging"
|
|
||||||
},
|
|
||||||
"database": {
|
"database": {
|
||||||
"$ref": "#/$defs/database"
|
"$ref": "#/$defs/database"
|
||||||
},
|
},
|
||||||
@@ -100,112 +85,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"logging": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"default_level",
|
|
||||||
"targets",
|
|
||||||
"target_filters"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"default_level": {
|
|
||||||
"$ref": "#/$defs/log_level"
|
|
||||||
},
|
|
||||||
"targets": {
|
|
||||||
"type": "array",
|
|
||||||
"minItems": 1,
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/$defs/log_target"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"target_filters": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/$defs/log_target_filter"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log_target": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"name",
|
|
||||||
"enabled",
|
|
||||||
"sink",
|
|
||||||
"level",
|
|
||||||
"path",
|
|
||||||
"rotation",
|
|
||||||
"format",
|
|
||||||
"ansi",
|
|
||||||
"targets"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"$ref": "#/$defs/non_empty_string"
|
|
||||||
},
|
|
||||||
"enabled": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"sink": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"console",
|
|
||||||
"file"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"level": {
|
|
||||||
"$ref": "#/$defs/log_level"
|
|
||||||
},
|
|
||||||
"path": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"rotation": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"none",
|
|
||||||
"never",
|
|
||||||
"daily",
|
|
||||||
"hourly"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"format": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": [
|
|
||||||
"human",
|
|
||||||
"compact",
|
|
||||||
"pretty",
|
|
||||||
"json"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"ansi": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
|
||||||
"targets": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"$ref": "#/$defs/non_empty_string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"log_target_filter": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"target",
|
|
||||||
"level"
|
|
||||||
],
|
|
||||||
"properties": {
|
|
||||||
"target": {
|
|
||||||
"$ref": "#/$defs/non_empty_string"
|
|
||||||
},
|
|
||||||
"level": {
|
|
||||||
"$ref": "#/$defs/log_level"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"database": {
|
"database": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
150
config/schemas/logging.config.schema.json
Normal file
150
config/schemas/logging.config.schema.json
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://khadhroony.local/schema/logging.config.schema.json",
|
||||||
|
"title": "Khadhroony Solana logging configuration",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"active_profile",
|
||||||
|
"profiles"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"active_profile": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
},
|
||||||
|
"profiles": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/logging_profile"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"non_empty_string": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1
|
||||||
|
},
|
||||||
|
"log_level": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"trace",
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"warn",
|
||||||
|
"error",
|
||||||
|
"off"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"logging_profile": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"default_level",
|
||||||
|
"targets",
|
||||||
|
"target_filters"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
},
|
||||||
|
"default_level": {
|
||||||
|
"$ref": "#/$defs/log_level"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"type": "array",
|
||||||
|
"minItems": 1,
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/log_target"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"target_filters": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/log_target_filter"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log_target": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"name",
|
||||||
|
"enabled",
|
||||||
|
"sink",
|
||||||
|
"level",
|
||||||
|
"path",
|
||||||
|
"rotation",
|
||||||
|
"format",
|
||||||
|
"ansi",
|
||||||
|
"targets"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"sink": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"console",
|
||||||
|
"file"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"$ref": "#/$defs/log_level"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"rotation": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"none",
|
||||||
|
"never",
|
||||||
|
"daily",
|
||||||
|
"hourly"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"format": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"human",
|
||||||
|
"compact",
|
||||||
|
"pretty",
|
||||||
|
"json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ansi": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"targets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"log_target_filter": {
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"target",
|
||||||
|
"level"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"target": {
|
||||||
|
"$ref": "#/$defs/non_empty_string"
|
||||||
|
},
|
||||||
|
"level": {
|
||||||
|
"$ref": "#/$defs/log_level"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md -->
|
<!-- file: docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# Politique de namespace Khadhroony Solana
|
# Politique de namespace Khadhroony Solana
|
||||||
|
|
||||||
@@ -65,8 +65,9 @@ La classification d'une valeur sensible doit survivre à la substitution. Une ch
|
|||||||
|
|
||||||
La restructuration `0.5.1` doit séparer au minimum :
|
La restructuration `0.5.1` doit séparer au minimum :
|
||||||
|
|
||||||
- la configuration généraliste dans son propre document et son propre schéma ;
|
- la configuration généraliste dans `config/app.config.json`, avec son schéma sous `config/schemas/app.config.schema.json` ;
|
||||||
- la configuration logging dans un document et un schéma indépendants, avec des profils logging sélectionnables indépendamment des profils réseau/applicatifs.
|
- la configuration logging dans `config/logging.config.json`, avec son schéma sous `config/schemas/logging.config.schema.json` et des profils logging sélectionnables indépendamment des profils réseau/applicatifs ;
|
||||||
|
- des exemples conformes mais non chargés par défaut sous `config/example.app.config.json` et `config/example.logging.config.json`.
|
||||||
|
|
||||||
D'autres documents spécialisés ne sont créés que si l'audit démontre une responsabilité, un cycle de vie ou une validation réellement indépendants.
|
D'autres documents spécialisés ne sont créés que si l'audit démontre une responsabilité, un cycle de vie ou une validation réellement indépendants.
|
||||||
|
|
||||||
|
|||||||
@@ -1,85 +1,95 @@
|
|||||||
<!-- file: docs/guides/CONFIGURATION.md -->
|
<!-- file: docs/guides/CONFIGURATION.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Guide de configuration
|
# Guide de configuration
|
||||||
|
|
||||||
## Objectif
|
## Objectif
|
||||||
|
|
||||||
Ce guide décrit le chargement et l’utilisation de la configuration bot3. La référence d’API détaillée reste `ks-config/USAGE.md`.
|
Ce guide décrit le chargement des documents de configuration après leur séparation. La référence d’API générale reste `ks-config/USAGE.md` et la configuration logging appartient à `ks-logging`.
|
||||||
|
|
||||||
## Fichiers actifs
|
## Fichiers actifs
|
||||||
|
|
||||||
- `config/example.config.json` : exemple utilisateur complet ;
|
- `config/app.config.json` : configuration générale chargée par défaut ;
|
||||||
- `config/schema.config.json` : contrat JSON formel ;
|
- `config/logging.config.json` : configuration logging chargée par défaut ;
|
||||||
|
- `config/example.app.config.json` et `config/example.logging.config.json` : exemples minimaux conformes ;
|
||||||
|
- `config/schemas/app.config.schema.json` : schéma général ;
|
||||||
|
- `config/schemas/logging.config.schema.json` : schéma logging ;
|
||||||
- `.env`, ou le fichier sélectionné par `KS_ENV_FILE` : valeurs d’environnement non versionnées ;
|
- `.env`, ou le fichier sélectionné par `KS_ENV_FILE` : valeurs d’environnement non versionnées ;
|
||||||
- `.env.example` : noms de variables attendues sans secrets.
|
- `.env.example` : noms de variables attendues sans secrets réels.
|
||||||
|
|
||||||
Le format actif est JSON. Le futur split de configuration prévu en `0.5.x` ne modifie pas le contrat actuel.
|
`KS_CONFIG_PATH` et `KS_LOGGING_CONFIG_PATH` permettent de remplacer indépendamment les deux chemins par défaut.
|
||||||
|
|
||||||
## Séquence de chargement
|
## Configuration générale
|
||||||
|
|
||||||
L’API recommandée est `read_config_json_file_with_environment`. Elle :
|
L’API recommandée reste `ks_config::read_config_json_file_with_environment`. Elle :
|
||||||
|
|
||||||
1. charge `.env`, ou le fichier explicitement sélectionné par `KS_ENV_FILE`, sans écraser les variables du processus ;
|
1. charge `.env`, ou le fichier sélectionné par `KS_ENV_FILE`, sans écraser les variables du processus ;
|
||||||
2. lit le JSON ;
|
2. lit le JSON général ;
|
||||||
3. résout les placeholders namespacés `${KS_*}` / `${KB_*}` et les fallbacks éventuels ;
|
3. résout les placeholders `${KS_*}` / `${KB_*}` ;
|
||||||
4. valide le schéma JSON ;
|
4. valide `config/schemas/app.config.schema.json` ;
|
||||||
5. désérialise les types de configuration et applique les invariants métier ;
|
5. désérialise `AppConfig` et applique les invariants métier ;
|
||||||
6. laisse ensuite `active_profile` sélectionner le profil actif.
|
6. laisse `active_profile` sélectionner le profil applicatif actif.
|
||||||
|
|
||||||
## Exemple opérateur
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let config = match ks_config::read_config_json_file_with_environment(
|
let config = match ks_config::read_config_json_file_with_environment(
|
||||||
std::path::Path::new("config/example.config.json"),
|
std::path::Path::new("config/app.config.json"),
|
||||||
std::path::Path::new("."),
|
std::path::Path::new("."),
|
||||||
) {
|
) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
let profile = match ks_config::active_profile(&config) {
|
let profile = match ks_config::active_profile(&config) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
println!("active profile={}", profile.name);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Le profil général ne contient plus de bloc `logging`.
|
||||||
|
|
||||||
|
## Configuration logging
|
||||||
|
|
||||||
|
`ks_logging::read_logging_json_file_with_environment` charge séparément le document logging et `ks_logging::active_logging_profile` sélectionne son profil actif.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let logging = match ks_logging::read_logging_json_file_with_environment(
|
||||||
|
std::path::Path::new("config/logging.config.json"),
|
||||||
|
std::path::Path::new("."),
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let logging_profile = match ks_logging::active_logging_profile(&logging) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Les deux `active_profile` sont indépendants. Il n’existe aucun fallback implicite du profil logging vers le nom du profil applicatif.
|
||||||
|
|
||||||
## Invariants
|
## Invariants
|
||||||
|
|
||||||
- aucun secret ne doit être ajouté à l’exemple versionné ;
|
- aucun secret réel ne doit être ajouté aux fichiers versionnés ;
|
||||||
- les placeholders non résolus doivent provoquer un diagnostic explicite ;
|
- les placeholders non résolus doivent rester détectables ;
|
||||||
- le schéma embarqué et `config/schema.config.json` doivent rester identiques ;
|
- chaque schéma embarqué doit rester identique au fichier sous `config/schemas/` ;
|
||||||
- tant que `0.5.1-pre.006` n’a pas introduit les DTO publics sûrs, la configuration runtime résolue reste strictement backend et ne doit pas être exposée telle quelle ;
|
- les profils applicatifs et logging sont validés indépendamment ;
|
||||||
- les noms de profils et rôles d’endpoints doivent rester cohérents avec leurs consommateurs.
|
- `ks-config` ne possède plus de types logging ;
|
||||||
|
- tant que `0.5.1-pre.006` n’a pas introduit les DTO sûrs, la configuration runtime résolue reste strictement backend et ne doit pas être exposée telle quelle.
|
||||||
## Quotas HTTP cumulés
|
|
||||||
|
|
||||||
Les limites HTTP sont déclarées par rôle, mais les quotas d’un endpoint public s’appliquent généralement à l’endpoint ou à l’adresse IP entière. Pour un endpoint unique, il faut donc considérer au minimum :
|
|
||||||
|
|
||||||
- la somme des `requests_per_second` de tous les rôles actifs ;
|
|
||||||
- la somme des bursts pouvant partir dans la même fenêtre ;
|
|
||||||
- la limite propre à une méthode RPC répétée ;
|
|
||||||
- les autres processus utilisant la même IP ou le même fournisseur.
|
|
||||||
|
|
||||||
Le profil public Devnet de l’exemple utilise `3 r/s` pour `http_queries` et `1 r/s` pour `http_transactions`. Un profil privé ou payant peut utiliser d’autres valeurs, mais elles doivent suivre le contrat réel du fournisseur.
|
|
||||||
|
|
||||||
## Diagnostic
|
## Diagnostic
|
||||||
|
|
||||||
Pour isoler une erreur :
|
Pour isoler une erreur :
|
||||||
|
|
||||||
1. afficher la liste des fichiers d’environnement chargés ;
|
1. vérifier le fichier d’environnement chargé ;
|
||||||
2. résoudre le JSON sans l’écrire dans les logs s’il contient des secrets ;
|
2. vérifier séparément les chemins app et logging ;
|
||||||
3. valider le schéma ;
|
3. valider chaque document contre son schéma ;
|
||||||
4. valider le modèle typé ;
|
4. vérifier son `active_profile` ;
|
||||||
5. vérifier le profil actif ;
|
5. vérifier ensuite les rôles HTTP/WebSocket, stockage ou routes logging concernés.
|
||||||
6. vérifier les rôles HTTP, WebSocket, stockage et logging.
|
|
||||||
|
Ne jamais écrire dans les logs le JSON résolu complet s’il contient une valeur issue de `KS_SECRET_*` ou `KB_SECRET_*`.
|
||||||
|
|
||||||
## Références
|
## Références
|
||||||
|
|
||||||
- `ks-config/README.md` ;
|
- `ks-config/README.md` et `ks-config/USAGE.md` ;
|
||||||
- `ks-config/USAGE.md` ;
|
- `ks-logging/README.md` et `ks-logging/USAGE.md` ;
|
||||||
- `ks-config/TODO.md` ;
|
|
||||||
- `config/README.md` ;
|
- `config/README.md` ;
|
||||||
- `docs/decisions/WINCODE_COMPATIBILITY_POLICY.md`.
|
- `docs/decisions/KHADHROONY_SOLANA_NAMESPACE_POLICY.md`.
|
||||||
|
|||||||
@@ -1,34 +1,43 @@
|
|||||||
<!-- file: docs/guides/LOGGING.md -->
|
<!-- file: docs/guides/LOGGING.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Guide de logging et tracing
|
# Guide de logging et tracing
|
||||||
|
|
||||||
## Objectif
|
## Objectif
|
||||||
|
|
||||||
`ks-logging` initialise les routes de tracing définies par la configuration et conserve les guards nécessaires à leur durée de vie.
|
`ks-logging` possède désormais le contrat logging, son document de profils, son schéma JSON et l’initialisation runtime `tracing`.
|
||||||
|
|
||||||
## Flux de démarrage
|
## Flux de démarrage
|
||||||
|
|
||||||
1. charger et valider la configuration avec `ks-config` ;
|
1. charger `config/logging.config.json` avec `ks_logging::read_logging_json_file_with_environment` ;
|
||||||
2. construire `LoggingConfig` ;
|
2. sélectionner le profil avec `ks_logging::active_logging_profile` ;
|
||||||
3. appeler `ks_logging::init_logging` une seule fois ;
|
3. appeler `ks_logging::init_logging` une seule fois ;
|
||||||
4. conserver `LoggingGuard` jusqu’à la fermeture du processus ;
|
4. conserver `LoggingGuard` jusqu’à la fermeture du processus ;
|
||||||
5. émettre les événements avec des targets canoniques.
|
5. émettre les événements avec des targets canoniques.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let guard = match ks_logging::init_logging(&config.logging) {
|
let document = match ks_logging::read_logging_json_file_with_environment(
|
||||||
|
std::path::Path::new("config/logging.config.json"),
|
||||||
|
std::path::Path::new("."),
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let config = match ks_logging::active_logging_profile(&document) {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let guard = match ks_logging::init_logging(config) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
tracing::info!(
|
|
||||||
target: ks_logging::tracing_target(),
|
|
||||||
routes = guard.route_count(),
|
|
||||||
"logging initialized"
|
|
||||||
);
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Routes
|
Le profil logging est sélectionné indépendamment du profil général de `app.config.json`.
|
||||||
|
|
||||||
|
## Contrat JSON
|
||||||
|
|
||||||
|
Le schéma est `config/schemas/logging.config.schema.json`. Les exemples sont `config/logging.config.json` pour la configuration runtime de référence et `config/example.logging.config.json` pour un exemple minimal.
|
||||||
|
|
||||||
Une route définit notamment :
|
Une route définit notamment :
|
||||||
|
|
||||||
@@ -52,28 +61,28 @@ ks-onchain-transport.http
|
|||||||
ks-lib-executor.spl.token-2022
|
ks-lib-executor.spl.token-2022
|
||||||
ks-lib-materializer.compliance.audit
|
ks-lib-materializer.compliance.audit
|
||||||
ks-lib-materializer.token.accounts
|
ks-lib-materializer.token.accounts
|
||||||
ks-lib-materializer.transaction.annotations
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour un matérialisateur, la target de tracing est l’identité runtime `ks-lib-materializer.<domain>[.<subsystem>]`. Elle reste distincte du `processorName` persisté `materializer.<domain>[.<subsystem>]`, qui ne doit pas être utilisé comme target de logs.
|
Pour un matérialisateur, la target de tracing reste distincte du `processorName` persisté.
|
||||||
|
|
||||||
Une nouvelle target doit être ajoutée selon `docs/OPERATION_NAMING_CONVENTION.md` et les règles Khadhroony.
|
|
||||||
|
|
||||||
## Frontend desktop
|
## Frontend desktop
|
||||||
|
|
||||||
Les fenêtres Tauri utilisent la permission tracing prévue par leurs capabilities. Les logs frontend sont adaptés vers le backend sans permettre au frontend de choisir arbitrairement une target sensible.
|
Le desktop charge les documents app et logging séparément. Il ne convertit plus un type `ks_config::LoggingConfig` vers `ks_logging::LoggingConfig` : le type runtime est possédé directement par `ks-logging`.
|
||||||
|
|
||||||
|
La surface publique de diagnostic/configuration reste volontairement inchangée jusqu’à `0.5.1-pre.006`, qui supprimera l’exposition des configurations résolues complètes.
|
||||||
|
|
||||||
## Diagnostic
|
## Diagnostic
|
||||||
|
|
||||||
- vérifier les routes actives via `route_names()` ;
|
- vérifier `KS_LOGGING_CONFIG_PATH` si le fichier par défaut n’est pas utilisé ;
|
||||||
|
- vérifier le profil logging actif indépendamment du profil app ;
|
||||||
|
- valider le document contre son schéma ;
|
||||||
- confirmer le niveau global et les filtres spécifiques ;
|
- confirmer le niveau global et les filtres spécifiques ;
|
||||||
- vérifier le chemin et les permissions d’une route fichier ;
|
- vérifier les chemins et permissions des routes fichier ;
|
||||||
- vérifier que le guard n’est pas détruit prématurément ;
|
|
||||||
- ne pas réinitialiser le subscriber global pendant l’exécution.
|
- ne pas réinitialiser le subscriber global pendant l’exécution.
|
||||||
|
|
||||||
## Références
|
## Références
|
||||||
|
|
||||||
- `ks-logging/README.md` ;
|
- `ks-logging/README.md` ;
|
||||||
- `ks-logging/USAGE.md` ;
|
- `ks-logging/USAGE.md` ;
|
||||||
- `ks-config/USAGE.md` ;
|
- `config/README.md` ;
|
||||||
- `docs/architecture/ARCHITECTURE.md`.
|
- `docs/OPERATION_NAMING_CONVENTION.md`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/plans/V0_5_1_KHADHROONY_SOLANA_NAMESPACE_AND_CONFIG_PLAN.md -->
|
<!-- file: docs/plans/V0_5_1_KHADHROONY_SOLANA_NAMESPACE_AND_CONFIG_PLAN.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Plan temporaire `0.5.1` — namespace Khadhroony Solana et configuration sûre
|
# Plan temporaire `0.5.1` — namespace Khadhroony Solana et configuration sûre
|
||||||
|
|
||||||
@@ -408,7 +408,7 @@ Les targets de tracing de crates génériques doivent également migrer vers `ks
|
|||||||
|
|
||||||
L'audit distingue trois ensembles afin de ne pas transformer des exemples historiques en contrats runtime :
|
L'audit distingue trois ensembles afin de ne pas transformer des exemples historiques en contrats runtime :
|
||||||
|
|
||||||
- **85 noms** actuellement utilisés/référencés par le code, les tests, `.env.example`, les fixtures ou `config/example.config.json` ;
|
- **85 noms** actuellement utilisés/référencés par le code, les tests, `.env.example`, les fixtures ou `config/app.config.json` ;
|
||||||
- **17 noms supplémentaires** présents uniquement dans le guide opérateur Devnet actif ;
|
- **17 noms supplémentaires** présents uniquement dans le guide opérateur Devnet actif ;
|
||||||
- soit **102 noms historiques actifs à migrer ou consolider**, dont plusieurs convergent volontairement vers une même cible canonique.
|
- soit **102 noms historiques actifs à migrer ou consolider**, dont plusieurs convergent volontairement vers une même cible canonique.
|
||||||
|
|
||||||
@@ -561,10 +561,12 @@ Les anciennes fixtures `TOKEN_2022_*` et les alias opérateur déjà préfixés
|
|||||||
L'audit `0.5.0` a confirmé une duplication importante : chaque profil généraliste transporte un bloc logging volumineux. La cible minimale devient :
|
L'audit `0.5.0` a confirmé une duplication importante : chaque profil généraliste transporte un bloc logging volumineux. La cible minimale devient :
|
||||||
|
|
||||||
```text
|
```text
|
||||||
config/example.config.json
|
config/app.config.json
|
||||||
config/example.logging.json
|
config/logging.config.json
|
||||||
config/schema.config.json
|
config/example.app.config.json
|
||||||
config/schema.logging.json
|
config/example.logging.config.json
|
||||||
|
config/schemas/app.config.schema.json
|
||||||
|
config/schemas/logging.config.schema.json
|
||||||
```
|
```
|
||||||
|
|
||||||
Les profils applicatifs/réseau et les profils logging sont sélectionnables indépendamment. Un profil `devnet` n'impose donc pas un profil logging `debug`, et `mainnet` n'impose pas mécaniquement un profil logging `release`.
|
Les profils applicatifs/réseau et les profils logging sont sélectionnables indépendamment. Un profil `devnet` n'impose donc pas un profil logging `debug`, et `mainnet` n'impose pas mécaniquement un profil logging `release`.
|
||||||
@@ -575,7 +577,7 @@ Le code duplique actuellement dans `ks-config` et `ks-logging` les concepts :
|
|||||||
- `LogTargetConfig` ;
|
- `LogTargetConfig` ;
|
||||||
- `LogTargetFilterConfig`.
|
- `LogTargetFilterConfig`.
|
||||||
|
|
||||||
`kb-app-demo-desktop` contient en plus une conversion manuelle de `ks_config::LoggingConfig` vers `ks_logging::LoggingConfig`. `0.5.1` doit supprimer cette duplication en donnant au logging un contrat possédé par `ks-logging`, tout en laissant `ks-config` charger/valider/composer les documents sans réinventer les types runtime du logging.
|
La migration `pre.005` ferme cette dette : `ks-config` ne définit plus de type logging, `ks-logging` possède le document, le schéma et les types runtime, et le desktop charge les deux fichiers indépendamment. `ks-logging` réutilise uniquement les helpers génériques d’environnement de `ks-config`, sans créer de dépendance inverse.
|
||||||
|
|
||||||
Aucun troisième document spécialisé n'est créé dans `0.5.1` sans bénéfice de découplage démontré.
|
Aucun troisième document spécialisé n'est créé dans `0.5.1` sans bénéfice de découplage démontré.
|
||||||
|
|
||||||
@@ -628,7 +630,7 @@ Avant chaque changement structurel correspondant, conserver ou ajouter des tests
|
|||||||
- l'interdiction des variables de configuration possédées par le workspace hors namespace d'ownership `KS_*` ou `KB_*` ;
|
- l'interdiction des variables de configuration possédées par le workspace hors namespace d'ownership `KS_*` ou `KB_*` ;
|
||||||
- la propagation de sensibilité depuis `KS_SECRET_*` vers les valeurs composées ;
|
- la propagation de sensibilité depuis `KS_SECRET_*` vers les valeurs composées ;
|
||||||
- l'impossibilité pour une sentinelle secrète d'apparaître dans `Debug`, logs, erreurs, payloads Tauri, sérialisation publique ou diagnostic ;
|
- l'impossibilité pour une sentinelle secrète d'apparaître dans `Debug`, logs, erreurs, payloads Tauri, sérialisation publique ou diagnostic ;
|
||||||
- la validation indépendante de `schema.config.json` et `schema.logging.json` ;
|
- la validation indépendante de `config/schemas/app.config.schema.json` et `config/schemas/logging.config.schema.json` ;
|
||||||
- l'indépendance des profils généralistes et logging ;
|
- l'indépendance des profils généralistes et logging ;
|
||||||
- l'absence de duplication structurelle des types logging entre `ks-config` et `ks-logging` ;
|
- l'absence de duplication structurelle des types logging entre `ks-config` et `ks-logging` ;
|
||||||
- le maintien des contrats publics réellement nécessaires après remplacement des types de configuration exposés.
|
- le maintien des contrats publics réellement nécessaires après remplacement des types de configuration exposés.
|
||||||
@@ -672,10 +674,12 @@ Les tests ne doivent pas figer comme comportement légitime la fuite actuelle de
|
|||||||
|
|
||||||
### `0.5.1-pre.005` — split config/logging
|
### `0.5.1-pre.005` — split config/logging
|
||||||
|
|
||||||
- extraire `example.logging.json` et `schema.logging.json` ;
|
- **implémenté** : `config/app.config.json` et `config/logging.config.json` deviennent les deux documents chargés par défaut ;
|
||||||
- rendre profils logging et généralistes indépendants ;
|
- **implémenté** : les schémas sont centralisés sous `config/schemas/app.config.schema.json` et `config/schemas/logging.config.schema.json` ;
|
||||||
- supprimer les structures logging dupliquées et la conversion desktop manuelle ;
|
- **implémenté** : `example.app.config.json` et `example.logging.config.json` fournissent des exemples minimaux conformes ;
|
||||||
- maintenir schémas, exemples et documentation synchronisés.
|
- **implémenté** : les profils logging et généralistes possèdent chacun leur propre `active_profile` ;
|
||||||
|
- **implémenté** : `ks-logging` devient propriétaire unique du contrat logging et de sa validation ;
|
||||||
|
- **implémenté** : le desktop supprime la conversion `ks_config::LoggingConfig` -> `ks_logging::LoggingConfig`.
|
||||||
|
|
||||||
### `0.5.1-pre.006` — surfaces publiques sûres
|
### `0.5.1-pre.006` — surfaces publiques sûres
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: docs/rules/RULES_SPECIFIC_KHADHROONY.md -->
|
<!-- file: docs/rules/RULES_SPECIFIC_KHADHROONY.md -->
|
||||||
<!-- version: 15 -->
|
<!-- version: 16 -->
|
||||||
|
|
||||||
# Règles spécifiques à `khadhroony-bot3`
|
# Règles spécifiques à `khadhroony-bot3`
|
||||||
|
|
||||||
@@ -238,6 +238,9 @@ Aucun renommage massif de modules n'est autorisé sans étape de contrôle dédi
|
|||||||
## Règles de configuration
|
## Règles de configuration
|
||||||
|
|
||||||
- La configuration applicative commune doit passer par `ks-config`.
|
- La configuration applicative commune doit passer par `ks-config`.
|
||||||
|
- Les fichiers runtime chargés par défaut sont `config/app.config.json` pour la configuration générale et `config/logging.config.json` pour le logging ; leurs sélections de profils sont indépendantes.
|
||||||
|
- Les schémas JSON de configuration actifs sont conservés sous `config/schemas/`, notamment `app.config.schema.json` et `logging.config.schema.json`.
|
||||||
|
- Les exemples conformes sont conservés sous `config/` avec des noms distincts des fichiers runtime, notamment `example.app.config.json` et `example.logging.config.json`.
|
||||||
- Les fichiers JSON de configuration ne doivent pas contenir de commentaires.
|
- Les fichiers JSON de configuration ne doivent pas contenir de commentaires.
|
||||||
- Les secrets ne doivent pas être écrits en clair dans le dépôt.
|
- Les secrets ne doivent pas être écrits en clair dans le dépôt.
|
||||||
- Les valeurs sensibles doivent utiliser des variables d'environnement ou un stockage chiffré dédié.
|
- Les valeurs sensibles doivent utiliser des variables d'environnement ou un stockage chiffré dédié.
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
<!-- file: kb-app-demo-desktop/CHANGELOG.md -->
|
||||||
<!-- version: 44 -->
|
<!-- version: 45 -->
|
||||||
|
|
||||||
# CHANGELOG — kb-app-demo-desktop
|
# CHANGELOG — kb-app-demo-desktop
|
||||||
|
|
||||||
|
## `0.5.1-pre.005`
|
||||||
|
|
||||||
|
- charge séparément `config/app.config.json` et `config/logging.config.json` ;
|
||||||
|
- ajoute `KS_LOGGING_CONFIG_PATH` sans coupler sa sélection à `KS_CONFIG_PATH` ;
|
||||||
|
- initialise `ks-logging` directement depuis son profil actif et supprime la conversion manuelle depuis un type logging de `ks-config` ;
|
||||||
|
- conserve les payloads Tauri de configuration existants jusqu’à la fermeture de la surface publique en `pre.006`.
|
||||||
|
|
||||||
## `0.5.1-pre.004`
|
## `0.5.1-pre.004`
|
||||||
|
|
||||||
- lit désormais le chemin de configuration Solana via `KS_CONFIG_PATH` et les fixtures Token-2022 via leurs noms `KS_PUBLIC_*` canoniques ;
|
- lit désormais le chemin de configuration Solana via `KS_CONFIG_PATH` et les fixtures Token-2022 via leurs noms `KS_PUBLIC_*` canoniques ;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
// file: kb-app-demo-desktop/src/app_state.rs
|
// file: kb-app-demo-desktop/src/app_state.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
//! Shared Tauri application state and startup initialization.
|
//! Shared Tauri application state and startup initialization.
|
||||||
|
|
||||||
/// Shared state managed by Tauri for the desktop demo application.
|
/// Shared state managed by Tauri for the desktop demo application.
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
config_path: std::string::String,
|
config_path: std::string::String,
|
||||||
|
logging_config_path: std::string::String,
|
||||||
app_config: ks_config::AppConfig,
|
app_config: ks_config::AppConfig,
|
||||||
active_profile: ks_config::ProfileConfig,
|
active_profile: ks_config::ProfileConfig,
|
||||||
logging_guard: std::sync::Mutex<ks_logging::LoggingGuard>,
|
logging_guard: std::sync::Mutex<ks_logging::LoggingGuard>,
|
||||||
@@ -30,6 +31,7 @@ impl crate::AppState {
|
|||||||
/// Initializes configuration, logging and shared runtime state.
|
/// Initializes configuration, logging and shared runtime state.
|
||||||
pub(crate) fn initialize() -> ks_core::Result<crate::AppState> {
|
pub(crate) fn initialize() -> ks_core::Result<crate::AppState> {
|
||||||
let config_path = resolve_config_path();
|
let config_path = resolve_config_path();
|
||||||
|
let logging_config_path = resolve_logging_config_path();
|
||||||
let workspace_root = crate::workspace_root_dir();
|
let workspace_root = crate::workspace_root_dir();
|
||||||
let app_config = match ks_config::read_config_json_file_with_environment(
|
let app_config = match ks_config::read_config_json_file_with_environment(
|
||||||
&config_path,
|
&config_path,
|
||||||
@@ -42,7 +44,17 @@ impl crate::AppState {
|
|||||||
std::result::Result::Ok(profile) => profile.clone(),
|
std::result::Result::Ok(profile) => profile.clone(),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
};
|
};
|
||||||
let logging_config = convert_logging_config(&active_profile.logging);
|
let logging_document = match ks_logging::read_logging_json_file_with_environment(
|
||||||
|
&logging_config_path,
|
||||||
|
&workspace_root,
|
||||||
|
) {
|
||||||
|
std::result::Result::Ok(config) => config,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let logging_config = match ks_logging::active_logging_profile(&logging_document) {
|
||||||
|
std::result::Result::Ok(profile) => profile.clone(),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
let logging_guard = match ks_logging::init_logging(&logging_config) {
|
let logging_guard = match ks_logging::init_logging(&logging_config) {
|
||||||
std::result::Result::Ok(guard) => guard,
|
std::result::Result::Ok(guard) => guard,
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
@@ -54,6 +66,7 @@ impl crate::AppState {
|
|||||||
};
|
};
|
||||||
return std::result::Result::Ok(crate::AppState {
|
return std::result::Result::Ok(crate::AppState {
|
||||||
config_path: config_path.display().to_string(),
|
config_path: config_path.display().to_string(),
|
||||||
|
logging_config_path: logging_config_path.display().to_string(),
|
||||||
app_config,
|
app_config,
|
||||||
active_profile,
|
active_profile,
|
||||||
logging_guard: std::sync::Mutex::new(logging_guard),
|
logging_guard: std::sync::Mutex::new(logging_guard),
|
||||||
@@ -78,6 +91,11 @@ impl crate::AppState {
|
|||||||
return self.config_path.as_str();
|
return self.config_path.as_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the path used to load the logging configuration file.
|
||||||
|
pub(crate) fn logging_config_path(&self) -> &str {
|
||||||
|
return self.logging_config_path.as_str();
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the complete parsed application configuration.
|
/// Returns the complete parsed application configuration.
|
||||||
pub(crate) fn app_config(&self) -> &ks_config::AppConfig {
|
pub(crate) fn app_config(&self) -> &ks_config::AppConfig {
|
||||||
return &self.app_config;
|
return &self.app_config;
|
||||||
@@ -198,40 +216,26 @@ impl crate::AppState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn convert_logging_config(config: &ks_config::LoggingConfig) -> ks_logging::LoggingConfig {
|
|
||||||
let mut targets = std::vec::Vec::<ks_logging::LogTargetConfig>::new();
|
|
||||||
for target in &config.targets {
|
|
||||||
targets.push(ks_logging::LogTargetConfig {
|
|
||||||
name: target.name.clone(),
|
|
||||||
enabled: target.enabled,
|
|
||||||
sink: target.sink.clone(),
|
|
||||||
level: target.level.clone(),
|
|
||||||
path: target.path.clone(),
|
|
||||||
rotation: target.rotation.clone(),
|
|
||||||
format: target.format.clone(),
|
|
||||||
ansi: target.ansi,
|
|
||||||
targets: target.targets.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
let mut target_filters = std::vec::Vec::<ks_logging::LogTargetFilterConfig>::new();
|
|
||||||
for filter in &config.target_filters {
|
|
||||||
target_filters.push(ks_logging::LogTargetFilterConfig {
|
|
||||||
target: filter.target.clone(),
|
|
||||||
level: filter.level.clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return ks_logging::LoggingConfig {
|
|
||||||
default_level: config.default_level.clone(),
|
|
||||||
targets,
|
|
||||||
target_filters,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_config_path() -> std::path::PathBuf {
|
fn resolve_config_path() -> std::path::PathBuf {
|
||||||
let configured = std::env::var("KS_CONFIG_PATH").ok();
|
let configured = std::env::var("KS_CONFIG_PATH").ok();
|
||||||
return resolve_config_path_from_value(configured.as_deref());
|
return resolve_config_path_from_value(configured.as_deref());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn resolve_logging_config_path() -> std::path::PathBuf {
|
||||||
|
let configured = std::env::var("KS_LOGGING_CONFIG_PATH").ok();
|
||||||
|
return resolve_logging_config_path_from_value(configured.as_deref());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_logging_config_path_from_value(value: std::option::Option<&str>) -> std::path::PathBuf {
|
||||||
|
if let std::option::Option::Some(configured) = value {
|
||||||
|
let trimmed = configured.trim();
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
return std::path::PathBuf::from(trimmed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return crate::workspace_root_dir().join("config/logging.config.json");
|
||||||
|
}
|
||||||
|
|
||||||
fn resolve_config_path_from_value(value: std::option::Option<&str>) -> std::path::PathBuf {
|
fn resolve_config_path_from_value(value: std::option::Option<&str>) -> std::path::PathBuf {
|
||||||
if let std::option::Option::Some(configured) = value {
|
if let std::option::Option::Some(configured) = value {
|
||||||
let trimmed = configured.trim();
|
let trimmed = configured.trim();
|
||||||
@@ -239,17 +243,26 @@ fn resolve_config_path_from_value(value: std::option::Option<&str>) -> std::path
|
|||||||
return std::path::PathBuf::from(trimmed);
|
return std::path::PathBuf::from(trimmed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return crate::workspace_root_dir().join("config/example.config.json");
|
return crate::workspace_root_dir().join("config/app.config.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn resolve_config_path_uses_workspace_example_by_default() {
|
fn resolve_config_path_uses_workspace_default_by_default() {
|
||||||
let path = super::resolve_config_path_from_value(std::option::Option::None);
|
let path = super::resolve_config_path_from_value(std::option::Option::None);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
path.file_name().and_then(std::ffi::OsStr::to_str),
|
path.file_name().and_then(std::ffi::OsStr::to_str),
|
||||||
std::option::Option::Some("example.config.json")
|
std::option::Option::Some("app.config.json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolve_logging_config_path_uses_workspace_default_by_default() {
|
||||||
|
let path = super::resolve_logging_config_path_from_value(std::option::Option::None);
|
||||||
|
assert_eq!(
|
||||||
|
path.file_name().and_then(std::ffi::OsStr::to_str),
|
||||||
|
std::option::Option::Some("logging.config.json")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
// file: kb-app-demo-desktop/src/demo_execution_solana_core.rs
|
||||||
// version: 14
|
// version: 15
|
||||||
|
|
||||||
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
//! Tauri adapter for bounded Solana Core execution on Devnet.
|
||||||
|
|
||||||
@@ -851,7 +851,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn example_config_exposes_one_devnet_execution_profile() {
|
fn example_config_exposes_one_devnet_execution_profile() {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
match ks_config::parse_config_json(include_str!("../../config/app.config.json")) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
};
|
};
|
||||||
@@ -865,7 +865,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn profile_selection_rejects_mainnet_profiles() {
|
fn profile_selection_rejects_mainnet_profiles() {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
match ks_config::parse_config_json(include_str!("../../config/app.config.json")) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: kb-app-demo-desktop/src/tauri.rs
|
// file: kb-app-demo-desktop/src/tauri.rs
|
||||||
// version: 35
|
// version: 36
|
||||||
|
|
||||||
//! Tauri runtime assembly and private command wrappers.
|
//! Tauri runtime assembly and private command wrappers.
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ pub fn run() -> ks_core::Result<()> {
|
|||||||
tracing::info!(
|
tracing::info!(
|
||||||
target: crate::TRACING_TARGET,
|
target: crate::TRACING_TARGET,
|
||||||
config_path = app_state.config_path(),
|
config_path = app_state.config_path(),
|
||||||
|
logging_config_path = app_state.logging_config_path(),
|
||||||
active_profile = app_state.active_profile().name.as_str(),
|
active_profile = app_state.active_profile().name.as_str(),
|
||||||
logging_routes = app_state.logging_route_count(),
|
logging_routes = app_state.logging_route_count(),
|
||||||
configured_profiles = app_state.app_config().profiles.len(),
|
configured_profiles = app_state.app_config().profiles.len(),
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
<!-- file: ks-config/CHANGELOG.md -->
|
<!-- file: ks-config/CHANGELOG.md -->
|
||||||
<!-- version: 14 -->
|
<!-- version: 15 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-config
|
# CHANGELOG — ks-config
|
||||||
|
|
||||||
|
## `0.5.1-pre.005`
|
||||||
|
|
||||||
|
- retire définitivement le bloc `logging` de `ProfileConfig` et du schéma général ;
|
||||||
|
- aligne le contrat général et le desktop sur `config/app.config.json`, validé par `config/schemas/app.config.schema.json` ;
|
||||||
|
- conserve des profils applicatifs indépendants du document logging possédé par `ks-logging` ;
|
||||||
|
- ajoute `config/example.app.config.json` comme exemple minimal conforme.
|
||||||
|
|
||||||
## `0.5.1-pre.004`
|
## `0.5.1-pre.004`
|
||||||
|
|
||||||
- migre le sélecteur d'environnement vers `KS_ENV_FILE` et les placeholders PostgreSQL/Helius vers `KS_SECRET_*` ;
|
- migre le sélecteur d'environnement vers `KS_ENV_FILE` et les placeholders PostgreSQL/Helius vers `KS_SECRET_*` ;
|
||||||
|
|||||||
@@ -1,41 +1,47 @@
|
|||||||
<!-- file: ks-config/README.md -->
|
<!-- file: ks-config/README.md -->
|
||||||
<!-- version: 7 -->
|
<!-- version: 8 -->
|
||||||
|
|
||||||
# ks-config
|
# ks-config
|
||||||
|
|
||||||
`ks-config` définit le contrat de configuration typé du workspace, son schéma JSON embarqué et les fonctions de chargement, résolution d’environnement, validation et sérialisation.
|
`ks-config` définit le contrat de configuration générale typé du workspace, son schéma JSON embarqué et les fonctions de chargement, résolution d’environnement, validation et sérialisation.
|
||||||
|
|
||||||
## Responsabilités
|
## Responsabilités
|
||||||
|
|
||||||
- exposer `AppConfig` et les sections de configuration publiques ;
|
- exposer `AppConfig` et les sections de configuration générale ;
|
||||||
- valider le JSON contre le schéma embarqué ;
|
- valider le JSON général contre `config/schemas/app.config.schema.json` ;
|
||||||
- appliquer les invariants métier après désérialisation ;
|
- appliquer les invariants métier après désérialisation ;
|
||||||
- charger `.env`, ou le fichier explicitement sélectionné par `KS_ENV_FILE`, depuis la racine du workspace ;
|
- charger `.env`, ou le fichier sélectionné par `KS_ENV_FILE`, depuis la racine du workspace ;
|
||||||
- résoudre les placeholders namespacés `${KS_*}` / `${KB_*}` et leurs fallbacks ;
|
- résoudre les placeholders namespacés `${KS_*}` / `${KB_*}` et leurs fallbacks ;
|
||||||
- sélectionner le profil actif ;
|
- sélectionner le profil applicatif actif ;
|
||||||
- exporter les types nécessaires au frontend avec `ts-rs`.
|
- exporter les types généraux nécessaires au frontend avec `ts-rs`.
|
||||||
|
|
||||||
## Hors périmètre
|
## Hors périmètre
|
||||||
|
|
||||||
La crate n’initialise ni le logging, ni PostgreSQL, ni les transports et ne manipule aucun secret de wallet. Elle fournit uniquement la configuration validée à ces consommateurs.
|
`ks-config` ne possède plus le contrat logging. `LoggingConfig`, les routes, filtres, profils logging et `config/schemas/logging.config.schema.json` appartiennent à `ks-logging`.
|
||||||
|
|
||||||
|
La crate n’initialise ni PostgreSQL, ni les transports et ne manipule aucun secret de wallet. Elle fournit uniquement la configuration générale validée à ces consommateurs.
|
||||||
|
|
||||||
## Surface publique
|
## Surface publique
|
||||||
|
|
||||||
Les principales fonctions sont `read_config_json_file_with_environment`, `parse_config_json`, `validate_config`, `validate_config_json_schema`, `active_profile` et les sérialiseurs JSON. Les types publics couvrent les profils, endpoints, listeners, logging, base de données, wallet, exécution et démonstration.
|
Les principales fonctions sont `read_config_json_file_with_environment`, `parse_config_json`, `validate_config`, `validate_config_json_schema`, `active_profile` et les sérialiseurs JSON.
|
||||||
|
|
||||||
|
Les types publics couvrent les profils applicatifs, endpoints, listeners, base de données, wallet, exécution et démonstration.
|
||||||
|
|
||||||
## Relations
|
## Relations
|
||||||
|
|
||||||
- dépend de `ks-core` pour les erreurs structurées ;
|
- dépend de `ks-core` pour les erreurs structurées ;
|
||||||
- alimente `ks-logging`, `ks-store`, `ks-onchain-transport`, `ks-pipeline`, `ks-wallet` et les applications ;
|
- fournit les profils généraux à `ks-store`, `ks-onchain-transport`, `ks-pipeline`, `ks-wallet` et aux applications ;
|
||||||
- utilise [`../config/example.config.json`](../config/example.config.json) et [`../config/schema.config.json`](../config/schema.config.json) comme exemple utilisateur et contrat de schéma actifs.
|
- fournit à `ks-logging` les helpers génériques de chargement `.env` et de résolution des placeholders ;
|
||||||
|
- embarque [`../config/schemas/app.config.schema.json`](../config/schemas/app.config.schema.json) ;
|
||||||
|
- utilise [`../config/app.config.json`](../config/app.config.json) comme configuration générale chargée par défaut et [`../config/example.app.config.json`](../config/example.app.config.json) comme exemple minimal conforme.
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
La configuration actuelle est fonctionnelle et validée, mais son format `0.4.8` reste monolithique. Le cadrage `0.5.0-pre.002` confirme que `0.5.1` séparera la configuration générale et le logging en documents et schémas distincts, applique les namespaces d’environnement `KS_*` / `KB_*` par ownership et séparera les représentations source, runtime et publiques afin qu’aucun secret résolu ne soit exposé.
|
Le split config/logging est effectif : un profil applicatif ne transporte plus de bloc logging et la sélection logging est indépendante. La prerelease suivante sépare les représentations source/runtime/public/diagnostic et ferme l’exposition des secrets résolus.
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|
||||||
- [Utilisation](USAGE.md)
|
- [Utilisation](USAGE.md)
|
||||||
- [Travaux restants](TODO.md)
|
- [Travaux restants](TODO.md)
|
||||||
- [Historique](CHANGELOG.md)
|
- [Historique](CHANGELOG.md)
|
||||||
- [Architecture](../docs/architecture/ARCHITECTURE.md)
|
- [Configuration locale](../config/README.md)
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
<!-- file: ks-config/TODO.md -->
|
<!-- file: ks-config/TODO.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# TODO — ks-config
|
# TODO — ks-config
|
||||||
|
|
||||||
## Série `0.5.x`
|
## Série `0.5.x`
|
||||||
|
|
||||||
- [ ] `0.5.1` - séparer la configuration générale et le logging en documents et schémas JSON distincts.
|
|
||||||
- [ ] `0.5.1` - rendre la sélection du profil logging indépendante du profil généraliste.
|
|
||||||
- [ ] `0.5.1` - définir un propriétaire unique du contrat source logging et supprimer la conversion manuelle du desktop.
|
|
||||||
- [ ] `0.5.1` - appliquer à l’exécution les classes `KS_SECRET_*` / `KB_SECRET_*`, `KS_PUBLIC_*` / `KB_PUBLIC_*` et internes, avec propagation de sensibilité aux valeurs composées.
|
- [ ] `0.5.1` - appliquer à l’exécution les classes `KS_SECRET_*` / `KB_SECRET_*`, `KS_PUBLIC_*` / `KB_PUBLIC_*` et internes, avec propagation de sensibilité aux valeurs composées.
|
||||||
- [ ] `0.5.1` - séparer les représentations source, runtime résolue, publique et diagnostic.
|
- [ ] `0.5.1` - séparer les représentations source, runtime résolue, publique et diagnostic.
|
||||||
- [ ] `0.5.1` - interdire qu’un secret résolu soit sérialisé, loggé, inclus dans une erreur ou transmis via Tauri.
|
- [ ] `0.5.1` - interdire qu’un secret résolu soit sérialisé, loggé, inclus dans une erreur ou transmis via Tauri.
|
||||||
- [ ] `0.5.1` - supprimer l’exposition frontend de `AppConfig` et `ProfileConfig` résolus complets.
|
- [ ] `0.5.1` - supprimer l’exposition frontend de `AppConfig` et `ProfileConfig` résolus complets.
|
||||||
- [ ] Contrat - maintenir l’identité entre chaque schéma embarqué et son fichier sous `config/`.
|
- [ ] Contrat - maintenir l’identité entre chaque schéma embarqué et son fichier sous `config/schemas/`.
|
||||||
- [ ] Tests - ajouter les tests d’API externe, de migration multi-fichiers et les canaris de non-divulgation.
|
- [ ] Tests - ajouter les tests d’API externe et les canaris de non-divulgation.
|
||||||
- [ ] Documentation - mettre à jour guides, exemples et `.env.example` seulement avec l’implémentation correspondante.
|
- [ ] Documentation - maintenir guides, exemples et `.env.example` avec l’implémentation correspondante.
|
||||||
- [ ] Intégration - coordonner la migration avec logging, transports, pipeline, scénarios et applications.
|
- [ ] Intégration - coordonner les surfaces publiques sûres avec logging, transports, pipeline, scénarios et applications.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-config/USAGE.md -->
|
<!-- file: ks-config/USAGE.md -->
|
||||||
<!-- version: 6 -->
|
<!-- version: 7 -->
|
||||||
|
|
||||||
# Utilisation de ks-config
|
# Utilisation de ks-config
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
```rust
|
```rust
|
||||||
let config_result = ks_config::read_config_json_file_with_environment(
|
let config_result = ks_config::read_config_json_file_with_environment(
|
||||||
std::path::Path::new("config/example.config.json"),
|
std::path::Path::new("config/app.config.json"),
|
||||||
std::path::Path::new("."),
|
std::path::Path::new("."),
|
||||||
);
|
);
|
||||||
let config = match config_result {
|
let config = match config_result {
|
||||||
@@ -20,7 +20,9 @@ let profile = match ks_config::active_profile(&config) {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Cette fonction charge l’environnement du workspace, résout les placeholders puis applique successivement le schéma JSON, la désérialisation typée et les invariants métier.
|
Cette API charge l’environnement du workspace, résout les placeholders puis applique successivement le schéma JSON général, la désérialisation typée et les invariants métier.
|
||||||
|
|
||||||
|
Le fichier chargé par défaut par le desktop est `config/app.config.json`. `KS_CONFIG_PATH` permet de remplacer explicitement ce chemin.
|
||||||
|
|
||||||
## Chargement de l’environnement
|
## Chargement de l’environnement
|
||||||
|
|
||||||
@@ -35,18 +37,20 @@ if let Some(path) = report.loaded_path {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
`EnvironmentLoadReport` indique le fichier chargé, s’il existe. Par défaut, `ks-config` sélectionne `.env`; `KS_ENV_FILE` permet de choisir explicitement un autre fichier sans écraser les variables déjà présentes dans le processus. Les placeholders non résolus sans fallback restent visibles afin que la validation ou le consommateur puisse les signaler explicitement. Les contrats d’environnement du workspace utilisent désormais `KS_*` pour Khadhroony Solana et `KB_*` pour les futurs besoins réellement propres au Bot.
|
`EnvironmentLoadReport` indique le fichier chargé. Par défaut, `ks-config` sélectionne `.env`; `KS_ENV_FILE` permet d’en choisir un autre sans écraser les variables déjà présentes dans le processus.
|
||||||
|
|
||||||
|
Les contrats d’environnement utilisent `KS_*` pour Khadhroony Solana et `KB_*` pour les besoins réellement propres au Bot.
|
||||||
|
|
||||||
## Résolution explicite des placeholders
|
## Résolution explicite des placeholders
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let raw = r#"{"databaseUrl":"${KS_SECRET_POSTGRES_DEVNET_URL:-postgres://localhost/kb}"}"#;
|
let raw = r#"{"databaseUrl":"${KS_SECRET_POSTGRES_DEVNET_URL:-postgres://localhost/ks}"}"#;
|
||||||
let resolved = ks_config::resolve_environment_placeholders(raw);
|
let resolved = ks_config::resolve_environment_placeholders(raw);
|
||||||
|
|
||||||
assert!(resolved.contains("databaseUrl"));
|
assert!(resolved.contains("databaseUrl"));
|
||||||
```
|
```
|
||||||
|
|
||||||
Cette API retourne actuellement une chaîne résolue ordinaire et peut donc contenir des secrets issus de l’environnement. Elle doit rester strictement backend et ne doit pas être utilisée pour afficher, logger ou transmettre le JSON résolu. `0.5.1` remplacera cette frontière par une résolution conservant la classification de sensibilité.
|
Cette API retourne encore une chaîne ordinaire pouvant contenir des secrets résolus. Elle reste strictement backend jusqu’à l’introduction de la représentation sensible de `0.5.1-pre.006`.
|
||||||
|
|
||||||
## Validation et parsing
|
## Validation et parsing
|
||||||
|
|
||||||
@@ -65,29 +69,6 @@ if let Err(error) = ks_config::validate_config(&config) {
|
|||||||
|
|
||||||
`parse_config_json` effectue déjà les deux validations ; les appels séparés servent aux outils de diagnostic.
|
`parse_config_json` effectue déjà les deux validations ; les appels séparés servent aux outils de diagnostic.
|
||||||
|
|
||||||
## Sérialisation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
let compact = match ks_config::serialize_config_json(&config) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
|
||||||
let pretty = match ks_config::serialize_config_json_pretty(&config) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
|
||||||
|
|
||||||
let write_result = std::fs::write("config/generated.config.json", pretty);
|
|
||||||
if let Err(error) = write_result {
|
|
||||||
return Err(ks_core::Error::new(
|
|
||||||
"config_write_failed",
|
|
||||||
format!("cannot write generated configuration: {error}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
La configuration est validée avant sérialisation.
|
|
||||||
|
|
||||||
## Schéma embarqué
|
## Schéma embarqué
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -97,15 +78,11 @@ let schema_value = match ks_config::config_json_schema_value() {
|
|||||||
Err(error) => return Err(error),
|
Err(error) => return Err(error),
|
||||||
};
|
};
|
||||||
|
|
||||||
let property_count = schema_value
|
println!("embedded app schema bytes={}", schema_text.len());
|
||||||
.get("properties")
|
assert!(schema_value.is_object());
|
||||||
.and_then(serde_json::Value::as_object)
|
|
||||||
.map_or(0, serde_json::Map::len);
|
|
||||||
|
|
||||||
println!("embedded schema bytes={}, properties={property_count}", schema_text.len());
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Le schéma actif est aussi disponible sous [`../config/schema.config.json`](../config/schema.config.json). Le fichier [`../config/example.config.json`](../config/example.config.json) fournit un exemple utilisateur complet.
|
Le schéma actif est [`../config/schemas/app.config.schema.json`](../config/schemas/app.config.schema.json). Les fichiers [`../config/app.config.json`](../config/app.config.json) et [`../config/example.app.config.json`](../config/example.app.config.json) doivent tous deux être conformes.
|
||||||
|
|
||||||
## Sélection du profil actif
|
## Sélection du profil actif
|
||||||
|
|
||||||
@@ -120,24 +97,47 @@ println!("http endpoints: {}", profile.solana.http_endpoints.len());
|
|||||||
println!("websocket endpoints: {}", profile.solana.ws_endpoints.len());
|
println!("websocket endpoints: {}", profile.solana.ws_endpoints.len());
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Le profil applicatif ne contient plus de configuration logging. Le document logging et son `active_profile` sont chargés par `ks-logging`.
|
||||||
|
|
||||||
|
## Sérialisation
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let pretty = match ks_config::serialize_config_json_pretty(&config) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
|
||||||
|
let write_result = std::fs::write("config/generated.app.config.json", pretty);
|
||||||
|
if let Err(error) = write_result {
|
||||||
|
return Err(ks_core::Error::new(
|
||||||
|
"config_write_failed",
|
||||||
|
format!("cannot write generated configuration: {error}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
La configuration est validée avant sérialisation.
|
||||||
|
|
||||||
## Types publics importants
|
## Types publics importants
|
||||||
|
|
||||||
- `AppConfig`, `ProfileConfig`, `AppSectionConfig` ;
|
- `AppConfig`, `ProfileConfig`, `AppSectionConfig` ;
|
||||||
- `DatabaseConfig`, `PostgresConfig`, `SqliteConfig`, `DataConfig` ;
|
- `DatabaseConfig`, `PostgresConfig`, `SqliteConfig`, `DataConfig` ;
|
||||||
- `SolanaConfig`, `HttpEndpointConfig`, `WsEndpointConfig`, `EndpointRoleConfig` ;
|
- `SolanaConfig`, `HttpEndpointConfig`, `WsEndpointConfig`, `EndpointRoleConfig` ;
|
||||||
- `ListenerConfig` et ses variantes ;
|
- `ListenerConfig` et ses variantes ;
|
||||||
- `LoggingConfig`, `LogTargetConfig`, `LogTargetFilterConfig` ;
|
|
||||||
- `WalletConfig`, `ExecutionConfig`, `DemoConfig`.
|
- `WalletConfig`, `ExecutionConfig`, `DemoConfig`.
|
||||||
|
|
||||||
|
Les types logging ne font plus partie de `ks-config`.
|
||||||
|
|
||||||
## Erreurs et invariants
|
## Erreurs et invariants
|
||||||
|
|
||||||
Les erreurs utilisent `ks_core::Error` avec un code stable. Les validations couvrent notamment l’unicité des profils, l’existence du profil actif, les URLs, les rôles d’endpoints, les limites d’exécution, les routes de logging et les contraintes wallet.
|
Les erreurs utilisent `ks_core::Error` avec un code stable. Les validations couvrent notamment l’unicité des profils, l’existence du profil actif, les URLs, les rôles d’endpoints, les limites d’exécution et les contraintes wallet.
|
||||||
|
|
||||||
## Tests instructifs
|
## Tests instructifs
|
||||||
|
|
||||||
Les tests `example_config_validates_against_schema`, `example_config_parses_and_resolves_active_profile` et `example_config_routes_global_and_operational_crate_files` démontrent le contrat complet de l’exemple actif. Les tests `parser_rejects_*` et `schema_rejects_*` documentent les invariants refusés.
|
Les tests vérifient séparément `app.config.json` et `example.app.config.json`, le roundtrip du document général et les invariants `parser_rejects_*` / `schema_rejects_*`.
|
||||||
|
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
- format JSON uniquement ;
|
- format JSON uniquement ;
|
||||||
- la crate valide les références et paramètres, mais n’ouvre aucune connexion externe.
|
- aucune connexion externe n’est ouverte par la crate ;
|
||||||
|
- la politique de camouflage des valeurs résolues appartient à `0.5.1-pre.006`.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// file: ks-config/src/lib.rs
|
// file: ks-config/src/lib.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Khadhroony Bot3 workspace configuration contract and loading helpers.
|
//! Khadhroony Solana application configuration contract and loading helpers.
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
#![deny(unreachable_pub)]
|
#![deny(unreachable_pub)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -37,12 +37,6 @@ pub use self::settings::HttpEndpointConfig;
|
|||||||
pub use self::settings::ListenerConfig;
|
pub use self::settings::ListenerConfig;
|
||||||
/// Exposes the log listener configuration type.
|
/// Exposes the log listener configuration type.
|
||||||
pub use self::settings::LogListenerConfig;
|
pub use self::settings::LogListenerConfig;
|
||||||
/// Exposes the logging target configuration type.
|
|
||||||
pub use self::settings::LogTargetConfig;
|
|
||||||
/// Exposes the logging target filter configuration type.
|
|
||||||
pub use self::settings::LogTargetFilterConfig;
|
|
||||||
/// Exposes the logging configuration type.
|
|
||||||
pub use self::settings::LoggingConfig;
|
|
||||||
/// Exposes the PostgreSQL configuration type.
|
/// Exposes the PostgreSQL configuration type.
|
||||||
pub use self::settings::PostgresConfig;
|
pub use self::settings::PostgresConfig;
|
||||||
/// Exposes the profile configuration type.
|
/// Exposes the profile configuration type.
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
// file: ks-config/src/settings.rs
|
// file: ks-config/src/settings.rs
|
||||||
// version: 19
|
// version: 20
|
||||||
|
|
||||||
//! Typed configuration models shared by applications and workers.
|
//! Typed configuration models shared by applications and workers.
|
||||||
|
|
||||||
use ts_rs::TS; // rust-rules: derive-import
|
use ts_rs::TS; // rust-rules: derive-import
|
||||||
|
|
||||||
const CONFIG_JSON_SCHEMA: &str = include_str!("../../config/schema.config.json");
|
const CONFIG_JSON_SCHEMA: &str = include_str!("../../config/schemas/app.config.schema.json");
|
||||||
|
|
||||||
/// Root configuration containing every named profile.
|
/// Root configuration containing every named profile.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||||
@@ -28,8 +28,6 @@ pub struct ProfileConfig {
|
|||||||
pub name: std::string::String,
|
pub name: std::string::String,
|
||||||
/// Application metadata.
|
/// Application metadata.
|
||||||
pub app: AppSectionConfig,
|
pub app: AppSectionConfig,
|
||||||
/// Logging configuration.
|
|
||||||
pub logging: LoggingConfig,
|
|
||||||
/// Database configuration.
|
/// Database configuration.
|
||||||
pub database: DatabaseConfig,
|
pub database: DatabaseConfig,
|
||||||
/// Local data directories.
|
/// Local data directories.
|
||||||
@@ -59,61 +57,6 @@ pub struct AppSectionConfig {
|
|||||||
pub auto_reconnect_default: bool,
|
pub auto_reconnect_default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logging configuration shared by apps and worker processes.
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
|
||||||
#[ts(
|
|
||||||
export,
|
|
||||||
export_to = "../frontend/ts/bindings/ks_config/settings/LoggingConfig.ts"
|
|
||||||
)]
|
|
||||||
pub struct LoggingConfig {
|
|
||||||
/// Default log level.
|
|
||||||
pub default_level: std::string::String,
|
|
||||||
/// Output targets.
|
|
||||||
pub targets: std::vec::Vec<LogTargetConfig>,
|
|
||||||
/// Target-specific filters.
|
|
||||||
pub target_filters: std::vec::Vec<LogTargetFilterConfig>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Single logging output target.
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
|
||||||
#[ts(
|
|
||||||
export,
|
|
||||||
export_to = "../frontend/ts/bindings/ks_config/settings/LogTargetConfig.ts"
|
|
||||||
)]
|
|
||||||
pub struct LogTargetConfig {
|
|
||||||
/// Output target name.
|
|
||||||
pub name: std::string::String,
|
|
||||||
/// Enables this target.
|
|
||||||
pub enabled: bool,
|
|
||||||
/// Sink kind, such as console or file.
|
|
||||||
pub sink: std::string::String,
|
|
||||||
/// Minimum level for this target.
|
|
||||||
pub level: std::string::String,
|
|
||||||
/// File path for file sinks or an empty string for console sinks.
|
|
||||||
pub path: std::string::String,
|
|
||||||
/// Rotation mode for file sinks.
|
|
||||||
pub rotation: std::string::String,
|
|
||||||
/// Message format, such as human or json.
|
|
||||||
pub format: std::string::String,
|
|
||||||
/// Enables ANSI escape sequences for this target.
|
|
||||||
pub ansi: bool,
|
|
||||||
/// Included tracing targets or globs.
|
|
||||||
pub targets: std::vec::Vec<std::string::String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-target logging filter.
|
|
||||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
|
||||||
#[ts(
|
|
||||||
export,
|
|
||||||
export_to = "../frontend/ts/bindings/ks_config/settings/LogTargetFilterConfig.ts"
|
|
||||||
)]
|
|
||||||
pub struct LogTargetFilterConfig {
|
|
||||||
/// Tracing target or crate prefix.
|
|
||||||
pub target: std::string::String,
|
|
||||||
/// Level assigned to the target.
|
|
||||||
pub level: std::string::String,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Database backend configuration.
|
/// Database backend configuration.
|
||||||
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize, TS)]
|
||||||
#[ts(
|
#[ts(
|
||||||
@@ -650,10 +593,6 @@ fn validate_profile(profile: &ProfileConfig) -> ks_core::Result<()> {
|
|||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
}
|
}
|
||||||
match validate_logging(&profile.logging) {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
match validate_database(&profile.database) {
|
match validate_database(&profile.database) {
|
||||||
std::result::Result::Ok(()) => (),
|
std::result::Result::Ok(()) => (),
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
@@ -685,109 +624,6 @@ fn validate_app_section(config: &AppSectionConfig) -> ks_core::Result<()> {
|
|||||||
return require_non_empty(&config.environment, "app.environment");
|
return require_non_empty(&config.environment, "app.environment");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_logging(config: &LoggingConfig) -> ks_core::Result<()> {
|
|
||||||
match validate_log_level(&config.default_level, "logging.default_level") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
if config.targets.is_empty() {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_targets_empty",
|
|
||||||
"at least one logging target is required",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
let mut enabled_count = 0_u32;
|
|
||||||
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
|
||||||
for target in &config.targets {
|
|
||||||
if target.enabled {
|
|
||||||
enabled_count += 1;
|
|
||||||
}
|
|
||||||
if !names.insert(target.name.clone()) {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_target_duplicate",
|
|
||||||
target.name.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
match validate_log_target(target) {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for filter in &config.target_filters {
|
|
||||||
match require_non_empty(&filter.target, "logging.target_filters.target") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
match validate_log_level(&filter.level, "logging.target_filters.level") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if enabled_count == 0 {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_no_enabled_targets",
|
|
||||||
"at least one logging target must be enabled",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return std::result::Result::Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_log_target(config: &LogTargetConfig) -> ks_core::Result<()> {
|
|
||||||
match require_non_empty(&config.name, "logging.targets.name") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
match validate_log_level(&config.level, "logging.targets.level") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
if config.sink != "console" && config.sink != "file" {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_sink_invalid",
|
|
||||||
config.sink.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.rotation != "none"
|
|
||||||
&& config.rotation != "never"
|
|
||||||
&& config.rotation != "daily"
|
|
||||||
&& config.rotation != "hourly"
|
|
||||||
{
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_rotation_invalid",
|
|
||||||
config.rotation.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.format != "human"
|
|
||||||
&& config.format != "compact"
|
|
||||||
&& config.format != "pretty"
|
|
||||||
&& config.format != "json"
|
|
||||||
{
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_format_invalid",
|
|
||||||
config.format.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.sink == "file" && config.path.trim().is_empty() {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_file_path_empty",
|
|
||||||
config.name.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if config.sink == "console" && !config.path.trim().is_empty() {
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"logging_console_path_not_empty",
|
|
||||||
config.name.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
for target in &config.targets {
|
|
||||||
match require_non_empty(target, "logging.targets.targets") {
|
|
||||||
std::result::Result::Ok(()) => (),
|
|
||||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::result::Result::Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
|
fn validate_database(config: &DatabaseConfig) -> ks_core::Result<()> {
|
||||||
if config.backend != "postgres" && config.backend != "sqlite" {
|
if config.backend != "postgres" && config.backend != "sqlite" {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
@@ -1179,22 +1015,6 @@ fn validate_wallet_execution_pair(
|
|||||||
return std::result::Result::Ok(());
|
return std::result::Result::Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validate_log_level(value: &str, field_name: &str) -> ks_core::Result<()> {
|
|
||||||
if value == "trace"
|
|
||||||
|| value == "debug"
|
|
||||||
|| value == "info"
|
|
||||||
|| value == "warn"
|
|
||||||
|| value == "error"
|
|
||||||
|| value == "off"
|
|
||||||
{
|
|
||||||
return std::result::Result::Ok(());
|
|
||||||
}
|
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
|
||||||
"log_level_invalid",
|
|
||||||
format!("{field_name}: {value}"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||||
if value.trim().is_empty() {
|
if value.trim().is_empty() {
|
||||||
return std::result::Result::Err(ks_core::Error::new(
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
@@ -1225,13 +1045,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EXAMPLE_CONFIG: &str = include_str!("../../config/example.config.json");
|
const DEFAULT_CONFIG: &str = include_str!("../../config/app.config.json");
|
||||||
|
const EXAMPLE_CONFIG: &str = include_str!("../../config/example.app.config.json");
|
||||||
|
|
||||||
fn parse_example_value() -> serde_json::Value {
|
fn parse_default_value() -> serde_json::Value {
|
||||||
let result = serde_json::from_str::<serde_json::Value>(EXAMPLE_CONFIG);
|
let result = serde_json::from_str::<serde_json::Value>(DEFAULT_CONFIG);
|
||||||
match result {
|
match result {
|
||||||
std::result::Result::Ok(value) => return value,
|
std::result::Result::Ok(value) => return value,
|
||||||
std::result::Result::Err(error) => panic!("example config must be valid JSON: {error}"),
|
std::result::Result::Err(error) => panic!("default app config must be valid JSON: {error}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1243,57 +1064,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn tracing_crate_names() -> std::vec::Vec<std::string::String> {
|
|
||||||
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
||||||
let workspace_root = match manifest_dir.parent() {
|
|
||||||
std::option::Option::Some(path) => path,
|
|
||||||
std::option::Option::None => panic!("workspace root must exist"),
|
|
||||||
};
|
|
||||||
let read_result = std::fs::read_dir(workspace_root);
|
|
||||||
let entries = match read_result {
|
|
||||||
std::result::Result::Ok(entries) => entries,
|
|
||||||
std::result::Result::Err(error) => panic!("workspace must be readable: {error}"),
|
|
||||||
};
|
|
||||||
let mut names = std::vec::Vec::<std::string::String>::new();
|
|
||||||
for entry_result in entries {
|
|
||||||
let entry = match entry_result {
|
|
||||||
std::result::Result::Ok(entry) => entry,
|
|
||||||
std::result::Result::Err(error) => {
|
|
||||||
panic!("workspace entry must be readable: {error}")
|
|
||||||
},
|
|
||||||
};
|
|
||||||
let cargo_path = entry.path().join("Cargo.toml");
|
|
||||||
if !cargo_path.is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let cargo_result = std::fs::read_to_string(&cargo_path);
|
|
||||||
let cargo_toml = match cargo_result {
|
|
||||||
std::result::Result::Ok(content) => content,
|
|
||||||
std::result::Result::Err(error) => panic!("Cargo.toml must be readable: {error}"),
|
|
||||||
};
|
|
||||||
if !cargo_toml.lines().any(|line| return line.trim() == "tracing.workspace = true") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let mut in_package = false;
|
|
||||||
for line in cargo_toml.lines() {
|
|
||||||
let trimmed = line.trim();
|
|
||||||
if trimmed == "[package]" {
|
|
||||||
in_package = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if in_package && trimmed.starts_with('[') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if in_package && trimmed.starts_with("name = ") {
|
|
||||||
names.push(trimmed.trim_start_matches("name = ").trim_matches('"').to_string());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
names.sort();
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_text_is_valid_json() {
|
fn schema_text_is_valid_json() {
|
||||||
let result = super::config_json_schema_value();
|
let result = super::config_json_schema_value();
|
||||||
@@ -1301,110 +1071,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn example_config_validates_against_schema() {
|
fn default_and_example_app_configs_validate_against_schema() {
|
||||||
let result = super::validate_config_json_schema(EXAMPLE_CONFIG);
|
assert!(super::validate_config_json_schema(DEFAULT_CONFIG).is_ok());
|
||||||
assert!(result.is_ok());
|
assert!(super::validate_config_json_schema(EXAMPLE_CONFIG).is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn example_config_routes_global_and_operational_crate_files() {
|
fn default_app_config_parses_and_resolves_active_profile() {
|
||||||
let config_result = super::parse_config_json(EXAMPLE_CONFIG);
|
let config_result = super::parse_config_json(DEFAULT_CONFIG);
|
||||||
let config = match config_result {
|
|
||||||
std::result::Result::Ok(value) => value,
|
|
||||||
std::result::Result::Err(error) => panic!("example config must parse: {error}"),
|
|
||||||
};
|
|
||||||
let operational_crates = tracing_crate_names();
|
|
||||||
assert!(!operational_crates.is_empty());
|
|
||||||
for profile in config.profiles {
|
|
||||||
let directory = match profile.name.as_str() {
|
|
||||||
"local_devnet" => "devnet",
|
|
||||||
"mainnet_research" => "mainnet_research",
|
|
||||||
"mainnet" => "mainnet",
|
|
||||||
value => panic!("unexpected example profile: {value}"),
|
|
||||||
};
|
|
||||||
for (suffix, level, format) in [
|
|
||||||
("debug.log", "debug", "human"),
|
|
||||||
("info.log", "info", "human"),
|
|
||||||
("error.jsonl", "error", "json"),
|
|
||||||
] {
|
|
||||||
let expected_path = format!("logs/{directory}/{suffix}");
|
|
||||||
assert!(profile.logging.targets.iter().any(|target| {
|
|
||||||
return target.enabled
|
|
||||||
&& target.path == expected_path
|
|
||||||
&& target.level == level
|
|
||||||
&& target.format == format
|
|
||||||
&& target.targets == std::vec!["*".to_string()];
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
assert!(profile.logging.targets.iter().any(|target| {
|
|
||||||
return target.enabled
|
|
||||||
&& target.path == format!("logs/{directory}/app.log")
|
|
||||||
&& target.level == "debug"
|
|
||||||
&& target.format == "human"
|
|
||||||
&& target.targets == std::vec!["kb-app-demo-desktop".to_string()];
|
|
||||||
}));
|
|
||||||
for crate_name in &operational_crates {
|
|
||||||
for (suffix, level, format) in [
|
|
||||||
("debug.log", "debug", "human"),
|
|
||||||
("info.log", "info", "human"),
|
|
||||||
("error.jsonl", "error", "json"),
|
|
||||||
] {
|
|
||||||
let route_directory = if crate_name == "ks-pipeline-demo-scenarios" {
|
|
||||||
"ks-pipeline"
|
|
||||||
} else {
|
|
||||||
crate_name.as_str()
|
|
||||||
};
|
|
||||||
let expected_path = format!("logs/{directory}/{route_directory}/{suffix}");
|
|
||||||
let route_exists = profile.logging.targets.iter().any(|target| {
|
|
||||||
return target.enabled
|
|
||||||
&& target.path == expected_path
|
|
||||||
&& target.level == level
|
|
||||||
&& target.format == format
|
|
||||||
&& target.targets.iter().any(|value| return value == crate_name);
|
|
||||||
});
|
|
||||||
assert!(
|
|
||||||
route_exists,
|
|
||||||
"profile {} is missing the canonical {} route for {} at {}",
|
|
||||||
profile.name, level, crate_name, expected_path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn example_config_uses_canonical_wallet_tracing_routes_without_embedded_secrets() {
|
|
||||||
let config_result = super::parse_config_json(EXAMPLE_CONFIG);
|
|
||||||
let config = match config_result {
|
|
||||||
std::result::Result::Ok(value) => value,
|
|
||||||
std::result::Result::Err(error) => panic!("example config must parse: {error}"),
|
|
||||||
};
|
|
||||||
assert!(!EXAMPLE_CONFIG.contains("api-key=95e73621"));
|
|
||||||
assert!(!EXAMPLE_CONFIG.contains("\"ks_wallet\""));
|
|
||||||
for profile in config.profiles {
|
|
||||||
assert!(profile.logging.targets.iter().any(|target| {
|
|
||||||
return target.enabled && target.targets == std::vec!["ks-wallet".to_string()];
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn example_config_uses_canonical_pipeline_tracing_routes() {
|
|
||||||
let source = include_str!("../../config/example.config.json");
|
|
||||||
assert!(source.contains("\"ks-pipeline\""));
|
|
||||||
assert!(source.contains("logs/devnet/ks-pipeline/debug.log"));
|
|
||||||
assert!(source.contains("logs/mainnet_research/ks-pipeline/info.log"));
|
|
||||||
assert!(source.contains("logs/mainnet/ks-pipeline/error.jsonl"));
|
|
||||||
assert!(!source.contains("ks_pipeline"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn example_config_parses_and_resolves_active_profile() {
|
|
||||||
let config_result = super::parse_config_json(EXAMPLE_CONFIG);
|
|
||||||
assert!(config_result.is_ok());
|
assert!(config_result.is_ok());
|
||||||
let config = match config_result {
|
let config = match config_result {
|
||||||
std::result::Result::Ok(config) => config,
|
std::result::Result::Ok(config) => config,
|
||||||
std::result::Result::Err(error) => panic!("example config must parse: {error}"),
|
std::result::Result::Err(error) => panic!("default app config must parse: {error}"),
|
||||||
};
|
};
|
||||||
let active_result = super::active_profile(&config);
|
let active_result = super::active_profile(&config);
|
||||||
assert!(active_result.is_ok());
|
assert!(active_result.is_ok());
|
||||||
@@ -1417,18 +1095,18 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn example_config_serializes_and_roundtrips() {
|
fn default_app_config_serializes_and_roundtrips() {
|
||||||
let config_result = super::parse_config_json(EXAMPLE_CONFIG);
|
let config_result = super::parse_config_json(DEFAULT_CONFIG);
|
||||||
assert!(config_result.is_ok());
|
assert!(config_result.is_ok());
|
||||||
let config = match config_result {
|
let config = match config_result {
|
||||||
std::result::Result::Ok(config) => config,
|
std::result::Result::Ok(config) => config,
|
||||||
std::result::Result::Err(error) => panic!("example config must parse: {error}"),
|
std::result::Result::Err(error) => panic!("default app config must parse: {error}"),
|
||||||
};
|
};
|
||||||
let serialized_result = super::serialize_config_json_pretty(&config);
|
let serialized_result = super::serialize_config_json_pretty(&config);
|
||||||
assert!(serialized_result.is_ok());
|
assert!(serialized_result.is_ok());
|
||||||
let serialized = match serialized_result {
|
let serialized = match serialized_result {
|
||||||
std::result::Result::Ok(serialized) => serialized,
|
std::result::Result::Ok(serialized) => serialized,
|
||||||
std::result::Result::Err(error) => panic!("example config must serialize: {error}"),
|
std::result::Result::Err(error) => panic!("default app config must serialize: {error}"),
|
||||||
};
|
};
|
||||||
let reparsed_result = super::parse_config_json(&serialized);
|
let reparsed_result = super::parse_config_json(&serialized);
|
||||||
assert!(reparsed_result.is_ok());
|
assert!(reparsed_result.is_ok());
|
||||||
@@ -1441,7 +1119,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_missing_active_profile() {
|
fn parser_rejects_missing_active_profile() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["active_profile"] = serde_json::Value::String("missing_profile".to_string());
|
value["active_profile"] = serde_json::Value::String("missing_profile".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
let result = super::parse_config_json(&raw_json);
|
let result = super::parse_config_json(&raw_json);
|
||||||
@@ -1449,8 +1127,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn example_config_separates_runtime_postgres_profiles() {
|
fn default_app_config_separates_runtime_postgres_profiles() {
|
||||||
let value = parse_example_value();
|
let value = parse_default_value();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
value["profiles"][0]["database"]["postgres"]["url"],
|
value["profiles"][0]["database"]["postgres"]["url"],
|
||||||
serde_json::Value::String("${KS_SECRET_POSTGRES_DEVNET_URL}".to_string())
|
serde_json::Value::String("${KS_SECRET_POSTGRES_DEVNET_URL}".to_string())
|
||||||
@@ -1467,7 +1145,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_rejects_profile_enabled_field() {
|
fn schema_rejects_profile_enabled_field() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["enabled"] = serde_json::Value::Bool(true);
|
value["profiles"][0]["enabled"] = serde_json::Value::Bool(true);
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
let result = super::validate_config_json_schema(&raw_json);
|
let result = super::validate_config_json_schema(&raw_json);
|
||||||
@@ -1476,7 +1154,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_duplicate_profile_names() {
|
fn parser_rejects_duplicate_profile_names() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][1]["name"] = serde_json::Value::String("local_devnet".to_string());
|
value["profiles"][1]["name"] = serde_json::Value::String("local_devnet".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
let result = super::parse_config_json(&raw_json);
|
let result = super::parse_config_json(&raw_json);
|
||||||
@@ -1485,7 +1163,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_rejects_unknown_endpoint_secret_field() {
|
fn schema_rejects_unknown_endpoint_secret_field() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["solana"]["http_endpoints"][0]["secret_env_field"] =
|
value["profiles"][0]["solana"]["http_endpoints"][0]["secret_env_field"] =
|
||||||
serde_json::Value::String("KS_SECRET_HELIUS_API_KEY".to_string());
|
serde_json::Value::String("KS_SECRET_HELIUS_API_KEY".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1495,7 +1173,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_rejects_runtime_idls_directory_field() {
|
fn schema_rejects_runtime_idls_directory_field() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["data"]["idls_directory"] =
|
value["profiles"][0]["data"]["idls_directory"] =
|
||||||
serde_json::Value::String("idls".to_string());
|
serde_json::Value::String("idls".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1505,7 +1183,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_rejects_unsupported_transport_surface() {
|
fn schema_rejects_unsupported_transport_surface() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["solana"]["advanced_streams"] = serde_json::json!([]);
|
value["profiles"][0]["solana"]["advanced_streams"] = serde_json::json!([]);
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
let result = super::validate_config_json_schema(&raw_json);
|
let result = super::validate_config_json_schema(&raw_json);
|
||||||
@@ -1514,7 +1192,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_invalid_http_endpoint_url() {
|
fn parser_rejects_invalid_http_endpoint_url() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["solana"]["http_endpoints"][0]["url"] =
|
value["profiles"][0]["solana"]["http_endpoints"][0]["url"] =
|
||||||
serde_json::Value::String("wss://api.devnet.solana.com".to_string());
|
serde_json::Value::String("wss://api.devnet.solana.com".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1524,7 +1202,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_invalid_ws_endpoint_url() {
|
fn parser_rejects_invalid_ws_endpoint_url() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["solana"]["ws_endpoints"][0]["url"] =
|
value["profiles"][0]["solana"]["ws_endpoints"][0]["url"] =
|
||||||
serde_json::Value::String("https://api.devnet.solana.com".to_string());
|
serde_json::Value::String("https://api.devnet.solana.com".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1532,19 +1210,9 @@ mod tests {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parser_rejects_file_logging_target_without_path() {
|
|
||||||
let mut value = parse_example_value();
|
|
||||||
value["profiles"][0]["logging"]["targets"][1]["path"] =
|
|
||||||
serde_json::Value::String("".to_string());
|
|
||||||
let raw_json = value_to_json(&value);
|
|
||||||
let result = super::parse_config_json(&raw_json);
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_invalid_temporary_wallet_alias() {
|
fn parser_rejects_invalid_temporary_wallet_alias() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["wallet"]["temporary_wallet_alias"] =
|
value["profiles"][0]["wallet"]["temporary_wallet_alias"] =
|
||||||
serde_json::Value::String("../wallet".to_string());
|
serde_json::Value::String("../wallet".to_string());
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1554,7 +1222,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_persistent_disabled_temporary_wallet() {
|
fn parser_rejects_persistent_disabled_temporary_wallet() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["wallet"]["temporary_wallet_enabled"] = serde_json::Value::Bool(false);
|
value["profiles"][0]["wallet"]["temporary_wallet_enabled"] = serde_json::Value::Bool(false);
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
let result = super::parse_config_json(&raw_json);
|
let result = super::parse_config_json(&raw_json);
|
||||||
@@ -1563,7 +1231,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_enabled_cluster_without_spend_limit() {
|
fn parser_rejects_enabled_cluster_without_spend_limit() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["execution"]["devnet_max_spend_lamports"] =
|
value["profiles"][0]["execution"]["devnet_max_spend_lamports"] =
|
||||||
serde_json::Value::Number(serde_json::Number::from(0));
|
serde_json::Value::Number(serde_json::Number::from(0));
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1579,7 +1247,7 @@ mod tests {
|
|||||||
"confirmation_poll_interval_ms",
|
"confirmation_poll_interval_ms",
|
||||||
"confirmation_max_attempts",
|
"confirmation_max_attempts",
|
||||||
] {
|
] {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["execution"][field] =
|
value["profiles"][0]["execution"][field] =
|
||||||
serde_json::Value::Number(serde_json::Number::from(0));
|
serde_json::Value::Number(serde_json::Number::from(0));
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
@@ -1590,7 +1258,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parser_rejects_http_role_with_subscriptions() {
|
fn parser_rejects_http_role_with_subscriptions() {
|
||||||
let mut value = parse_example_value();
|
let mut value = parse_default_value();
|
||||||
value["profiles"][0]["solana"]["http_endpoints"][0]["roles"][0]["max_subscriptions"] =
|
value["profiles"][0]["solana"]["http_endpoints"][0]["roles"][0]["max_subscriptions"] =
|
||||||
serde_json::Value::Number(serde_json::Number::from(1));
|
serde_json::Value::Number(serde_json::Number::from(1));
|
||||||
let raw_json = value_to_json(&value);
|
let raw_json = value_to_json(&value);
|
||||||
|
|||||||
@@ -1,8 +1,16 @@
|
|||||||
<!-- file: ks-logging/CHANGELOG.md -->
|
<!-- file: ks-logging/CHANGELOG.md -->
|
||||||
<!-- version: 13 -->
|
<!-- version: 14 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-logging
|
# CHANGELOG — ks-logging
|
||||||
|
|
||||||
|
## `0.5.1-pre.005`
|
||||||
|
|
||||||
|
- devient propriétaire unique des types `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` ;
|
||||||
|
- ajoute `LoggingConfigDocument`, les profils logging indépendants et leur chargement avec résolution d’environnement ;
|
||||||
|
- embarque et valide `config/schemas/logging.config.schema.json` ;
|
||||||
|
- charge `config/logging.config.json` par défaut via le desktop et supprime la conversion champ par champ depuis `ks-config` ;
|
||||||
|
- ajoute les tests de schéma, profil actif et routes canoniques du document logging.
|
||||||
|
|
||||||
## `0.5.1-pre.003`
|
## `0.5.1-pre.003`
|
||||||
|
|
||||||
- aligne les routes de référence et la documentation de logging sur les identités `ks-lib-decoder.*`, `ks-lib-executor.*` et `ks-lib-materializer.*` ;
|
- aligne les routes de référence et la documentation de logging sur les identités `ks-lib-decoder.*`, `ks-lib-executor.*` et `ks-lib-materializer.*` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# file: ks-logging/Cargo.toml
|
# file: ks-logging/Cargo.toml
|
||||||
# version: 3
|
# version: 4
|
||||||
|
|
||||||
[package]
|
[package]
|
||||||
name = "ks-logging"
|
name = "ks-logging"
|
||||||
@@ -9,7 +9,11 @@ license.workspace = true
|
|||||||
publish.workspace = true
|
publish.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
jsonschema.workspace = true
|
||||||
|
ks-config = { path = "../ks-config" }
|
||||||
ks-core = { path = "../ks-core" }
|
ks-core = { path = "../ks-core" }
|
||||||
|
serde.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
tracing.workspace = true
|
tracing.workspace = true
|
||||||
tracing-appender.workspace = true
|
tracing-appender.workspace = true
|
||||||
tracing-subscriber.workspace = true
|
tracing-subscriber.workspace = true
|
||||||
|
|||||||
@@ -1,38 +1,45 @@
|
|||||||
<!-- file: ks-logging/README.md -->
|
<!-- file: ks-logging/README.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# ks-logging
|
# ks-logging
|
||||||
|
|
||||||
`ks-logging` initialise le subscriber global `tracing` à partir d’une configuration de routes explicites vers la console ou des fichiers.
|
`ks-logging` possède le contrat logging indépendant du workspace et initialise le subscriber global `tracing` à partir du profil logging sélectionné.
|
||||||
|
|
||||||
## Responsabilités
|
## Responsabilités
|
||||||
|
|
||||||
|
- charger `logging.config.json` et résoudre ses placeholders d’environnement ;
|
||||||
|
- valider `config/schemas/logging.config.schema.json` ;
|
||||||
|
- sélectionner un profil logging indépendamment du profil applicatif ;
|
||||||
|
- posséder `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` ;
|
||||||
- sélectionner les routes activées ;
|
- sélectionner les routes activées ;
|
||||||
- appliquer niveaux, targets exactes et préfixes wildcard ;
|
- appliquer niveaux, targets exactes et préfixes wildcard ;
|
||||||
- créer les writers non bloquants ;
|
- créer les writers non bloquants et gérer les formats/rotations ;
|
||||||
- gérer les formats human, compact, pretty et JSON ;
|
|
||||||
- gérer les rotations de fichiers supportées ;
|
|
||||||
- conserver les `WorkerGuard` pendant toute la durée du processus.
|
- conserver les `WorkerGuard` pendant toute la durée du processus.
|
||||||
|
|
||||||
## Hors périmètre
|
## Relations
|
||||||
|
|
||||||
La crate ne charge pas actuellement le fichier de configuration et ne définit pas les targets des autres crates. Le cadrage `0.5.0-pre.002` confirme qu’un document logging séparé sera introduit en `0.5.1`; la propriété exacte du DTO source sera fixée sans faire dépendre la configuration générale du runtime tracing.
|
`ks-logging` dépend de `ks-config` uniquement pour le chargement du fichier `.env` et la résolution générique `${KS_*}` / `${KB_*}`. `ks-config` ne dépend pas de `ks-logging` et ne duplique aucun type logging.
|
||||||
|
|
||||||
|
Le desktop charge `config/app.config.json` et `config/logging.config.json` séparément puis transmet directement le profil logging actif à `init_logging`.
|
||||||
|
|
||||||
## API publique
|
## API publique
|
||||||
|
|
||||||
|
- `read_logging_json_file_with_environment` charge le document logging ;
|
||||||
|
- `parse_logging_json` et `validate_logging_json_schema` valident le format ;
|
||||||
|
- `active_logging_profile` sélectionne le profil actif ;
|
||||||
|
- `LoggingConfigDocument` et `LoggingProfileConfig` décrivent le document ;
|
||||||
|
- `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` décrivent le runtime ;
|
||||||
- `init_logging` initialise le subscriber global ;
|
- `init_logging` initialise le subscriber global ;
|
||||||
- `LoggingConfig`, `LogTargetConfig` et `LogTargetFilterConfig` décrivent les routes ;
|
- `LoggingGuard` maintient les writers ;
|
||||||
- `LoggingGuard` maintient les writers et expose les routes installées ;
|
- `tracing_target` retourne le target canonique de la crate.
|
||||||
- `tracing_target` retourne le target canonique de la crate ;
|
|
||||||
- `LogFileRoute` reste disponible pour les anciens appelants file-only.
|
|
||||||
|
|
||||||
## Statut
|
## Statut
|
||||||
|
|
||||||
L’initialisation multi-routes est fonctionnelle. Elle échoue explicitement si aucune route n’est activée ou si le subscriber global a déjà été initialisé.
|
Le split config/logging est fonctionnel. Les profils logging sont indépendants des profils applicatifs et les routes existantes restent conservées dans `config/logging.config.json`.
|
||||||
|
|
||||||
## Documents
|
## Documents
|
||||||
|
|
||||||
- [Utilisation](USAGE.md)
|
- [Utilisation](USAGE.md)
|
||||||
- [Travaux restants](TODO.md)
|
- [Travaux restants](TODO.md)
|
||||||
- [Historique](CHANGELOG.md)
|
- [Historique](CHANGELOG.md)
|
||||||
- [Guide de configuration](../ks-config/USAGE.md)
|
- [Configuration locale](../config/README.md)
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
<!-- file: ks-logging/TODO.md -->
|
<!-- file: ks-logging/TODO.md -->
|
||||||
<!-- version: 5 -->
|
<!-- version: 6 -->
|
||||||
|
|
||||||
# TODO — ks-logging
|
# TODO — ks-logging
|
||||||
|
|
||||||
## Série `0.5.x`
|
## Série `0.5.x`
|
||||||
|
|
||||||
- [ ] `0.5.1` - consommer un document logging et un schéma distincts de la configuration généraliste.
|
- [ ] `0.5.1` - intégrer la propagation de sensibilité et le camouflage des valeurs provenant de `KS_SECRET_*` / `KB_SECRET_*` avant tout diagnostic ou log.
|
||||||
- [ ] `0.5.1` - définir avec `ks-config` un propriétaire unique du DTO source logging sans coupler `ks-config` au runtime tracing.
|
- [ ] Compatibilité - préserver targets, niveaux, filtres, formats, routes et sémantique wildcard pendant les prochaines migrations.
|
||||||
- [ ] `0.5.1` - supprimer la conversion champ par champ actuellement portée par le desktop.
|
|
||||||
- [ ] Compatibilité - préserver targets, niveaux, filtres, formats, routes et sémantique wildcard pendant la migration.
|
|
||||||
- [ ] Contrat - réévaluer `LogFileRoute`, actuellement sans consommateur actif hors de la crate.
|
- [ ] Contrat - réévaluer `LogFileRoute`, actuellement sans consommateur actif hors de la crate.
|
||||||
- [ ] Tests - ajouter les tests d’API externe et de migration du futur format logging.
|
- [ ] Tests - ajouter les canaris garantissant qu’un secret résolu n’atteint jamais une sortie logging.
|
||||||
- [ ] Documentation - mettre à jour les exemples après implémentation du nouveau chargement.
|
- [ ] Documentation - maintenir le schéma et les exemples logging alignés avec les types possédés par la crate.
|
||||||
|
|||||||
@@ -1,9 +1,57 @@
|
|||||||
<!-- file: ks-logging/USAGE.md -->
|
<!-- file: ks-logging/USAGE.md -->
|
||||||
<!-- version: 4 -->
|
<!-- version: 5 -->
|
||||||
|
|
||||||
# Utilisation de ks-logging
|
# Utilisation de ks-logging
|
||||||
|
|
||||||
## Initialisation
|
## Chargement recommandé
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let document = match ks_logging::read_logging_json_file_with_environment(
|
||||||
|
std::path::Path::new("config/logging.config.json"),
|
||||||
|
std::path::Path::new("."),
|
||||||
|
) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let config = match ks_logging::active_logging_profile(&document) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
let guard = match ks_logging::init_logging(config) {
|
||||||
|
Ok(value) => value,
|
||||||
|
Err(error) => return Err(error),
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Le desktop utilise `config/logging.config.json` par défaut. `KS_LOGGING_CONFIG_PATH` peut sélectionner un autre document sans modifier `KS_CONFIG_PATH` ni le profil applicatif.
|
||||||
|
|
||||||
|
## Format du document
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"active_profile": "local_devnet",
|
||||||
|
"profiles": [
|
||||||
|
{
|
||||||
|
"name": "local_devnet",
|
||||||
|
"default_level": "info",
|
||||||
|
"targets": [],
|
||||||
|
"target_filters": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Le schéma formel est `config/schemas/logging.config.schema.json`. `config/example.logging.config.json` fournit un exemple minimal conforme avec des routes valides.
|
||||||
|
|
||||||
|
`targets` doit contenir au moins une route et au moins une route doit être activée.
|
||||||
|
|
||||||
|
## Sélection indépendante
|
||||||
|
|
||||||
|
Le `active_profile` logging n’est jamais déduit de `app.config.json`. Un opérateur peut donc utiliser, par exemple, un profil applicatif `mainnet_research` avec un profil logging `local_devnet` ou tout autre profil logging explicitement défini.
|
||||||
|
|
||||||
|
## Construction directe
|
||||||
|
|
||||||
|
Les tests ou consommateurs spécialisés peuvent toujours construire directement `LoggingConfig` puis appeler `init_logging` :
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
let config = ks_logging::LoggingConfig {
|
let config = ks_logging::LoggingConfig {
|
||||||
@@ -17,7 +65,7 @@ let config = ks_logging::LoggingConfig {
|
|||||||
rotation: "none".to_string(),
|
rotation: "none".to_string(),
|
||||||
format: "compact".to_string(),
|
format: "compact".to_string(),
|
||||||
ansi: true,
|
ansi: true,
|
||||||
targets: vec!["kb-*".to_string()],
|
targets: vec!["ks-*".to_string()],
|
||||||
}],
|
}],
|
||||||
target_filters: Vec::new(),
|
target_filters: Vec::new(),
|
||||||
};
|
};
|
||||||
@@ -27,16 +75,6 @@ let guard = match ks_logging::init_logging(&config) {
|
|||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
`init_logging` doit être appelée une seule fois par processus. Le `LoggingGuard` retourné doit rester vivant jusqu’à l’arrêt afin de ne pas interrompre les writers non bloquants.
|
|
||||||
|
|
||||||
## Inspection des routes
|
|
||||||
|
|
||||||
```rust
|
|
||||||
assert_eq!(guard.route_count(), 1);
|
|
||||||
assert_eq!(guard.guard_count(), 1);
|
|
||||||
assert_eq!(guard.route_names(), &["console".to_string()]);
|
|
||||||
```
|
|
||||||
|
|
||||||
## Route fichier JSON
|
## Route fichier JSON
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -49,76 +87,13 @@ let file_route = ks_logging::LogTargetConfig {
|
|||||||
rotation: "daily".to_string(),
|
rotation: "daily".to_string(),
|
||||||
format: "json".to_string(),
|
format: "json".to_string(),
|
||||||
ansi: false,
|
ansi: false,
|
||||||
targets: vec![
|
targets: vec!["ks-pipeline*".to_string()],
|
||||||
"ks-pipeline*".to_string(),
|
|
||||||
"ks-onchain-transport*".to_string(),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
let config = ks_logging::LoggingConfig {
|
|
||||||
default_level: "info".to_string(),
|
|
||||||
targets: vec![file_route],
|
|
||||||
target_filters: Vec::new(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let guard = match ks_logging::init_logging(&config) {
|
|
||||||
Ok(value) => value,
|
|
||||||
Err(error) => return Err(error),
|
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
Pour une route `file`, `path` doit désigner le fichier cible et `rotation` doit être une valeur supportée (`none`, `never`, `daily` ou `hourly`). Les formats supportés sont `human`, `compact`, `pretty` et `json`.
|
Les rotations supportées sont `none`, `never`, `daily` et `hourly`. Les formats supportés sont `human`, `compact`, `pretty` et `json`.
|
||||||
|
|
||||||
## Filtres de targets
|
## Inspection des routes
|
||||||
|
|
||||||
```rust
|
|
||||||
let config = ks_logging::LoggingConfig {
|
|
||||||
default_level: "warn".to_string(),
|
|
||||||
targets: vec![ks_logging::LogTargetConfig {
|
|
||||||
name: "console".to_string(),
|
|
||||||
enabled: true,
|
|
||||||
sink: "console".to_string(),
|
|
||||||
level: "info".to_string(),
|
|
||||||
path: String::new(),
|
|
||||||
rotation: "none".to_string(),
|
|
||||||
format: "compact".to_string(),
|
|
||||||
ansi: true,
|
|
||||||
targets: vec!["kb-*".to_string()],
|
|
||||||
}],
|
|
||||||
target_filters: vec![
|
|
||||||
ks_logging::LogTargetFilterConfig {
|
|
||||||
target: "ks-pipeline".to_string(),
|
|
||||||
level: "debug".to_string(),
|
|
||||||
},
|
|
||||||
ks_logging::LogTargetFilterConfig {
|
|
||||||
target: "ks-store".to_string(),
|
|
||||||
level: "info".to_string(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
Les routes acceptent des targets exactes et des préfixes wildcard. Les `target_filters` ajoutent des niveaux spécifiques aux routes wildcard. Une route d’erreur conserve la sémantique stricte prévue par l’implémentation.
|
|
||||||
|
|
||||||
## Émission d’événements après initialisation
|
|
||||||
|
|
||||||
```rust
|
|
||||||
tracing::info!(
|
|
||||||
target: ks_logging::tracing_target(),
|
|
||||||
action = "startup",
|
|
||||||
"logging runtime initialized"
|
|
||||||
);
|
|
||||||
|
|
||||||
tracing::debug!(
|
|
||||||
target: "ks-pipeline.demo",
|
|
||||||
scenario = "token_2022",
|
|
||||||
"scenario preparation started"
|
|
||||||
);
|
|
||||||
```
|
|
||||||
|
|
||||||
Les targets doivent suivre la nomenclature canonique du workspace pour que les routes et filtres restent prévisibles.
|
|
||||||
|
|
||||||
## Inspection des routes actives
|
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
for route_name in guard.route_names() {
|
for route_name in guard.route_names() {
|
||||||
@@ -132,6 +107,8 @@ assert_eq!(guard.route_count(), guard.route_names().len());
|
|||||||
|
|
||||||
Les erreurs utilisent `ks_core::Error`. Les cas principaux sont :
|
Les erreurs utilisent `ks_core::Error`. Les cas principaux sont :
|
||||||
|
|
||||||
|
- document ou schéma invalide ;
|
||||||
|
- profil actif absent ou dupliqué ;
|
||||||
- aucune route activée ;
|
- aucune route activée ;
|
||||||
- sink, format, rotation ou niveau inconnu ;
|
- sink, format, rotation ou niveau inconnu ;
|
||||||
- chemin fichier invalide ;
|
- chemin fichier invalide ;
|
||||||
@@ -139,12 +116,11 @@ Les erreurs utilisent `ks_core::Error`. Les cas principaux sont :
|
|||||||
|
|
||||||
## Tests instructifs
|
## Tests instructifs
|
||||||
|
|
||||||
- `enabled_routes_keep_all_configured_outputs` vérifie l’installation multi-routes ;
|
- les tests de `document` vérifient les deux fichiers logging versionnés et les profils indépendants ;
|
||||||
- `route_writer_filter_preserves_target_and_level_semantics` vérifie l’admission finale ;
|
- `default_logging_config_routes_global_and_operational_crate_files` vérifie les routes canoniques ;
|
||||||
- `more_than_64_routes_compose_without_filtered_layer_ids` couvre un volume élevé de routes ;
|
- les tests de `tracing_runtime` vérifient l’installation, les filtres et le volume de routes.
|
||||||
- `every_tracing_crate_exposes_canonical_targets` vérifie la cohérence des targets du workspace.
|
|
||||||
|
|
||||||
## Limites
|
## Limites
|
||||||
|
|
||||||
- initialisation globale unique par processus ;
|
- initialisation globale unique par processus ;
|
||||||
- configuration fournie par l’appelant ;
|
- la politique de redaction des valeurs secrètes avant logging est renforcée dans `0.5.1-pre.006`.
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-logging/src/config.rs
|
// file: ks-logging/src/config.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Logging configuration data structures consumed by the tracing runtime.
|
//! Logging configuration data structures consumed by the tracing runtime.
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ pub struct LogFileRoute {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One target-level filter shared by routes that include the wildcard target.
|
/// One target-level filter shared by routes that include the wildcard target.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||||
pub struct LogTargetFilterConfig {
|
pub struct LogTargetFilterConfig {
|
||||||
/// Tracing target or crate prefix.
|
/// Tracing target or crate prefix.
|
||||||
pub target: std::string::String,
|
pub target: std::string::String,
|
||||||
@@ -24,7 +24,7 @@ pub struct LogTargetFilterConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// One logging output route.
|
/// One logging output route.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||||
pub struct LogTargetConfig {
|
pub struct LogTargetConfig {
|
||||||
/// Route name used for diagnostics.
|
/// Route name used for diagnostics.
|
||||||
pub name: std::string::String,
|
pub name: std::string::String,
|
||||||
@@ -46,8 +46,8 @@ pub struct LogTargetConfig {
|
|||||||
pub targets: std::vec::Vec<std::string::String>,
|
pub targets: std::vec::Vec<std::string::String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Logging configuration consumed by `ks-logging` before profile binding exists.
|
/// Logging configuration consumed by `ks-logging` for one named profile.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||||
pub struct LoggingConfig {
|
pub struct LoggingConfig {
|
||||||
/// Default log level used when a route does not provide target directives.
|
/// Default log level used when a route does not provide target directives.
|
||||||
pub default_level: std::string::String,
|
pub default_level: std::string::String,
|
||||||
|
|||||||
519
ks-logging/src/document.rs
Normal file
519
ks-logging/src/document.rs
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
// file: ks-logging/src/document.rs
|
||||||
|
// version: 1
|
||||||
|
|
||||||
|
//! Independent logging configuration document loading and validation.
|
||||||
|
|
||||||
|
const LOGGING_JSON_SCHEMA: &str = include_str!("../../config/schemas/logging.config.schema.json");
|
||||||
|
|
||||||
|
/// Root logging document containing independently selectable named profiles.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||||
|
pub struct LoggingConfigDocument {
|
||||||
|
/// Active logging profile name.
|
||||||
|
pub active_profile: std::string::String,
|
||||||
|
/// Named logging profiles available in this document.
|
||||||
|
pub profiles: std::vec::Vec<LoggingProfileConfig>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Named logging profile wrapping the runtime logging configuration.
|
||||||
|
#[derive(Clone, Debug, serde::Deserialize, Eq, PartialEq, serde::Serialize)]
|
||||||
|
pub struct LoggingProfileConfig {
|
||||||
|
/// Profile code selected independently from the application profile.
|
||||||
|
pub name: std::string::String,
|
||||||
|
/// Runtime logging configuration owned by `ks-logging`.
|
||||||
|
#[serde(flatten)]
|
||||||
|
pub config: crate::LoggingConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the embedded JSON Schema text used for logging configuration validation.
|
||||||
|
pub fn logging_json_schema_text() -> &'static str {
|
||||||
|
return LOGGING_JSON_SCHEMA;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses the embedded logging JSON Schema into a JSON value.
|
||||||
|
pub fn logging_json_schema_value() -> ks_core::Result<serde_json::Value> {
|
||||||
|
let schema_result = serde_json::from_str::<serde_json::Value>(LOGGING_JSON_SCHEMA);
|
||||||
|
return match schema_result {
|
||||||
|
std::result::Result::Ok(schema) => std::result::Result::Ok(schema),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_schema_parse_failed",
|
||||||
|
error.to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates raw logging JSON against the embedded logging schema.
|
||||||
|
pub fn validate_logging_json_schema(raw_json: &str) -> ks_core::Result<()> {
|
||||||
|
let schema = match logging_json_schema_value() {
|
||||||
|
std::result::Result::Ok(schema) => schema,
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
let instance = match serde_json::from_str::<serde_json::Value>(raw_json) {
|
||||||
|
std::result::Result::Ok(instance) => instance,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_json_parse_failed",
|
||||||
|
error.to_string(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let validator = match jsonschema::validator_for(&schema) {
|
||||||
|
std::result::Result::Ok(validator) => validator,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_schema_compile_failed",
|
||||||
|
error.to_string(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let validation_result = validator.validate(&instance);
|
||||||
|
return match validation_result {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(()),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_schema_validation_failed",
|
||||||
|
error.to_string(),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parses and validates one independent logging configuration document.
|
||||||
|
pub fn parse_logging_json(raw_json: &str) -> ks_core::Result<LoggingConfigDocument> {
|
||||||
|
match validate_logging_json_schema(raw_json) {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
let document = match serde_json::from_str::<LoggingConfigDocument>(raw_json) {
|
||||||
|
std::result::Result::Ok(document) => document,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_json_decode_failed",
|
||||||
|
error.to_string(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return match validate_logging_document(&document) {
|
||||||
|
std::result::Result::Ok(()) => std::result::Result::Ok(document),
|
||||||
|
std::result::Result::Err(error) => std::result::Result::Err(error),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads and parses one logging configuration document from a filesystem path.
|
||||||
|
pub fn read_logging_json_file(path: &std::path::Path) -> ks_core::Result<LoggingConfigDocument> {
|
||||||
|
let raw_json = match std::fs::read_to_string(path) {
|
||||||
|
std::result::Result::Ok(content) => content,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_file_read_failed",
|
||||||
|
error.to_string(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
return parse_logging_json(&raw_json);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads workspace environment values, resolves placeholders and parses logging configuration.
|
||||||
|
pub fn read_logging_json_file_with_environment(
|
||||||
|
path: &std::path::Path,
|
||||||
|
workspace_root: &std::path::Path,
|
||||||
|
) -> ks_core::Result<LoggingConfigDocument> {
|
||||||
|
match ks_config::load_workspace_environment(workspace_root) {
|
||||||
|
std::result::Result::Ok(_) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
let raw_json = match std::fs::read_to_string(path) {
|
||||||
|
std::result::Result::Ok(content) => content,
|
||||||
|
std::result::Result::Err(error) => {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_file_read_failed",
|
||||||
|
error.to_string(),
|
||||||
|
));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let resolved = ks_config::resolve_environment_placeholders(&raw_json);
|
||||||
|
return parse_logging_json(&resolved);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the active runtime logging profile selected by the logging document.
|
||||||
|
pub fn active_logging_profile(
|
||||||
|
document: &LoggingConfigDocument,
|
||||||
|
) -> ks_core::Result<&crate::LoggingConfig> {
|
||||||
|
for profile in &document.profiles {
|
||||||
|
if profile.name == document.active_profile {
|
||||||
|
return std::result::Result::Ok(&profile.config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"active_logging_profile_not_found",
|
||||||
|
document.active_profile.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a typed logging configuration document.
|
||||||
|
pub fn validate_logging_document(document: &LoggingConfigDocument) -> ks_core::Result<()> {
|
||||||
|
match require_non_empty(&document.active_profile, "active_profile") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
if document.profiles.is_empty() {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_profiles_empty",
|
||||||
|
"at least one logging profile is required",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||||
|
let mut active_count = 0_u32;
|
||||||
|
for profile in &document.profiles {
|
||||||
|
match require_non_empty(&profile.name, "logging.profile.name") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
if !names.insert(profile.name.clone()) {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_profile_name_duplicate",
|
||||||
|
profile.name.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if profile.name == document.active_profile {
|
||||||
|
active_count += 1;
|
||||||
|
}
|
||||||
|
match validate_logging_config(&profile.config) {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if active_count != 1 {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"active_logging_profile_count_invalid",
|
||||||
|
format!(
|
||||||
|
"active logging profile '{}' must match exactly one profile",
|
||||||
|
document.active_profile
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_logging_config(config: &crate::LoggingConfig) -> ks_core::Result<()> {
|
||||||
|
match validate_log_level(&config.default_level, "logging.default_level") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
if config.targets.is_empty() {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_targets_empty",
|
||||||
|
"at least one logging target is required",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut enabled_count = 0_u32;
|
||||||
|
let mut names = std::collections::BTreeSet::<std::string::String>::new();
|
||||||
|
for target in &config.targets {
|
||||||
|
if target.enabled {
|
||||||
|
enabled_count += 1;
|
||||||
|
}
|
||||||
|
if !names.insert(target.name.clone()) {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_target_duplicate",
|
||||||
|
target.name.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
match validate_log_target(target) {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for filter in &config.target_filters {
|
||||||
|
match require_non_empty(&filter.target, "logging.target_filters.target") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
match validate_log_level(&filter.level, "logging.target_filters.level") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if enabled_count == 0 {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_no_enabled_targets",
|
||||||
|
"at least one logging target must be enabled",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_log_target(config: &crate::LogTargetConfig) -> ks_core::Result<()> {
|
||||||
|
match require_non_empty(&config.name, "logging.targets.name") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
match validate_log_level(&config.level, "logging.targets.level") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
if config.sink != "console" && config.sink != "file" {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_sink_invalid",
|
||||||
|
config.sink.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if config.rotation != "none"
|
||||||
|
&& config.rotation != "never"
|
||||||
|
&& config.rotation != "daily"
|
||||||
|
&& config.rotation != "hourly"
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_rotation_invalid",
|
||||||
|
config.rotation.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if config.format != "human"
|
||||||
|
&& config.format != "compact"
|
||||||
|
&& config.format != "pretty"
|
||||||
|
&& config.format != "json"
|
||||||
|
{
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_format_invalid",
|
||||||
|
config.format.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if config.sink == "file" && config.path.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_file_path_empty",
|
||||||
|
config.name.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if config.sink == "console" && !config.path.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_console_path_not_empty",
|
||||||
|
config.name.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for target in &config.targets {
|
||||||
|
match require_non_empty(target, "logging.targets.targets") {
|
||||||
|
std::result::Result::Ok(()) => (),
|
||||||
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_log_level(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||||
|
if value == "trace"
|
||||||
|
|| value == "debug"
|
||||||
|
|| value == "info"
|
||||||
|
|| value == "warn"
|
||||||
|
|| value == "error"
|
||||||
|
|| value == "off"
|
||||||
|
{
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"log_level_invalid",
|
||||||
|
format!("{field_name}: {value}"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn require_non_empty(value: &str, field_name: &str) -> ks_core::Result<()> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
return std::result::Result::Err(ks_core::Error::new(
|
||||||
|
"logging_config_field_empty",
|
||||||
|
field_name.to_string(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
return std::result::Result::Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
const DEFAULT_LOGGING_CONFIG: &str = include_str!("../../config/logging.config.json");
|
||||||
|
const EXAMPLE_LOGGING_CONFIG: &str = include_str!("../../config/example.logging.config.json");
|
||||||
|
|
||||||
|
fn parse_default_value() -> serde_json::Value {
|
||||||
|
let result = serde_json::from_str::<serde_json::Value>(DEFAULT_LOGGING_CONFIG);
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("default logging config must be JSON: {error}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn value_to_json(value: &serde_json::Value) -> std::string::String {
|
||||||
|
let result = serde_json::to_string(value);
|
||||||
|
return match result {
|
||||||
|
std::result::Result::Ok(raw_json) => raw_json,
|
||||||
|
std::result::Result::Err(error) => panic!("test logging value must serialize: {error}"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tracing_crate_names() -> std::vec::Vec<std::string::String> {
|
||||||
|
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||||
|
let workspace_root = match manifest_dir.parent() {
|
||||||
|
std::option::Option::Some(path) => path,
|
||||||
|
std::option::Option::None => panic!("workspace root must exist"),
|
||||||
|
};
|
||||||
|
let read_result = std::fs::read_dir(workspace_root);
|
||||||
|
let entries = match read_result {
|
||||||
|
std::result::Result::Ok(entries) => entries,
|
||||||
|
std::result::Result::Err(error) => panic!("workspace must be readable: {error}"),
|
||||||
|
};
|
||||||
|
let mut names = std::vec::Vec::<std::string::String>::new();
|
||||||
|
for entry_result in entries {
|
||||||
|
let entry = match entry_result {
|
||||||
|
std::result::Result::Ok(entry) => entry,
|
||||||
|
std::result::Result::Err(error) => panic!("workspace entry must be readable: {error}"),
|
||||||
|
};
|
||||||
|
let cargo_path = entry.path().join("Cargo.toml");
|
||||||
|
if !cargo_path.is_file() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let cargo_result = std::fs::read_to_string(&cargo_path);
|
||||||
|
let cargo_toml = match cargo_result {
|
||||||
|
std::result::Result::Ok(content) => content,
|
||||||
|
std::result::Result::Err(error) => panic!("Cargo.toml must be readable: {error}"),
|
||||||
|
};
|
||||||
|
if !cargo_toml.lines().any(|line| return line.trim() == "tracing.workspace = true") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut in_package = false;
|
||||||
|
for line in cargo_toml.lines() {
|
||||||
|
let trimmed = line.trim();
|
||||||
|
if trimmed == "[package]" {
|
||||||
|
in_package = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if in_package && trimmed.starts_with('[') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if in_package && trimmed.starts_with("name = ") {
|
||||||
|
names.push(trimmed.trim_start_matches("name = ").trim_matches('"').to_string());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
names.sort();
|
||||||
|
return names;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_schema_text_is_valid_json() {
|
||||||
|
let result = super::logging_json_schema_value();
|
||||||
|
assert!(result.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_and_example_logging_configs_validate_against_schema() {
|
||||||
|
assert!(super::validate_logging_json_schema(DEFAULT_LOGGING_CONFIG).is_ok());
|
||||||
|
assert!(super::validate_logging_json_schema(EXAMPLE_LOGGING_CONFIG).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_profiles_are_independent_and_active_profile_resolves() {
|
||||||
|
let document_result = super::parse_logging_json(DEFAULT_LOGGING_CONFIG);
|
||||||
|
let document = match document_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("default logging config must parse: {error}"),
|
||||||
|
};
|
||||||
|
assert_eq!(document.active_profile, "mainnet_research");
|
||||||
|
assert_eq!(document.profiles.len(), 3);
|
||||||
|
assert!(super::active_logging_profile(&document).is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_logging_config_routes_global_and_operational_crate_files() {
|
||||||
|
let document_result = super::parse_logging_json(DEFAULT_LOGGING_CONFIG);
|
||||||
|
let document = match document_result {
|
||||||
|
std::result::Result::Ok(value) => value,
|
||||||
|
std::result::Result::Err(error) => panic!("default logging config must parse: {error}"),
|
||||||
|
};
|
||||||
|
let operational_crates = tracing_crate_names();
|
||||||
|
assert!(!operational_crates.is_empty());
|
||||||
|
for profile in document.profiles {
|
||||||
|
let directory = match profile.name.as_str() {
|
||||||
|
"local_devnet" => "devnet",
|
||||||
|
"mainnet_research" => "mainnet_research",
|
||||||
|
"mainnet" => "mainnet",
|
||||||
|
value => panic!("unexpected logging profile: {value}"),
|
||||||
|
};
|
||||||
|
for (suffix, level, format) in [
|
||||||
|
("debug.log", "debug", "human"),
|
||||||
|
("info.log", "info", "human"),
|
||||||
|
("error.jsonl", "error", "json"),
|
||||||
|
] {
|
||||||
|
let expected_path = format!("logs/{directory}/{suffix}");
|
||||||
|
assert!(profile.config.targets.iter().any(|target| {
|
||||||
|
return target.enabled
|
||||||
|
&& target.path == expected_path
|
||||||
|
&& target.level == level
|
||||||
|
&& target.format == format
|
||||||
|
&& target.targets == std::vec!["*".to_string()];
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
assert!(profile.config.targets.iter().any(|target| {
|
||||||
|
return target.enabled
|
||||||
|
&& target.path == format!("logs/{directory}/app.log")
|
||||||
|
&& target.level == "debug"
|
||||||
|
&& target.format == "human"
|
||||||
|
&& target.targets == std::vec!["kb-app-demo-desktop".to_string()];
|
||||||
|
}));
|
||||||
|
for crate_name in &operational_crates {
|
||||||
|
for (suffix, level, format) in [
|
||||||
|
("debug.log", "debug", "human"),
|
||||||
|
("info.log", "info", "human"),
|
||||||
|
("error.jsonl", "error", "json"),
|
||||||
|
] {
|
||||||
|
let route_directory = if crate_name == "ks-pipeline-demo-scenarios" {
|
||||||
|
"ks-pipeline"
|
||||||
|
} else {
|
||||||
|
crate_name.as_str()
|
||||||
|
};
|
||||||
|
let expected_path = format!("logs/{directory}/{route_directory}/{suffix}");
|
||||||
|
let route_exists = profile.config.targets.iter().any(|target| {
|
||||||
|
return target.enabled
|
||||||
|
&& target.path == expected_path
|
||||||
|
&& target.level == level
|
||||||
|
&& target.format == format
|
||||||
|
&& target.targets.iter().any(|value| return value == crate_name);
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
route_exists,
|
||||||
|
"logging profile {} is missing the canonical {} route for {} at {}",
|
||||||
|
profile.name, level, crate_name, expected_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn default_logging_config_uses_canonical_wallet_and_pipeline_routes() {
|
||||||
|
assert!(!DEFAULT_LOGGING_CONFIG.contains("api-key=95e73621"));
|
||||||
|
assert!(!DEFAULT_LOGGING_CONFIG.contains("\"ks_wallet\""));
|
||||||
|
assert!(DEFAULT_LOGGING_CONFIG.contains("\"ks-wallet\""));
|
||||||
|
assert!(DEFAULT_LOGGING_CONFIG.contains("\"ks-pipeline\""));
|
||||||
|
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/devnet/ks-pipeline/debug.log"));
|
||||||
|
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/mainnet_research/ks-pipeline/info.log"));
|
||||||
|
assert!(DEFAULT_LOGGING_CONFIG.contains("logs/mainnet/ks-pipeline/error.jsonl"));
|
||||||
|
assert!(!DEFAULT_LOGGING_CONFIG.contains("ks_pipeline"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_parser_rejects_missing_active_profile() {
|
||||||
|
let mut value = parse_default_value();
|
||||||
|
value["active_profile"] = serde_json::Value::String("missing_profile".to_string());
|
||||||
|
let result = super::parse_logging_json(&value_to_json(&value));
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_parser_rejects_duplicate_profile_names() {
|
||||||
|
let mut value = parse_default_value();
|
||||||
|
value["profiles"][1]["name"] = serde_json::Value::String("local_devnet".to_string());
|
||||||
|
let result = super::parse_logging_json(&value_to_json(&value));
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logging_parser_rejects_file_target_without_path() {
|
||||||
|
let mut value = parse_default_value();
|
||||||
|
value["profiles"][0]["targets"][1]["path"] = serde_json::Value::String("".to_string());
|
||||||
|
let result = super::parse_logging_json(&value_to_json(&value));
|
||||||
|
assert!(result.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-logging/src/lib.rs
|
// file: ks-logging/src/lib.rs
|
||||||
// version: 7
|
// version: 8
|
||||||
|
|
||||||
//! Logging and tracing initialization for applications and workers.
|
//! Logging and tracing initialization for applications and workers.
|
||||||
#![warn(missing_docs)]
|
#![warn(missing_docs)]
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
mod config;
|
mod config;
|
||||||
mod constants;
|
mod constants;
|
||||||
|
mod document;
|
||||||
mod tracing_runtime;
|
mod tracing_runtime;
|
||||||
|
|
||||||
/// Canonical tracing target for this crate.
|
/// Canonical tracing target for this crate.
|
||||||
@@ -21,6 +22,26 @@ pub use self::config::LogTargetConfig;
|
|||||||
pub use self::config::LogTargetFilterConfig;
|
pub use self::config::LogTargetFilterConfig;
|
||||||
/// Exposes the logging configuration consumed by this crate.
|
/// Exposes the logging configuration consumed by this crate.
|
||||||
pub use self::config::LoggingConfig;
|
pub use self::config::LoggingConfig;
|
||||||
|
/// Exposes the root independent logging configuration document.
|
||||||
|
pub use self::document::LoggingConfigDocument;
|
||||||
|
/// Exposes one named logging profile.
|
||||||
|
pub use self::document::LoggingProfileConfig;
|
||||||
|
/// Exposes active logging profile resolution.
|
||||||
|
pub use self::document::active_logging_profile;
|
||||||
|
/// Exposes the embedded logging JSON Schema text.
|
||||||
|
pub use self::document::logging_json_schema_text;
|
||||||
|
/// Exposes the embedded logging JSON Schema parser.
|
||||||
|
pub use self::document::logging_json_schema_value;
|
||||||
|
/// Exposes logging configuration parsing from JSON.
|
||||||
|
pub use self::document::parse_logging_json;
|
||||||
|
/// Exposes logging configuration loading from a filesystem path.
|
||||||
|
pub use self::document::read_logging_json_file;
|
||||||
|
/// Exposes logging loading with workspace environment resolution.
|
||||||
|
pub use self::document::read_logging_json_file_with_environment;
|
||||||
|
/// Exposes typed logging document validation.
|
||||||
|
pub use self::document::validate_logging_document;
|
||||||
|
/// Exposes logging JSON Schema validation.
|
||||||
|
pub use self::document::validate_logging_json_schema;
|
||||||
/// Exposes the guard that keeps non-blocking logging workers alive.
|
/// Exposes the guard that keeps non-blocking logging workers alive.
|
||||||
pub use self::tracing_runtime::LoggingGuard;
|
pub use self::tracing_runtime::LoggingGuard;
|
||||||
/// Exposes initialization from a raw logging configuration section.
|
/// Exposes initialization from a raw logging configuration section.
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<!-- file: ks-pipeline-demo-scenarios/CHANGELOG.md -->
|
<!-- file: ks-pipeline-demo-scenarios/CHANGELOG.md -->
|
||||||
<!-- version: 75 -->
|
<!-- version: 76 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-pipeline-demo-scenarios
|
# CHANGELOG — ks-pipeline-demo-scenarios
|
||||||
|
|
||||||
|
## `0.5.1-pre.005`
|
||||||
|
|
||||||
|
- aligne les scénarios et fixtures sur `config/app.config.json` comme document général par défaut ;
|
||||||
|
- conserve `KS_DEVNET_CONFIG_PATH` comme override du document applicatif des scénarios, sans dépendre du document logging.
|
||||||
|
|
||||||
## `0.5.1-pre.004`
|
## `0.5.1-pre.004`
|
||||||
|
|
||||||
- migre les paramètres d'environnement des scénarios réutilisables vers `KS_*` et les DSN de test vers `KS_SECRET_*` ;
|
- migre les paramètres d'environnement des scénarios réutilisables vers `KS_*` et les DSN de test vers `KS_SECRET_*` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: ks-pipeline-demo-scenarios/USAGE.md -->
|
<!-- file: ks-pipeline-demo-scenarios/USAGE.md -->
|
||||||
<!-- version: 28 -->
|
<!-- version: 29 -->
|
||||||
|
|
||||||
# Utilisation de ks-pipeline-demo-scenarios
|
# Utilisation de ks-pipeline-demo-scenarios
|
||||||
|
|
||||||
@@ -113,7 +113,7 @@ KS_SECRET_POSTGRES_TEST_URL='postgresql://…' \
|
|||||||
cargo test -p ks-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
|
cargo test -p ks-pipeline-demo-scenarios optional_devnet_token_2022_metadata_campaign_from_env -- --nocapture
|
||||||
```
|
```
|
||||||
|
|
||||||
`KS_DEVNET_PROFILE` peut sélectionner explicitement le profil Devnet et `KS_DEVNET_CONFIG_PATH` peut remplacer `config/example.config.json`. Le profil doit utiliser un wallet persistant, autoriser les soumissions Devnet et respecter les plafonds de dépense et de frais.
|
`KS_DEVNET_PROFILE` peut sélectionner explicitement le profil Devnet et `KS_DEVNET_CONFIG_PATH` peut remplacer `config/app.config.json`. Le profil doit utiliser un wallet persistant, autoriser les soumissions Devnet et respecter les plafonds de dépense et de frais.
|
||||||
|
|
||||||
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve l’opération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. La campagne de référence de `pre.012` a été exécutée avec succès et ses preuves sont conservées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `docs/validation/V0_4_8_METADATA_VALIDATION_REPORT.md` ; le rapport détaillé `pre.012` est archivé sous `olddocs/archivekbot3/docs/validation/`. Une nouvelle exécution produit de nouvelles transactions et ne remplace ces preuves qu’après validation explicite.
|
La sortie `TOKEN_2022_METADATA_FIXTURE` conserve le mint et la signature de préparation. Chaque ligne `TOKEN_2022_METADATA_STEP` conserve l’opération, la signature, le slot, la postcondition, le nombre de matérialisations et, pour `Emit`, la taille du `returnData`. La campagne de référence de `pre.012` a été exécutée avec succès et ses preuves sont conservées dans `SPL_TOKEN_2022_METADATA_DEVNET_VALIDATION_MATRIX.json` et dans le rapport `docs/validation/V0_4_8_METADATA_VALIDATION_REPORT.md` ; le rapport détaillé `pre.012` est archivé sous `olddocs/archivekbot3/docs/validation/`. Une nouvelle exécution produit de nouvelles transactions et ne remplace ces preuves qu’après validation explicite.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/environment.rs
|
// file: ks-pipeline-demo-scenarios/src/environment.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Environment initialization for opt-in demonstration scenarios.
|
//! Environment initialization for opt-in demonstration scenarios.
|
||||||
|
|
||||||
@@ -151,7 +151,7 @@ pub async fn prepare_demo_devnet_profile_store(
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
fn example_config() -> ks_config::AppConfig {
|
fn example_config() -> ks_config::AppConfig {
|
||||||
return match ks_config::parse_config_json(include_str!("../../config/example.config.json"))
|
return match ks_config::parse_config_json(include_str!("../../config/app.config.json"))
|
||||||
{
|
{
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/collection_verify_campaign.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
|
//! Confirmed Devnet collection parent/member `Verify -> Unverify` campaign.
|
||||||
|
|
||||||
@@ -648,7 +648,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/create_mint_campaign.rs
|
||||||
// version: 11
|
// version: 12
|
||||||
|
|
||||||
//! Confirmed Devnet `Create -> Mint` campaign for one Metaplex asset family.
|
//! Confirmed Devnet `Create -> Mint` campaign for one Metaplex asset family.
|
||||||
|
|
||||||
@@ -813,7 +813,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/devnet_execution.rs
|
||||||
// version: 19
|
// version: 20
|
||||||
|
|
||||||
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
|
//! Real Devnet simulation and submission for current Metaplex Token Metadata operations.
|
||||||
|
|
||||||
@@ -1267,7 +1267,7 @@ fn validate_available_signers(
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config = match ks_config::parse_config_json(include_str!(
|
let config = match ks_config::parse_config_json(include_str!(
|
||||||
"../../../../config/example.config.json"
|
"../../../../config/app.config.json"
|
||||||
)) {
|
)) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/escrow_campaign.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Confirmed Devnet Token Owned Escrow campaign for current Metaplex escrow operations.
|
//! Confirmed Devnet Token Owned Escrow campaign for current Metaplex escrow operations.
|
||||||
|
|
||||||
@@ -973,7 +973,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/maintenance_campaign.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Final bounded Devnet qualification campaign for current Metaplex maintenance operations.
|
//! Final bounded Devnet qualification campaign for current Metaplex maintenance operations.
|
||||||
|
|
||||||
@@ -577,7 +577,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/pnft_lifecycle_campaign.rs
|
||||||
// version: 5
|
// version: 6
|
||||||
|
|
||||||
//! Confirmed Devnet pNFT delegate, lock, unlock, revoke and transfer campaign.
|
//! Confirmed Devnet pNFT delegate, lock, unlock, revoke and transfer campaign.
|
||||||
|
|
||||||
@@ -1071,7 +1071,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/print_burn_campaign.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Devnet printable master NFT `Print -> Burn` campaign.
|
//! Devnet printable master NFT `Print -> Burn` campaign.
|
||||||
|
|
||||||
@@ -1180,7 +1180,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/metadata/metaplex_token_metadata/use_campaign.rs
|
||||||
// version: 3
|
// version: 4
|
||||||
|
|
||||||
//! Bounded Devnet availability probe for the current Metaplex `Use` surface.
|
//! Bounded Devnet availability probe for the current Metaplex `Use` surface.
|
||||||
|
|
||||||
@@ -425,7 +425,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/solana.rs
|
// file: ks-pipeline-demo-scenarios/src/solana.rs
|
||||||
// version: 13
|
// version: 14
|
||||||
|
|
||||||
//! Devnet Solana execution orchestration with canonical post-validation.
|
//! Devnet Solana execution orchestration with canonical post-validation.
|
||||||
|
|
||||||
@@ -1225,7 +1225,7 @@ pub(crate) fn emit<O>(
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
match ks_config::parse_config_json(include_str!("../../config/app.config.json")) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs
|
// file: ks-pipeline-demo-scenarios/src/spl/associated_token_account.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
//! Devnet ATA execution with stateful and canonical post-validation.
|
//! Devnet ATA execution with stateful and canonical post-validation.
|
||||||
|
|
||||||
@@ -994,7 +994,7 @@ mod tests {
|
|||||||
|
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../../config/example.config.json"))
|
match ks_config::parse_config_json(include_str!("../../../config/app.config.json"))
|
||||||
{
|
{
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/spl/memo.rs
|
// file: ks-pipeline-demo-scenarios/src/spl/memo.rs
|
||||||
// version: 8
|
// version: 9
|
||||||
|
|
||||||
//! Devnet SPL Memo v4 execution with canonical post-validation.
|
//! Devnet SPL Memo v4 execution with canonical post-validation.
|
||||||
|
|
||||||
@@ -755,7 +755,7 @@ fn decode_completed(summary: &ks_pipeline::DecodeReplaySummary) -> bool {
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../../config/example.config.json"))
|
match ks_config::parse_config_json(include_str!("../../../config/app.config.json"))
|
||||||
{
|
{
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs
|
// file: ks-pipeline-demo-scenarios/src/spl/token/execution.rs
|
||||||
// version: 12
|
// version: 13
|
||||||
|
|
||||||
//! Devnet classic SPL Token execution with stateful and canonical post-validation.
|
//! Devnet classic SPL Token execution with stateful and canonical post-validation.
|
||||||
|
|
||||||
@@ -975,7 +975,7 @@ pub(crate) fn decode_completed(summary: &ks_pipeline::DecodeReplaySummary) -> bo
|
|||||||
mod tests {
|
mod tests {
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config = match ks_config::parse_config_json(include_str!(
|
let config = match ks_config::parse_config_json(include_str!(
|
||||||
"../../../../config/example.config.json"
|
"../../../../config/app.config.json"
|
||||||
)) {
|
)) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs
|
// file: ks-pipeline-demo-scenarios/src/spl/token/lifecycle.rs
|
||||||
// version: 11
|
// version: 12
|
||||||
|
|
||||||
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
|
//! Controlled Devnet lifecycle for freshly prepared classic SPL Token accounts.
|
||||||
|
|
||||||
@@ -1688,7 +1688,7 @@ mod tests {
|
|||||||
|
|
||||||
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
fn example_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config = match ks_config::parse_config_json(include_str!(
|
let config = match ks_config::parse_config_json(include_str!(
|
||||||
"../../../../config/example.config.json"
|
"../../../../config/app.config.json"
|
||||||
)) {
|
)) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs
|
// file: ks-pipeline-demo-scenarios/src/spl/token_2022/metadata/campaign.rs
|
||||||
// version: 6
|
// version: 7
|
||||||
|
|
||||||
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.
|
//! Ordered confirmed Devnet campaign for the five Token-2022 Token Metadata instructions.
|
||||||
|
|
||||||
@@ -502,7 +502,7 @@ mod tests {
|
|||||||
let path = std::path::PathBuf::from(value);
|
let path = std::path::PathBuf::from(value);
|
||||||
if path.is_absolute() { path } else { workspace_root.join(path) }
|
if path.is_absolute() { path } else { workspace_root.join(path) }
|
||||||
},
|
},
|
||||||
std::result::Result::Err(_) => workspace_root.join("config/example.config.json"),
|
std::result::Result::Err(_) => workspace_root.join("config/app.config.json"),
|
||||||
};
|
};
|
||||||
let config = ks_config::read_config_json_file_with_environment(
|
let config = ks_config::read_config_json_file_with_environment(
|
||||||
config_path.as_path(),
|
config_path.as_path(),
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
<!-- file: ks-pipeline/CHANGELOG.md -->
|
<!-- file: ks-pipeline/CHANGELOG.md -->
|
||||||
<!-- version: 33 -->
|
<!-- version: 34 -->
|
||||||
|
|
||||||
# CHANGELOG — ks-pipeline
|
# CHANGELOG — ks-pipeline
|
||||||
|
|
||||||
|
## `0.5.1-pre.005`
|
||||||
|
|
||||||
|
- aligne les tests de configuration sur le nouveau document général `config/app.config.json` après extraction du logging ;
|
||||||
|
- ne modifie aucune orchestration ou qualification réseau.
|
||||||
|
|
||||||
## `0.5.1-pre.004`
|
## `0.5.1-pre.004`
|
||||||
|
|
||||||
- migre les variables des tests Devnet SPL Token vers le namespace interne `KS_*` ;
|
- migre les variables des tests Devnet SPL Token vers le namespace interne `KS_*` ;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
// file: ks-pipeline/src/spl_token_stateful.rs
|
// file: ks-pipeline/src/spl_token_stateful.rs
|
||||||
// version: 4
|
// version: 5
|
||||||
|
|
||||||
//! Stateful Localnet and Devnet readiness checks for classic SPL Token operations.
|
//! Stateful Localnet and Devnet readiness checks for classic SPL Token operations.
|
||||||
|
|
||||||
@@ -1440,7 +1440,7 @@ mod tests {
|
|||||||
|
|
||||||
fn local_devnet_profile() -> ks_config::ProfileConfig {
|
fn local_devnet_profile() -> ks_config::ProfileConfig {
|
||||||
let config =
|
let config =
|
||||||
match ks_config::parse_config_json(include_str!("../../config/example.config.json")) {
|
match ks_config::parse_config_json(include_str!("../../config/app.config.json")) {
|
||||||
std::result::Result::Ok(value) => value,
|
std::result::Result::Ok(value) => value,
|
||||||
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
std::result::Result::Err(error) => panic!("example config parse failed: {error}"),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!-- file: prompts/030_v0_5_1_khadhroony_solana_namespace_and_config.md -->
|
<!-- file: prompts/030_v0_5_1_khadhroony_solana_namespace_and_config.md -->
|
||||||
<!-- version: 3 -->
|
<!-- version: 4 -->
|
||||||
|
|
||||||
# Khadhroony Bot3 — `0.5.1` — migration Khadhroony Solana et configuration sûre
|
# Khadhroony Bot3 — `0.5.1` — migration Khadhroony Solana et configuration sûre
|
||||||
|
|
||||||
@@ -61,7 +61,7 @@ Le workspace, le dépôt et le répertoire racine restent nommés `khadhroony-bo
|
|||||||
- `VERSION_DEVELOPMENT_LIFECYCLE.md` ;
|
- `VERSION_DEVELOPMENT_LIFECYCLE.md` ;
|
||||||
4. `docs/architecture/PROJECT_OBJECTIVES.md`, `CRATE_MAP.md`, `ARCHITECTURE.md`, `PIPELINE_ARCHITECTURE.md`, `STORAGE_ARCHITECTURE.md` et `SURFACE_CRATE_MATRIX.md` ;
|
4. `docs/architecture/PROJECT_OBJECTIVES.md`, `CRATE_MAP.md`, `ARCHITECTURE.md`, `PIPELINE_ARCHITECTURE.md`, `STORAGE_ARCHITECTURE.md` et `SURFACE_CRATE_MATRIX.md` ;
|
||||||
5. `README.md`, `USAGE.md`, `TODO.md` et `CHANGELOG.md` des dix crates à renommer et de `kb-app-demo-desktop` ;
|
5. `README.md`, `USAGE.md`, `TODO.md` et `CHANGELOG.md` des dix crates à renommer et de `kb-app-demo-desktop` ;
|
||||||
6. `config/example.config.json`, `config/schema.config.json`, `.env.example` et tous les exemples/configurations de test ;
|
6. `config/app.config.json`, `config/logging.config.json`, leurs schémas sous `config/schemas/`, `.env.example` et tous les exemples/configurations de test ;
|
||||||
7. les tests d'API externe, tests TS-RS, scripts d'audit et références de noms de crates/targets/identités ;
|
7. les tests d'API externe, tests TS-RS, scripts d'audit et références de noms de crates/targets/identités ;
|
||||||
8. les documents archivés `0.5.0` uniquement comme historique, jamais comme source prioritaire face à la politique active et au code courant.
|
8. les documents archivés `0.5.0` uniquement comme historique, jamais comme source prioritaire face à la politique active et au code courant.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
# file: scripts/audit_khadhroony_workspace_rules.py
|
# file: scripts/audit_khadhroony_workspace_rules.py
|
||||||
# version: 20
|
# version: 21
|
||||||
|
|
||||||
"""Audit mechanically verifiable rules specific to khadhroony-bot3."""
|
"""Audit mechanically verifiable rules specific to khadhroony-bot3."""
|
||||||
|
|
||||||
@@ -562,7 +562,7 @@ def audit_materializer_conventions(root: pathlib.Path) -> list[Violation]:
|
|||||||
f"materializer tracing target `{match.group(1)}` must equal one component name",
|
f"materializer tracing target `{match.group(1)}` must equal one component name",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
config_path = root / "config/example.config.json"
|
config_path = root / "config/logging.config.json"
|
||||||
if config_path.exists():
|
if config_path.exists():
|
||||||
try:
|
try:
|
||||||
config = json.loads(config_path.read_text(encoding="utf-8"))
|
config = json.loads(config_path.read_text(encoding="utf-8"))
|
||||||
@@ -583,7 +583,7 @@ def audit_materializer_conventions(root: pathlib.Path) -> list[Violation]:
|
|||||||
violations.append(
|
violations.append(
|
||||||
Violation(
|
Violation(
|
||||||
"KH_MT020",
|
"KH_MT020",
|
||||||
"config/example.config.json",
|
"config/logging.config.json",
|
||||||
1,
|
1,
|
||||||
f"logging target `{target}` is not a declared materializer component",
|
f"logging target `{target}` is not a declared materializer component",
|
||||||
)
|
)
|
||||||
@@ -906,10 +906,17 @@ def audit_environment_namespaces(root: pathlib.Path) -> list[Violation]:
|
|||||||
f"workspace environment example `{match.group(1)}` must use a `KS_*` or `KB_*` namespace",
|
f"workspace environment example `{match.group(1)}` must use a `KS_*` or `KB_*` namespace",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
config = root / "config/example.config.json"
|
placeholder = re.compile(r"\$\{([A-Z][A-Z0-9_]*)")
|
||||||
if config.is_file():
|
for relative in [
|
||||||
|
"config/app.config.json",
|
||||||
|
"config/logging.config.json",
|
||||||
|
"config/example.app.config.json",
|
||||||
|
"config/example.logging.config.json",
|
||||||
|
]:
|
||||||
|
config = root / relative
|
||||||
|
if not config.is_file():
|
||||||
|
continue
|
||||||
text = config.read_text(encoding="utf-8")
|
text = config.read_text(encoding="utf-8")
|
||||||
placeholder = re.compile(r"\$\{([A-Z][A-Z0-9_]*)")
|
|
||||||
for match in placeholder.finditer(text):
|
for match in placeholder.finditer(text):
|
||||||
name = match.group(1)
|
name = match.group(1)
|
||||||
if name.startswith(("KS_", "KB_")):
|
if name.startswith(("KS_", "KB_")):
|
||||||
@@ -917,7 +924,7 @@ def audit_environment_namespaces(root: pathlib.Path) -> list[Violation]:
|
|||||||
violations.append(
|
violations.append(
|
||||||
Violation(
|
Violation(
|
||||||
"KH_ENV003",
|
"KH_ENV003",
|
||||||
"config/example.config.json",
|
relative,
|
||||||
text[: match.start()].count("\n") + 1,
|
text[: match.start()].count("\n") + 1,
|
||||||
f"configuration placeholder `{name}` must use a `KS_*` or `KB_*` namespace",
|
f"configuration placeholder `{name}` must use a `KS_*` or `KB_*` namespace",
|
||||||
)
|
)
|
||||||
@@ -941,6 +948,86 @@ def audit_environment_namespaces(root: pathlib.Path) -> list[Violation]:
|
|||||||
return violations
|
return violations
|
||||||
|
|
||||||
|
|
||||||
|
def audit_configuration_split(root: pathlib.Path) -> list[Violation]:
|
||||||
|
"""Require the independent application and logging configuration contract."""
|
||||||
|
|
||||||
|
violations: list[Violation] = []
|
||||||
|
required_files = [
|
||||||
|
"config/app.config.json",
|
||||||
|
"config/logging.config.json",
|
||||||
|
"config/example.app.config.json",
|
||||||
|
"config/example.logging.config.json",
|
||||||
|
"config/schemas/app.config.schema.json",
|
||||||
|
"config/schemas/logging.config.schema.json",
|
||||||
|
]
|
||||||
|
for relative in required_files:
|
||||||
|
if (root / relative).is_file():
|
||||||
|
continue
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
"KH_CFG001",
|
||||||
|
relative,
|
||||||
|
1,
|
||||||
|
"required split configuration file is missing",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
app_path = root / "config/app.config.json"
|
||||||
|
if app_path.is_file():
|
||||||
|
try:
|
||||||
|
app = json.loads(app_path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
app = None
|
||||||
|
if isinstance(app, dict):
|
||||||
|
for index, profile in enumerate(app.get("profiles", []), 1):
|
||||||
|
if isinstance(profile, dict) and "logging" in profile:
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
"KH_CFG002",
|
||||||
|
"config/app.config.json",
|
||||||
|
index,
|
||||||
|
"application profiles must not embed logging configuration",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
logging_path = root / "config/logging.config.json"
|
||||||
|
if logging_path.is_file():
|
||||||
|
try:
|
||||||
|
logging = json.loads(logging_path.read_text(encoding="utf-8"))
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logging = None
|
||||||
|
if isinstance(logging, dict):
|
||||||
|
if not isinstance(logging.get("active_profile"), str):
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
"KH_CFG003",
|
||||||
|
"config/logging.config.json",
|
||||||
|
1,
|
||||||
|
"logging configuration must define its own active_profile",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if not isinstance(logging.get("profiles"), list) or not logging.get("profiles"):
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
"KH_CFG004",
|
||||||
|
"config/logging.config.json",
|
||||||
|
1,
|
||||||
|
"logging configuration must define independent named profiles",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
legacy = ["config/example.config.json", "config/schema.config.json"]
|
||||||
|
for relative in legacy:
|
||||||
|
if not (root / relative).exists():
|
||||||
|
continue
|
||||||
|
violations.append(
|
||||||
|
Violation(
|
||||||
|
"KH_CFG005",
|
||||||
|
relative,
|
||||||
|
1,
|
||||||
|
"legacy combined configuration file must be removed after the split",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return violations
|
||||||
|
|
||||||
|
|
||||||
def audit_wincode_resolution(root: pathlib.Path) -> list[Violation]:
|
def audit_wincode_resolution(root: pathlib.Path) -> list[Violation]:
|
||||||
"""Require the currently compatible Solana wincode dependency family."""
|
"""Require the currently compatible Solana wincode dependency family."""
|
||||||
|
|
||||||
@@ -1024,6 +1111,7 @@ def main() -> int:
|
|||||||
+audit_private_ks_lib_paths_in_active_docs(root)
|
+audit_private_ks_lib_paths_in_active_docs(root)
|
||||||
+audit_solana_types(root)
|
+audit_solana_types(root)
|
||||||
+audit_environment_namespaces(root)
|
+audit_environment_namespaces(root)
|
||||||
|
+audit_configuration_split(root)
|
||||||
+audit_wincode_resolution(root)
|
+audit_wincode_resolution(root)
|
||||||
)
|
)
|
||||||
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
|
violations.sort(key=lambda item: (item.code, item.path, item.line, item.message))
|
||||||
|
|||||||
Reference in New Issue
Block a user