# Guide de migration : TR-069 Exchange Viewer ## Résumé Ce guide décrit la migration du système de trace TR-069 depuis le stockage fichier (logs `.log` rotatifs) vers le stockage structuré MongoDB (`cwmp_exchanges`). La migration suit une approche en 3 phases pour garantir zéro perte de données et un rollback possible à chaque étape. | Phase | Durée | Description | Risque | |-------|-------|-------------|--------| | Phase 1 — Dual-Write | Semaines 1-2 | Écriture simultanée fichiers + MongoDB | Faible | | Phase 2 — Read from MongoDB | Semaines 3-4 | API lit depuis MongoDB, fichiers en backup | Moyen | | Phase 3 — Deprecate Files | Semaine 5+ | Suppression du système fichier | Irréversible | --- ## Prérequis ### Infrastructure - MongoDB 5.0+ (pas besoin de replica set — le système utilise un EventEmitter in-process, pas de change streams) - Espace disque MongoDB : prévoir ~500 Mo pour 10 000 devices actifs avec 5 000 messages max chacun - L'application adam-acs doit être redémarrée après la migration de base ### Variables d'environnement ```bash # Feature flags (déjà présents dans config/exchange_store.ts) FEATURE_EXCHANGE_PERSISTENCE=true # Active les écritures MongoDB FEATURE_SSE_ENABLED=true # Active le streaming SSE FEATURE_EXCHANGE_API_SOURCE=mongodb # Source des données API (mongodb | file) # Configuration optionnelle EXCHANGE_STORE_MAX_MESSAGES=5000 # Cap messages par device EXCHANGE_STORE_TTL_SECONDS=86400 # TTL 24h EXCHANGE_STORE_READ_TIMEOUT_MS=5000 # Timeout requêtes API ``` --- ## Phase 1 : Dual-Write (Semaines 1-2) ### Objectif Écrire simultanément dans les deux systèmes (fichiers ET MongoDB) pour valider la fiabilité du nouveau système sans risque de perte. ### 1.1 Exécuter la migration MongoDB La migration crée la collection `cwmp_exchanges` et ses index : ```bash # Exécuter la migration node ace migration:run --connection=mongodb ``` Cela exécute `database/mongo_migrations/011_create_cwmp_exchanges_collection.ts` qui crée : - Collection `cwmp_exchanges` avec 3 index : - `{ deviceId: 1, timestamp: 1 }` — requêtes par device - `{ deviceId: 1, sequenceNumber: 1 }` (unique) — numérotation séquentielle et reconnexion SSE - `{ createdAt: 1 }` (TTL: 86400s) — nettoyage automatique après 24h - Collection `cwmp_exchange_counters` — compteurs atomiques de séquence ### 1.2 Vérifier les index ```bash # Via mongosh mongosh --eval " db.cwmp_exchanges.getIndexes().forEach(idx => { print(idx.name + ': ' + JSON.stringify(idx.key)) }) " ``` Résultat attendu : ``` _id_: {"_id":1} idx_deviceId_timestamp: {"deviceId":1,"timestamp":1} idx_deviceId_sequenceNumber: {"deviceId":1,"sequenceNumber":1} idx_createdAt_ttl: {"createdAt":1} ``` ### 1.3 Activer le dual-write Le dual-write est déjà implémenté dans `CwmpLoggerMiddleware`. Vérifier les feature flags : ```bash # .env — Phase 1 FEATURE_EXCHANGE_PERSISTENCE=true FEATURE_SSE_ENABLED=true FEATURE_EXCHANGE_API_SOURCE=file # GARDER 'file' en Phase 1 ``` Le middleware écrit dans les deux systèmes : ```typescript // app/middleware/cwmp_logger_middleware.ts (extrait) if (isTraced && deviceId) { // Nouveau système : MongoDB (fire-and-forget) exchangeStore.persistExchange({ deviceId, direction, sessionId, rawXml }) .catch(() => {}) // Ancien système : fichiers (gardé pour rollback) traceService.writeExchange(deviceId, direction, sessionId, prettyXml) } ``` ### 1.4 Redémarrer l'application ```bash docker compose build app docker compose up -d app ``` ### 1.5 Vérifier le fonctionnement Activer le trace sur un device et déclencher un Inform : ```bash # Vérifier que MongoDB reçoit les données mongosh --eval "db.cwmp_exchanges.countDocuments({})" # Vérifier les derniers échanges mongosh --eval " db.cwmp_exchanges.find({}).sort({timestamp: -1}).limit(3).forEach(doc => { print(doc.deviceId + ' | ' + doc.direction + ' | ' + doc.cwmpMethod) }) " ``` ### 1.6 Vérification de cohérence (Dual-Write) Pendant la Phase 1, vérifier régulièrement que les deux systèmes restent synchronisés : #### Script de comparaison ```bash #!/bin/bash # scripts/verify_dual_write.sh # Compare le nombre d'échanges entre fichiers et MongoDB pour chaque device tracé DEVICE_ID=$1 if [ -z "$DEVICE_ID" ]; then echo "Usage: $0 " exit 1 fi # Compteur fichier (nombre de séparateurs '═' = nombre d'échanges) FILE_COUNT=$(grep -c '═════' "logs/tr069/${DEVICE_ID}.log" 2>/dev/null || echo "0") # Compteur MongoDB MONGO_COUNT=$(mongosh --quiet --eval "db.cwmp_exchanges.countDocuments({deviceId: '${DEVICE_ID}'})") echo "Device: ${DEVICE_ID}" echo " Fichiers : ${FILE_COUNT} échanges" echo " MongoDB : ${MONGO_COUNT} échanges" if [ "$FILE_COUNT" != "$MONGO_COUNT" ]; then DIFF=$((FILE_COUNT - MONGO_COUNT)) echo " ⚠️ Écart : ${DIFF} messages" if [ "${DIFF#-}" -gt $((FILE_COUNT / 20)) ]; then echo " 🚨 Écart > 5% — Investigation requise !" fi else echo " ✅ Cohérent" fi ``` #### Critères d'acceptation Phase 1 | Critère | Seuil | Action si dépassé | |---------|-------|-------------------| | Écart fichier/MongoDB | < 5% | Vérifier les logs d'erreur ExchangeStore | | Erreurs de persistance | < 0.1% | Vérifier la connectivité MongoDB | | Latence MongoDB p95 | < 100ms | Vérifier la charge et les index | | Messages perdus (critiques) | 0 après retries | Vérifier les logs de retry exhaustion | #### Durée recommandée Maintenir le dual-write pendant **minimum 1 semaine** avec des vérifications quotidiennes. Passer en Phase 2 uniquement si les critères ci-dessus sont respectés de manière constante. --- ## Phase 2 : Read from MongoDB (Semaines 3-4) ### Objectif Basculer la lecture des données vers MongoDB tout en gardant les fichiers en écriture backup. ### 2.1 Changer la source de lecture API ```bash # .env — Phase 2 FEATURE_EXCHANGE_PERSISTENCE=true FEATURE_SSE_ENABLED=true FEATURE_EXCHANGE_API_SOURCE=mongodb # ← Changer de 'file' à 'mongodb' ``` ### 2.2 Redémarrer l'application ```bash docker compose restart app ``` ### 2.3 Vérifier le fonctionnement ```bash # Vérifier que l'API retourne des données depuis MongoDB curl -s "http://localhost:8080/api/v1/tr069-trace//exchanges?limit=5" | \ python3 -c " import sys, json data = json.load(sys.stdin) print(f\"Source: MongoDB\") print(f\"Exchanges: {len(data.get('exchanges', []))} messages\") print(f\"Metadata: {data.get('metadata', {})}\") " ``` ### 2.4 Vérifier le SSE ```bash # Ouvrir une connexion SSE (ctrl+C pour arrêter) curl -N -H "Accept: text/event-stream" \ "http://localhost:8080/api/v1/tr069-trace//exchanges/stream" ``` Déclencher un Inform sur le device tracé et vérifier qu'un événement SSE arrive en temps réel. ### 2.5 Vérifier le fallback Si MongoDB est indisponible, le frontend doit retomber sur les fichiers : ```typescript // Logique dans DeviceDetail.vue const exchanges = await api.getExchanges(deviceId) .catch(() => legacyService.readExchanges(deviceId)) ``` ### 2.6 Critères de passage Phase 2 → Phase 3 | Critère | Seuil | Vérifié | |---------|-------|---------| | API MongoDB fonctionne sans erreur | 48h continues | ☐ | | SSE pousse les événements en < 500ms | p95 vérifié | ☐ | | Aucune perte de message critique | 0 sur 48h | ☐ | | Reconnexion SSE (Last-Event-ID) valide | Testé manuellement | ☐ | | Purge (DELETE) fonctionne | 204 retourné | ☐ | --- ## Phase 3 : Deprecate Old System (Semaine 5+) ### Objectif Supprimer le système fichier une fois que MongoDB est validé en production. ### 3.1 Supprimer le dual-write du middleware Dans `app/middleware/cwmp_logger_middleware.ts`, retirer les appels au système fichier : ```typescript // AVANT (dual-write) if (isTraced && deviceId) { exchangeStore.persistExchange({ deviceId, direction, sessionId, rawXml }).catch(() => {}) traceService.writeExchange(deviceId, direction, sessionId, prettyXml) // ← SUPPRIMER } // APRÈS (MongoDB uniquement) if (isTraced && deviceId) { exchangeStore.persistExchange({ deviceId, direction, sessionId, rawXml }).catch(() => {}) } ``` ### 3.2 Supprimer les dépendances obsolètes 1. Retirer l'import `xml-formatter` du middleware (le formatage n'est plus nécessaire côté persistance) 2. Supprimer les appels à `Tr069TraceService.writeExchange()` dans le middleware 3. **NE PAS** supprimer `Tr069TraceService` lui-même — il est encore utilisé pour gérer l'état trace (start/stop) ### 3.3 Nettoyer les fichiers de logs ```bash # Archiver les anciens logs avant suppression tar -czf logs/tr069-archive-$(date +%Y%m%d).tar.gz logs/tr069/ rm -rf logs/tr069/*.log* ``` ### 3.4 Mise à jour de la configuration ```bash # .env — Phase 3 (état final) FEATURE_EXCHANGE_PERSISTENCE=true FEATURE_SSE_ENABLED=true FEATURE_EXCHANGE_API_SOURCE=mongodb ``` ### 3.5 Vérification finale ```bash # Vérifier qu'aucun fichier .log n'est créé ls -la logs/tr069/ # Vérifier que le système fonctionne uniquement via MongoDB curl -s "http://localhost:8080/api/v1/health/tr069-exchanges" | python3 -m json.tool ``` --- ## Procédures de Rollback ### Rollback Phase 1 → Système fichier seul **Situation :** MongoDB pose problème, il faut revenir au système fichier uniquement. ```bash # 1. Désactiver la persistance MongoDB FEATURE_EXCHANGE_PERSISTENCE=false # 2. Redémarrer docker compose restart app # 3. Vérifier que les fichiers continuent d'être écrits ls -la logs/tr069/ ``` **Impact :** Les données déjà en MongoDB restent accessibles mais ne sont plus alimentées. Le SSE ne fonctionne plus. L'UI retombe sur les fichiers. **Temps de rollback :** < 2 minutes (changement d'env + restart) ### Rollback Phase 2 → Phase 1 (Dual-Write) **Situation :** L'API MongoDB a des problèmes, revenir au dual-write avec lecture fichier. ```bash # 1. Changer la source API FEATURE_EXCHANGE_API_SOURCE=file # 2. Redémarrer docker compose restart app ``` **Impact :** L'API lit les fichiers au lieu de MongoDB. Le dual-write continue. Aucune perte de données. **Temps de rollback :** < 2 minutes ### Rollback Phase 3 → Phase 2 **Situation :** Après suppression du dual-write, MongoDB pose problème. **⚠️ ATTENTION : Ce rollback nécessite un redéploiement du code.** 1. Revert du commit qui a supprimé le dual-write dans `CwmpLoggerMiddleware` 2. Redéployer l'application 3. Vérifier que les fichiers sont à nouveau écrits ```bash # Identifier le commit de suppression du dual-write git log --oneline -- app/middleware/cwmp_logger_middleware.ts # Revert git revert docker compose build app docker compose up -d app ``` **Impact :** Nécessite un redéploiement (~5 minutes). Les échanges entre la suppression et le rollback ne sont PAS dans les fichiers. **Temps de rollback :** 5-10 minutes ### Rollback SSE uniquement **Situation :** Le SSE cause des problèmes (connexions, mémoire) mais MongoDB est stable. ```bash # Désactiver uniquement le SSE FEATURE_SSE_ENABLED=false # Redémarrer docker compose restart app ``` **Impact :** Le frontend bascule automatiquement en REST polling (toutes les 5 secondes). Les données MongoDB sont toujours disponibles. Pas de perte. ### Rollback complet (retour système fichier) **Situation :** Problème majeur, tout désactiver. ```bash # Tout désactiver FEATURE_EXCHANGE_PERSISTENCE=false FEATURE_SSE_ENABLED=false FEATURE_EXCHANGE_API_SOURCE=file # Redémarrer docker compose restart app ``` **Impact :** Retour complet au système fichier. MongoDB n'est plus utilisé. L'UI affiche les logs comme avant la migration. --- ## Migration des données historiques (optionnel) Si vous souhaitez importer les logs fichier existants dans MongoDB, utilisez le script ci-dessous. Cette étape est **optionnelle** car les anciens logs disparaîtront naturellement via le TTL 24h. ### Script d'import ```typescript // scripts/migrate_file_logs_to_mongodb.ts import { readFileSync, readdirSync } from 'node:fs' import { join } from 'node:path' import { getMongoDb } from '#providers/mongodb_provider' const LOG_DIR = join(process.cwd(), 'logs', 'tr069') const SEPARATOR = '═'.repeat(80) interface ParsedEntry { deviceId: string timestamp: string direction: 'CPE→ACS' | 'ACS→CPE' sessionId: string rawXml: string } function parseLogFile(deviceId: string, content: string): ParsedEntry[] { const entries: ParsedEntry[] = [] const blocks = content.split(SEPARATOR).filter((b) => b.trim()) for (let i = 0; i < blocks.length - 1; i += 2) { const header = blocks[i].trim() const xml = blocks[i + 1]?.trim() || '' // Parse header: [2025-01-15T10:30:00.123Z] CPE→ACS | 10.0.0.1:54321->192.168.1.1:8080 const headerMatch = header.match( /\[(.+?)\]\s+(CPE→ACS|ACS→CPE)\s+\|\s+(.+)/ ) if (!headerMatch) continue entries.push({ deviceId, timestamp: headerMatch[1], direction: headerMatch[2] as 'CPE→ACS' | 'ACS→CPE', sessionId: headerMatch[3], rawXml: xml, }) } return entries } async function migrateAll(): Promise { const db = getMongoDb() const collection = db.collection('cwmp_exchanges') const counters = db.collection('cwmp_exchange_counters') const files = readdirSync(LOG_DIR).filter((f) => f.endsWith('.log')) let totalMigrated = 0 for (const file of files) { const deviceId = file.replace('.log', '') const content = readFileSync(join(LOG_DIR, file), 'utf-8') const entries = parseLogFile(deviceId, content) if (entries.length === 0) continue // Get or initialize sequence counter const counterResult = await counters.findOneAndUpdate( { _id: deviceId as any }, { $inc: { seq: entries.length } }, { upsert: true, returnDocument: 'after' } ) const startSeq = (counterResult!.seq as number) - entries.length + 1 // Build documents const docs = entries.map((entry, idx) => ({ ...entry, sequenceNumber: startSeq + idx, cwmpMethod: extractMethod(entry.rawXml), createdAt: new Date(entry.timestamp), })) // Bulk insert await collection.insertMany(docs, { ordered: false }).catch((err) => { console.warn(` ⚠️ Duplicates ignored for ${deviceId}: ${err.message}`) }) totalMigrated += docs.length console.log(` ✅ ${deviceId}: ${docs.length} échanges migrés`) } console.log(`\n📊 Total: ${totalMigrated} échanges migrés depuis ${files.length} fichiers`) } function extractMethod(xml: string): string { if (!xml || xml.trim() === '') return 'Empty' const match = xml.match(/]/) return match ? match[1] : 'Unknown' } migrateAll().catch(console.error) ``` ### Exécution ```bash # Depuis la racine du projet npx ts-node scripts/migrate_file_logs_to_mongodb.ts ``` **Notes :** - Le script est idempotent : les doublons sont ignorés (grâce à l'index unique deviceId + sequenceNumber) - Les documents importés auront un `createdAt` basé sur le timestamp du log, donc ils seront supprimés par le TTL 24h si ils datent de plus de 24 heures - Pour conserver les données historiques plus longtemps, ajustez temporairement `EXCHANGE_STORE_TTL_SECONDS` --- ## Vérifications post-migration ### Checklist | # | Vérification | Commande | Attendu | |---|-------------|----------|---------| | 1 | Collection existe | `db.cwmp_exchanges.stats()` | Collection présente | | 2 | Index créés | `db.cwmp_exchanges.getIndexes()` | 4 index (dont _id) | | 3 | TTL actif | `db.cwmp_exchanges.getIndexes()` | `expireAfterSeconds: 86400` | | 4 | Compteurs fonctionnent | `db.cwmp_exchange_counters.find()` | Entrées par device | | 5 | API accessible | `GET /api/v1/tr069-trace/:id/exchanges` | 200 avec données | | 6 | SSE connecte | `GET /api/v1/tr069-trace/:id/exchanges/stream` | Event stream ouvert | | 7 | Health check | `GET /api/v1/health/tr069-exchanges` | `status: "healthy"` | ### Commandes de diagnostic ```bash # Statistiques collection mongosh --eval " const stats = db.cwmp_exchanges.stats() print('Documents:', stats.count) print('Taille:', (stats.size / 1024 / 1024).toFixed(2), 'Mo') print('Index size:', (stats.totalIndexSize / 1024 / 1024).toFixed(2), 'Mo') " # Vérifier le TTL monitor mongosh --eval " db.cwmp_exchanges.aggregate([ { \$group: { _id: null, oldest: { \$min: '\$createdAt' }, newest: { \$max: '\$createdAt' } } } ]).forEach(doc => { print('Plus ancien:', doc.oldest) print('Plus récent:', doc.newest) const ageH = (Date.now() - new Date(doc.oldest).getTime()) / 3600000 print('Âge max:', ageH.toFixed(1), 'heures (devrait être < 24h)') }) " # Vérifier les compteurs de séquence mongosh --eval " db.cwmp_exchange_counters.find().sort({seq: -1}).limit(5).forEach(doc => { print(doc._id + ': seq=' + doc.seq) }) " ``` --- ## Résumé des feature flags | Flag | Phase 1 | Phase 2 | Phase 3 | Rollback | |------|---------|---------|---------|----------| | `FEATURE_EXCHANGE_PERSISTENCE` | `true` | `true` | `true` | `false` | | `FEATURE_SSE_ENABLED` | `true` | `true` | `true` | `false` | | `FEATURE_EXCHANGE_API_SOURCE` | `file` | `mongodb` | `mongodb` | `file` | --- ## FAQ ### Que se passe-t-il si MongoDB tombe pendant le dual-write ? Les écritures MongoDB échouent silencieusement (fire-and-forget). Le système fichier continue normalement. Les messages critiques sont retentés 3 fois avec backoff exponentiel (1s, 2s, 4s) avant d'être abandonnés. La session CWMP n'est jamais interrompue. ### Le TTL va-t-il supprimer des données pendant la migration ? Oui. Le TTL de 24h s'applique dès la création de l'index. Si vous importez des logs historiques de plus de 24h, ils seront supprimés par le TTL monitor MongoDB (qui tourne ~toutes les 60 secondes). Augmentez temporairement `EXCHANGE_STORE_TTL_SECONDS` si nécessaire. ### Puis-je migrer les données fichier vers MongoDB ? Oui, voir la section "Migration des données historiques". C'est optionnel car le système fonctionne immédiatement avec les nouveaux échanges. Les anciens logs fichier restent lisibles via `FEATURE_EXCHANGE_API_SOURCE=file`. ### Combien d'espace MongoDB est nécessaire ? Estimation : ~1 Ko par échange (métadonnées) + taille du XML brut (typiquement 2-50 Ko). Avec le cap de 5 000 messages par device et le TTL 24h, la croissance est bornée. Pour 100 devices tracés simultanément : ~500 Mo maximum. ### Le dual-write a-t-il un impact sur les performances CWMP ? Non. Les écritures MongoDB sont fire-and-forget (`catch(() => {})`) et ne bloquent jamais le traitement CWMP. En cas de lenteur MongoDB, le middleware continue sans attendre.