feat: TR-069 Exchange Viewer - structured MongoDB storage with SSE streaming and sequence diagram UI

- MongoDB storage: cwmp_exchanges collection with indexes (deviceId+timestamp, deviceId+sequenceNumber unique, TTL 24h)
- ExchangeStoreService: CWMP method extraction, per-device queue for ordering, retry logic, message cap (5000/device)
- Exchange API: GET/DELETE/SSE stream endpoints with Last-Event-ID reconnection
- CwmpLoggerMiddleware: dual-write (MongoDB + legacy files) with fire-and-forget
- Frontend: SequenceViewer (CPE↔ACS lifelines, arrows, auto-scroll, virtual scroll) + InspectorPanel (formatted XML, copy, syntax highlighting)
- Tr069Trace page: integrated new viewer with col-6/col-6 layout
- DeviceDetail page: collapsible exchange section when device is traced
- 14 property-based tests, 40+ unit tests, 5 integration tests
- Health check includes index verification
- Config: feature flags, SSE timeouts, retry policy
This commit is contained in:
Kevin L'Hereec committed 2026-06-03 15:52:44 +02:00
1 parent 3fefbf6c40
commit 0c83a4ccb2
50 files changed
+15731 -43

No files matched your search

+475
View File
@@ -0,0 +1,475 @@
openapi: 3.0.3
info:
title: TR-069 Exchange Viewer API
description: |
API REST pour la récupération, la suppression et le streaming en temps réel
des échanges CWMP/TR-069 capturés lors d'une session de trace sur un device.
## Architecture
Les échanges SOAP sont capturés par le `CwmpLoggerMiddleware`, persistés dans
MongoDB via `ExchangeStoreService`, et diffusés en temps réel via Server-Sent Events (SSE).
## Authentification
Tous les endpoints nécessitent une session authentifiée (cookie-based via AdonisJS).
## Feature Flags
| Variable d'environnement | Effet |
|--------------------------|-------|
| `FEATURE_EXCHANGE_PERSISTENCE=false` | Désactive l'écriture MongoDB |
| `FEATURE_SSE_ENABLED=false` | Désactive le SSE (endpoint renvoie 503) |
| `FEATURE_EXCHANGE_API_SOURCE=file` | Lecture depuis l'ancien système fichier |
version: 1.0.0
contact:
name: adam-acs Team
servers:
- url: http://localhost:8080/api/v1
description: Local development
paths:
/tr069-trace/{deviceId}/exchanges:
get:
operationId: getExchanges
summary: Récupérer les échanges CWMP d'un device
description: |
Retourne les échanges CWMP structurés pour un device tracé, ordonnés par
timestamp ascending. Inclut des métadonnées sur le statut de la trace et
la fenêtre d'enregistrement.
**Pagination cursor-based** : utiliser `after` pour charger les messages plus
récents, `before` pour charger les messages plus anciens, ou `latest=true`
pour obtenir les N messages les plus récents (chargement initial).
tags:
- Exchanges
parameters:
- $ref: '#/components/parameters/deviceId'
- name: limit
in: query
required: false
description: |
Nombre maximum de messages retournés.
- Défaut : 200
- Minimum : 1
- Maximum : 1000
- Retourne HTTP 400 si non-numérique ou < 1
schema:
type: integer
minimum: 1
maximum: 1000
default: 200
example: 50
- name: after
in: query
required: false
description: |
Timestamp ISO 8601 pour la pagination cursor-based (messages plus récents).
Seuls les messages avec un timestamp strictement supérieur à cette valeur
sont retournés. Retourne HTTP 400 si le format est invalide.
schema:
type: string
format: date-time
example: '2025-01-15T10:30:00.000Z'
- name: before
in: query
required: false
description: |
Timestamp ISO 8601 pour la pagination inverse (messages plus anciens).
Seuls les messages avec un timestamp strictement inférieur à cette valeur
sont retournés. Retourne HTTP 400 si le format est invalide.
schema:
type: string
format: date-time
example: '2025-01-15T10:35:00.000Z'
- name: latest
in: query
required: false
description: |
Si `true`, retourne les N messages les plus récents (pour le chargement
initial). Combiné avec `limit` pour contrôler le nombre de messages.
schema:
type: string
enum:
- 'true'
- 'false'
example: 'true'
responses:
'200':
description: Liste des échanges avec métadonnées
content:
application/json:
schema:
$ref: '#/components/schemas/ExchangeListResponse'
examples:
activeTrace:
summary: Device actuellement tracé avec des échanges
value:
exchanges:
- sequenceNumber: 1
timestamp: '2025-01-15T10:30:00.123Z'
direction: 'CPE→ACS'
cwmpMethod: 'Inform'
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
rawXml: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0"><soap-env:Body><cwmp:Inform>...</cwmp:Inform></soap-env:Body></soap-env:Envelope>'
- sequenceNumber: 2
timestamp: '2025-01-15T10:30:00.456Z'
direction: 'ACS→CPE'
cwmpMethod: 'InformResponse'
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
rawXml: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:cwmp="urn:dslforum-org:cwmp-1-0"><soap-env:Body><cwmp:InformResponse><MaxEnvelopes>1</MaxEnvelopes></cwmp:InformResponse></soap-env:Body></soap-env:Envelope>'
metadata:
traceStatus: 'ACTIVE'
traceStoppedAt: null
recordedFrom: '2025-01-15T10:30:00.123Z'
recordedUntil: '2025-01-15T10:35:42.567Z'
total: 42
stoppedTrace:
summary: Trace arrêtée avec historique conservé
value:
exchanges:
- sequenceNumber: 1
timestamp: '2025-01-15T10:30:00.123Z'
direction: 'CPE→ACS'
cwmpMethod: 'Inform'
sessionId: '10.0.0.1:54321->192.168.1.1:8080'
rawXml: '<soap-env:Envelope>...</soap-env:Envelope>'
metadata:
traceStatus: 'STOPPED'
traceStoppedAt: '2025-01-15T10:40:00.000Z'
recordedFrom: '2025-01-15T10:30:00.123Z'
recordedUntil: '2025-01-15T10:35:42.567Z'
total: 15
emptyTrace:
summary: Device tracé mais aucun échange encore enregistré
value:
exchanges: []
metadata:
traceStatus: 'ACTIVE'
traceStoppedAt: null
recordedFrom: null
recordedUntil: null
total: 0
truncatedResult:
summary: Résultat partiel (timeout de lecture dépassé)
value:
exchanges: []
metadata:
traceStatus: 'ACTIVE'
traceStoppedAt: null
recordedFrom: '2025-01-15T10:30:00.123Z'
recordedUntil: '2025-01-15T10:35:42.567Z'
total: 5000
isTruncated: true
'400':
description: Paramètre de requête invalide
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
examples:
invalidLimit:
summary: Paramètre limit invalide
value:
error: 'Invalid limit parameter. Must be an integer between 1 and 1000.'
invalidAfter:
summary: Paramètre after invalide
value:
error: 'Invalid after parameter. Must be a valid ISO 8601 timestamp.'
invalidBefore:
summary: Paramètre before invalide
value:
error: 'Invalid before parameter. Must be a valid ISO 8601 timestamp.'
'404':
description: Le device n'a jamais été tracé (aucun échange historique)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: 'No trace exists for device F8B7FF-Livebox4-LB4-TEST-001'
delete:
operationId: deleteExchanges
summary: Supprimer tous les échanges d'un device
description: |
Supprime tous les échanges CWMP stockés pour le device spécifié.
Retourne 204 sans corps de réponse en cas de succès.
Cette opération est irréversible. Si le device est actuellement tracé,
les nouveaux échanges continueront d'être enregistrés après la suppression.
tags:
- Exchanges
parameters:
- $ref: '#/components/parameters/deviceId'
responses:
'204':
description: Échanges supprimés avec succès (pas de corps de réponse)
/tr069-trace/{deviceId}/exchanges/stream:
get:
operationId: streamExchanges
summary: Flux SSE temps réel des échanges CWMP
description: |
Ouvre une connexion HTTP persistante avec `Content-Type: text/event-stream`
qui pousse chaque nouvel échange CWMP dès qu'il est persisté.
## Protocole SSE
La connexion utilise le protocole Server-Sent Events (SSE) standard :
- **Content-Type** : `text/event-stream`
- **Cache-Control** : `no-cache`
- **Connection** : `keep-alive`
- **X-Accel-Buffering** : `no` (pour nginx)
## Format des événements
Chaque événement SSE contient :
```
id: <sequenceNumber>
data: <JSON-encoded CwmpExchange>
```
Exemple d'événement brut :
```
id: 42
data: {"sequenceNumber":42,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
```
## Reconnexion (Last-Event-ID)
Lors d'une reconnexion, le client envoie l'en-tête `Last-Event-ID` avec le
`sequenceNumber` du dernier événement reçu. Le serveur reprend alors à partir
du message suivant (sequenceNumber > Last-Event-ID), sans duplication.
Le serveur envoie un champ `retry` au début de la connexion (défaut: 3000ms)
pour configurer l'intervalle de reconnexion automatique du client.
## Timeouts serveur
Le serveur ferme la connexion dans les cas suivants :
- **Inactivité** : aucun nouvel échange poussé pendant 5 minutes
- **Durée absolue** : connexion ouverte depuis plus de 2 heures
Dans les deux cas, le client devrait se reconnecter automatiquement
(mécanisme natif EventSource avec Last-Event-ID).
## Feature Flag
Si `FEATURE_SSE_ENABLED=false`, cet endpoint retourne HTTP 503.
tags:
- Exchanges
- SSE
parameters:
- $ref: '#/components/parameters/deviceId'
- name: Last-Event-ID
in: header
required: false
description: |
Sequence number du dernier événement reçu par le client.
Utilisé pour reprendre le flux après une déconnexion sans duplication.
schema:
type: string
example: '42'
responses:
'200':
description: Flux SSE ouvert avec succès
headers:
Content-Type:
schema:
type: string
example: text/event-stream
Cache-Control:
schema:
type: string
example: no-cache
Connection:
schema:
type: string
example: keep-alive
X-Accel-Buffering:
schema:
type: string
example: 'no'
content:
text/event-stream:
schema:
type: string
description: Flux d'événements SSE (voir description pour le format)
example: |
retry: 3000
id: 1
data: {"sequenceNumber":1,"timestamp":"2025-01-15T10:30:00.123Z","direction":"CPE→ACS","cwmpMethod":"Inform","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
id: 2
data: {"sequenceNumber":2,"timestamp":"2025-01-15T10:30:00.456Z","direction":"ACS→CPE","cwmpMethod":"InformResponse","sessionId":"10.0.0.1:54321->192.168.1.1:8080","rawXml":"<soap-env:Envelope>...</soap-env:Envelope>"}
'404':
description: Le device n'est pas actuellement tracé
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: 'Device is not being traced'
'503':
description: Le streaming SSE est désactivé (feature flag)
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error: 'SSE streaming is currently disabled'
components:
parameters:
deviceId:
name: deviceId
in: path
required: true
description: |
Identifiant du device (format: `OUI-ProductClass-SerialNumber`
ou hash MongoDB `_id`).
schema:
type: string
maxLength: 128
example: 'F8B7FF-Livebox4-LB4-TEST-001'
schemas:
CwmpExchange:
type: object
description: Un échange SOAP CWMP individuel capturé
required:
- sequenceNumber
- timestamp
- direction
- cwmpMethod
- sessionId
- rawXml
properties:
sequenceNumber:
type: integer
description: |
Numéro de séquence monotoniquement croissant par device (commence à 1).
Utilisé comme identifiant d'événement SSE pour la reconnexion.
example: 42
timestamp:
type: string
format: date-time
description: Horodatage UTC de la capture (ISO 8601)
example: '2025-01-15T10:30:00.123Z'
direction:
type: string
enum:
- 'CPE→ACS'
- 'ACS→CPE'
description: |
Direction du message :
- `CPE→ACS` : requête envoyée par le CPE vers l'ACS
- `ACS→CPE` : réponse envoyée par l'ACS vers le CPE
example: 'CPE→ACS'
cwmpMethod:
type: string
description: |
Méthode CWMP extraite du body SOAP :
- Nom de la méthode (ex: `Inform`, `GetParameterValues`, `SetParameterValuesResponse`)
- `"Empty"` si le body XML est vide (empty POST du CPE)
- `"Unknown"` si aucun élément `cwmp:` n'est trouvé dans le body
example: 'Inform'
sessionId:
type: string
description: Identifiant de session TCP (format `IP:port->IP:port`)
example: '10.0.0.1:54321->192.168.1.1:8080'
rawXml:
type: string
description: |
Contenu XML SOAP brut du message (maximum 2 Mo).
Peut être vide pour les messages de type "Empty".
maxLength: 2097152
example: '<soap-env:Envelope xmlns:soap-env="http://schemas.xmlsoap.org/soap/envelope/">...</soap-env:Envelope>'
ExchangeMetadata:
type: object
description: Métadonnées sur la session de trace du device
required:
- traceStatus
- traceStoppedAt
- recordedFrom
- recordedUntil
- total
properties:
traceStatus:
type: string
enum:
- ACTIVE
- STOPPED
description: |
Statut actuel de la trace :
- `ACTIVE` : le device est en cours de trace
- `STOPPED` : la trace a été arrêtée (historique conservé)
example: 'ACTIVE'
traceStoppedAt:
type: string
format: date-time
nullable: true
description: Timestamp d'arrêt de la trace (null si active)
example: null
recordedFrom:
type: string
format: date-time
nullable: true
description: Timestamp du plus ancien échange enregistré
example: '2025-01-15T10:30:00.123Z'
recordedUntil:
type: string
format: date-time
nullable: true
description: Timestamp du plus récent échange enregistré
example: '2025-01-15T10:35:42.567Z'
total:
type: integer
description: Nombre total d'échanges stockés pour ce device
example: 42
isTruncated:
type: boolean
description: |
Présent uniquement si le résultat est partiel (timeout de lecture dépassé).
Le client devrait réessayer avec des paramètres de pagination plus restreints.
example: true
ExchangeListResponse:
type: object
description: Réponse de l'endpoint GET /exchanges
required:
- exchanges
- metadata
properties:
exchanges:
type: array
items:
$ref: '#/components/schemas/CwmpExchange'
description: Liste des échanges ordonnés par timestamp ascending
metadata:
$ref: '#/components/schemas/ExchangeMetadata'
ErrorResponse:
type: object
description: Réponse d'erreur standard
required:
- error
properties:
error:
type: string
description: Message d'erreur décrivant le problème
example: 'Invalid limit parameter. Must be an integer between 1 and 1000.'
tags:
- name: Exchanges
description: Gestion des échanges CWMP capturés
- name: SSE
description: Streaming temps réel via Server-Sent Events
+363
View File
@@ -0,0 +1,363 @@
# Deployment Guide: TR-069 Exchange Viewer
## Overview
The TR-069 Exchange Viewer stores CWMP protocol exchanges in MongoDB and streams them in real-time via SSE (Server-Sent Events) to a frontend sequence diagram. This guide covers the infrastructure requirements, configuration, feature flags, and monitoring needed for deployment.
## MongoDB Setup
### Requirements
- **MongoDB version**: 4.4+ (tested with 5.0 and 6.0)
- **Replica set**: Not required. The system uses an in-process EventEmitter for real-time notifications instead of MongoDB Change Streams.
- **Authentication**: Standard MongoDB credentials (same as the existing adam-acs database)
### Collections
The feature creates two MongoDB collections:
| Collection | Purpose |
|-----------|---------|
| `cwmp_exchanges` | Stores structured CWMP exchange documents |
| `cwmp_exchange_counters` | Atomic counters for per-device sequence number generation |
### Indexes
Three indexes are created on `cwmp_exchanges`:
| Name | Fields | Options | Purpose |
|------|--------|---------|---------|
| `idx_deviceId_timestamp` | `{ deviceId: 1, timestamp: 1 }` | — | Efficient retrieval by device, ordered by time |
| `idx_deviceId_sequenceNumber` | `{ deviceId: 1, sequenceNumber: 1 }` | `unique: true` | Atomic sequence numbering, Last-Event-ID queries |
| `idx_createdAt_ttl` | `{ createdAt: 1 }` | `expireAfterSeconds: 86400` | Auto-remove exchanges older than 24 hours |
### Running the Migration
Indexes and collections are created via migration `011_create_cwmp_exchanges_collection.ts`:
```bash
# Run all pending migrations
node ace migration:run
# Verify migration applied
node ace migration:status
```
### Index Verification
On startup, the application checks that all expected indexes exist and logs warnings if any are missing. The health check endpoint also reports index status (see [Health Check](#health-check-endpoint) below).
To manually verify indexes in the MongoDB shell:
```javascript
db.cwmp_exchanges.getIndexes()
// Expected: _id, idx_deviceId_timestamp, idx_deviceId_sequenceNumber, idx_createdAt_ttl
```
### Storage Considerations
- **TTL**: Documents are automatically removed after 24 hours (configurable via `EXCHANGE_STORE_TTL_SECONDS`)
- **Message cap**: Maximum 5000 exchanges per device (configurable via `EXCHANGE_STORE_MAX_MESSAGES`)
- **Document size**: Each exchange document stores raw XML (max 2 MB per message)
- **Estimated storage**: For 100 traced devices with 5000 messages averaging 10 KB each → ~5 GB peak
---
## Environment Variables
### Feature Flags
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `FEATURE_EXCHANGE_PERSISTENCE` | Boolean | `true` | Enable/disable MongoDB persistence of exchanges |
| `FEATURE_SSE_ENABLED` | Boolean | `true` | Enable/disable the SSE streaming endpoint |
| `FEATURE_EXCHANGE_API_SOURCE` | String | `mongodb` | Data source for the Exchange API: `mongodb` or `file` |
### Exchange Store Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `EXCHANGE_STORE_MAX_MESSAGES` | Number | `5000` | Max exchanges stored per device |
| `EXCHANGE_STORE_TTL_SECONDS` | Number | `86400` | TTL in seconds for auto-removal (24h) |
| `EXCHANGE_STORE_READ_TIMEOUT_MS` | Number | `5000` | Query timeout before returning partial results |
### SSE Configuration
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `EXCHANGE_STORE_SSE_INACTIVITY_TIMEOUT_MS` | Number | `300000` | Close SSE connection after 5 min of inactivity |
| `EXCHANGE_STORE_SSE_ABSOLUTE_TIMEOUT_MS` | Number | `7200000` | Close SSE connection after 2 hours regardless |
| `EXCHANGE_STORE_SSE_MAX_CONNECTIONS_PER_DEVICE` | Number | `5` | Max concurrent SSE connections per device |
| `EXCHANGE_STORE_SSE_RETRY_INTERVAL_MS` | Number | `3000` | Retry interval sent to clients for reconnection |
### Retry Policy
| Variable | Type | Default | Description |
|----------|------|---------|-------------|
| `EXCHANGE_STORE_RETRY_MAX_ATTEMPTS` | Number | `3` | Max retries for critical message persistence |
| `EXCHANGE_STORE_RETRY_BASE_DELAY_MS` | Number | `1000` | Base delay for exponential backoff (1s, 2s, 4s) |
### Example .env Addition
```dotenv
# TR-069 Exchange Viewer - Feature Flags
FEATURE_EXCHANGE_PERSISTENCE=true
FEATURE_SSE_ENABLED=true
FEATURE_EXCHANGE_API_SOURCE=mongodb
# TR-069 Exchange Viewer - Storage
EXCHANGE_STORE_MAX_MESSAGES=5000
EXCHANGE_STORE_TTL_SECONDS=86400
EXCHANGE_STORE_READ_TIMEOUT_MS=5000
# TR-069 Exchange Viewer - SSE
EXCHANGE_STORE_SSE_INACTIVITY_TIMEOUT_MS=300000
EXCHANGE_STORE_SSE_ABSOLUTE_TIMEOUT_MS=7200000
EXCHANGE_STORE_SSE_MAX_CONNECTIONS_PER_DEVICE=5
EXCHANGE_STORE_SSE_RETRY_INTERVAL_MS=3000
# TR-069 Exchange Viewer - Retry Policy
EXCHANGE_STORE_RETRY_MAX_ATTEMPTS=3
EXCHANGE_STORE_RETRY_BASE_DELAY_MS=1000
```
---
## Feature Flags Usage and Toggle Procedures
The system uses three feature flags for gradual rollout and safe rollback. All flags can be toggled at runtime by changing environment variables and restarting the application.
### FEATURE_EXCHANGE_PERSISTENCE
Controls whether CWMP exchanges are written to MongoDB.
| Value | Behavior |
|-------|----------|
| `true` (default) | Exchanges are persisted to MongoDB for traced devices |
| `false` | No writes to MongoDB; CWMP sessions continue unaffected |
**When to disable:**
- MongoDB is experiencing high latency or unavailability
- Storage exhaustion on the MongoDB server
- You need to temporarily reduce load on the database
**Toggle procedure:**
```bash
# Disable persistence
export FEATURE_EXCHANGE_PERSISTENCE=false
# Restart the application
docker compose restart app
```
> Note: Disabling persistence means no new exchanges are stored. Existing exchanges remain queryable until TTL expires.
### FEATURE_SSE_ENABLED
Controls the real-time streaming endpoint.
| Value | Behavior |
|-------|----------|
| `true` (default) | SSE endpoint is available at `/api/v1/tr069-trace/:deviceId/exchanges/stream` |
| `false` | SSE endpoint returns 503; frontend falls back to REST polling every 5 seconds |
**When to disable:**
- High number of concurrent SSE connections causing resource exhaustion
- Network infrastructure (proxy/load balancer) not supporting long-lived connections
- Debugging connectivity issues
**Toggle procedure:**
```bash
# Disable SSE streaming
export FEATURE_SSE_ENABLED=false
# Restart the application
docker compose restart app
```
> Note: The frontend automatically falls back to REST polling (5-second interval) when SSE is unavailable. No user action required.
### FEATURE_EXCHANGE_API_SOURCE
Controls where the Exchange API reads data from.
| Value | Behavior |
|-------|----------|
| `mongodb` (default) | API reads from the `cwmp_exchanges` MongoDB collection |
| `file` | API reads from the legacy file-based trace system |
**When to switch to `file`:**
- MongoDB data is corrupted or inconsistent
- Rollback to the previous system during migration
- Comparing outputs between old and new systems
**Toggle procedure:**
```bash
# Fallback to file-based system
export FEATURE_EXCHANGE_API_SOURCE=file
# Restart the application
docker compose restart app
```
### Recommended Rollout Sequence
1. **Phase 1 — Dual-write** (default state):
- `FEATURE_EXCHANGE_PERSISTENCE=true`
- `FEATURE_SSE_ENABLED=true`
- `FEATURE_EXCHANGE_API_SOURCE=mongodb`
- Old file-based system continues writing in parallel (dual-write)
2. **Phase 2 — Validate**:
- Compare file-based and MongoDB exchange counts
- Monitor health check for discrepancies
- Verify SSE connections are stable
3. **Phase 3 — Full cutover**:
- Remove file-based writes from `CwmpLoggerMiddleware`
- `FEATURE_EXCHANGE_API_SOURCE=mongodb` (permanently)
### Emergency Rollback
To fully disable the exchange viewer feature without stopping the application:
```bash
export FEATURE_EXCHANGE_PERSISTENCE=false
export FEATURE_SSE_ENABLED=false
export FEATURE_EXCHANGE_API_SOURCE=file
docker compose restart app
```
This restores pre-feature behavior: no MongoDB writes, no SSE connections, and the API reads from the legacy file system.
---
## Health Check Endpoint
### Endpoint
```
GET /api/v1/health
```
The existing health check endpoint (`/api/v1/health`) reports on the overall application status including MongoDB connectivity and `cwmp_exchanges` index status.
### Response Format
```json
{
"status": "OK",
"version": "1.0.0",
"indexes": {
"cwmp_exchanges": {
"allPresent": true,
"indexes": [
{ "name": "idx_deviceId_timestamp", "present": true },
{ "name": "idx_deviceId_sequenceNumber", "present": true },
{ "name": "idx_createdAt_ttl", "present": true }
]
}
}
}
```
### Status Codes
| Status | Code | Meaning |
|--------|------|---------|
| OK | 200 | MongoDB connected, all indexes present |
| KO | 503 | MongoDB unreachable or critical failure |
### Monitoring Recommendations
| Metric | Threshold | Action |
|--------|-----------|--------|
| Health check status `KO` | Any occurrence | Investigate MongoDB connectivity |
| Missing indexes (`allPresent: false`) | Any occurrence | Re-run migration: `node ace migration:run` |
| MongoDB ping latency | > 100ms p95 | Check MongoDB server load |
| Active SSE connections | > 100 total | Consider scaling or limiting connections |
### Integration with Monitoring Tools
The health check endpoint can be used with standard monitoring tools:
```bash
# Simple liveness check (Docker HEALTHCHECK)
curl -f http://localhost:8080/api/v1/health || exit 1
# Kubernetes readiness probe
livenessProbe:
httpGet:
path: /api/v1/health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
# Nagios/Zabbix check
curl -s http://localhost:8080/api/v1/health | jq -e '.status == "OK"'
```
---
## API Endpoints
The feature exposes three endpoints under the `/api/v1` prefix:
| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/v1/tr069-trace/:deviceId/exchanges` | Retrieve exchanges (paginated) |
| DELETE | `/api/v1/tr069-trace/:deviceId/exchanges` | Purge all exchanges for a device |
| GET | `/api/v1/tr069-trace/:deviceId/exchanges/stream` | SSE endpoint for real-time streaming |
---
## Reverse Proxy Configuration
If running behind a reverse proxy (nginx, Traefik, etc.), configure it to support SSE:
### Nginx
```nginx
location /api/v1/tr069-trace/ {
proxy_pass http://app:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 7200s; # Match absolute timeout (2h)
chunked_transfer_encoding off;
}
```
### Traefik
```yaml
http:
middlewares:
sse-headers:
headers:
customResponseHeaders:
X-Accel-Buffering: "no"
services:
adam-acs:
loadBalancer:
responseForwarding:
flushInterval: "0ms"
```
---
## Docker Deployment
No additional Docker services are needed. The feature uses the existing MongoDB instance.
```yaml
# docker-compose.yml (relevant section)
services:
app:
environment:
- FEATURE_EXCHANGE_PERSISTENCE=true
- FEATURE_SSE_ENABLED=true
- FEATURE_EXCHANGE_API_SOURCE=mongodb
depends_on:
- mongodb
```
Ensure the MongoDB container has sufficient storage for exchange data (see [Storage Considerations](#storage-considerations)).
+592
View File
@@ -0,0 +1,592 @@
# 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 <deviceId>"
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/<DEVICE_ID>/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/<DEVICE_ID>/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 <commit-hash>
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<void> {
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(/<cwmp:(\w+)[\s/>]/)
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.
+396
View File
@@ -0,0 +1,396 @@
# Alerting Rules — TR-069 Exchange Viewer
## Overview
Ce document décrit les règles d'alerte configurées pour le système TR-069 Exchange Viewer. Chaque règle définit un seuil critique, les conditions de déclenchement, l'impact attendu et la procédure d'escalade associée.
Les métriques sont exposées via le endpoint `/api/v1/health/tr069-exchanges` et les logs structurés JSON.
---
## Alert 1: SSE Active Connections > 100
### Seuil
| Métrique | Condition | Sévérité |
|----------|-----------|----------|
| `sse.active_connections` | > 100 connexions simultanées | **Warning** |
| `sse.active_connections` | > 200 connexions simultanées | **Critical** |
### Description
Un nombre élevé de connexions SSE simultanées indique une charge serveur importante. Chaque connexion SSE maintient un socket HTTP ouvert et un listener EventEmitter en mémoire. Au-delà de 100 connexions, le serveur risque une dégradation de performance (latence accrue, consommation mémoire excessive).
### Causes possibles
- Pic d'activité : de nombreux opérateurs tracent des devices simultanément
- Fuite de connexions : les clients ne ferment pas correctement les connexions EventSource (navigation sans cleanup)
- Reconnection storm : perte réseau transitoire provoquant la reconnexion simultanée de tous les clients
- Bug client : boucle de reconnexion infinie (EventSource retry sans backoff)
### Impact
- Augmentation de la consommation mémoire du processus Node.js
- Latence accrue sur les push SSE (chaque nouveau message est dispatché à N listeners)
- Risque d'épuisement des file descriptors (limite OS)
- Dégradation possible du traitement CWMP si le thread principal est saturé
### Détection
```json
// GET /api/v1/health/tr069-exchanges
{
"sse": {
"active_connections": 142,
"reconnections_1h": 56
}
}
```
Logs structurés :
```json
{ "level": "warn", "msg": "SSE connection count high", "active_connections": 142, "threshold": 100 }
```
### Procédure de résolution
1. **Vérifier** le nombre de connexions actives via le health check endpoint
2. **Identifier** les devices avec le plus de connexions (logs : `sse.connect` events par deviceId)
3. **Vérifier** si des connexions zombies existent (connexions ouvertes sans activité > 5 minutes)
4. **Appliquer** les timeouts serveur : les connexions inactives sont fermées après 5 min, les absolues après 2h
5. **Si le problème persiste** : redémarrer le service pour forcer la fermeture de toutes les connexions SSE
### Escalade
| Durée | Action |
|-------|--------|
| 0-5 min | Monitoring automatique, pas d'action |
| 5-15 min (Warning) | Vérifier les logs, identifier la cause |
| > 15 min (Warning) | Notifier l'équipe Dev via Slack #adam-acs-alerts |
| Immédiat (Critical > 200) | Notifier l'équipe Dev + Ops, envisager un restart |
| > 30 min (Critical) | Activer le feature flag `FEATURE_SSE_ENABLED=false` (fallback REST polling) |
---
## Alert 2: MongoDB Write Latency p95 > 100ms
### Seuil
| Métrique | Condition | Sévérité |
|----------|-----------|----------|
| `mongodb.write_latency_p95` | > 100ms | **Warning** |
| `mongodb.write_latency_p95` | > 500ms | **Critical** |
| `mongodb.write_latency_p99` | > 1000ms | **Critical** |
### Description
La latence d'écriture MongoDB au percentile 95 reflète la performance de persistance des échanges CWMP. Au-delà de 100ms (p95), les écritures commencent à s'accumuler, ce qui peut provoquer un retard dans la diffusion SSE et une pression mémoire sur le processus.
### Causes possibles
- Charge MongoDB élevée (autres opérations concurrentes, compaction, journalisation)
- Index manquant ou dégradé sur la collection `cwmp_exchanges`
- Disque I/O saturé sur le serveur MongoDB
- Collection `cwmp_exchanges` trop volumineuse (TTL cleanup en retard)
- Locks MongoDB : opérations bulk ou migrations en cours
### Impact
- Retard dans la diffusion SSE des nouveaux échanges (latence end-to-end augmentée)
- Activation des retries pour les messages critiques (backoff 1s, 2s, 4s)
- En cas de timeout persistant : perte potentielle de messages non-critiques (Empty, Unknown)
- Le traitement CWMP n'est jamais impacté (fire-and-forget)
### Détection
Logs structurés :
```json
{ "level": "warn", "msg": "MongoDB write slow", "collection": "cwmp_exchanges", "duration_ms": 145, "deviceId": "abc123", "cwmpMethod": "Inform" }
```
Métriques agrégées (via logs ou Prometheus) :
```
mongodb_write_latency_ms{collection="cwmp_exchanges", quantile="0.95"} 145
mongodb_write_latency_ms{collection="cwmp_exchanges", quantile="0.99"} 320
```
### Procédure de résolution
1. **Vérifier** l'état de MongoDB : `db.serverStatus()`, `db.currentOp()`
2. **Vérifier** les index de `cwmp_exchanges` :
```bash
mongosh --eval "db.cwmp_exchanges.getIndexes()"
```
3. **Vérifier** la taille de la collection et le nombre de documents :
```bash
mongosh --eval "db.cwmp_exchanges.stats()"
```
4. **Vérifier** le TTL cleanup (voir Alert 4)
5. **Si les index sont manquants** : recréer via la migration ou manuellement :
```bash
mongosh --eval "db.cwmp_exchanges.createIndex({deviceId:1, timestamp:1})"
mongosh --eval "db.cwmp_exchanges.createIndex({deviceId:1, sequenceNumber:1}, {unique:true})"
mongosh --eval "db.cwmp_exchanges.createIndex({createdAt:1}, {expireAfterSeconds:86400})"
```
6. **Si la charge est transitoire** : attendre la stabilisation (compaction terminée, etc.)
### Escalade
| Durée | Action |
|-------|--------|
| 0-5 min | Monitoring automatique |
| 5-15 min (Warning) | Vérifier les index et la charge MongoDB |
| > 15 min (Warning) | Notifier l'équipe Dev |
| Immédiat (Critical > 500ms) | Notifier Dev + DBA |
| > 30 min (Critical) | Envisager `FEATURE_EXCHANGE_PERSISTENCE=false` pour délester MongoDB |
---
## Alert 3: Message Loss Rate > 0.1%
### Seuil
| Métrique | Condition | Sévérité |
|----------|-----------|----------|
| `exchange.loss_rate` | > 0.1% (1 message sur 1000) | **Critical** |
| `exchange.loss_rate` | > 1.0% (1 message sur 100) | **Urgent** |
### Description
Le taux de perte de messages représente le pourcentage de messages CWMP capturés par le middleware mais non persistés en MongoDB (après épuisement des retries pour les critiques, ou discard immédiat pour les non-critiques). Un taux > 0.1% indique des défaillances critiques du backend de persistance.
### Causes possibles
- Défaillance MongoDB : connexion perdue, timeout, disque plein
- Retry exhaustion : 3 retries échoués consécutivement pour les messages critiques
- Erreur dans le service `ExchangeStoreService` (bug, exception non catchée)
- Saturation de la collection (cap à 5000 messages avec suppression simultanée)
- Problème réseau entre l'application et MongoDB
### Impact
- Trous dans l'historique des échanges visibles dans le Sequence Viewer
- Les opérateurs voient des séquences incomplètes, rendant le debug difficile
- Les messages critiques (Inform, SetParameterValues) perdus représentent une perte d'observabilité sur les actions CPE
### Détection
Logs structurés :
```json
{ "level": "error", "msg": "Exchange persistence failed after retries", "deviceId": "abc123", "cwmpMethod": "Inform", "retries": 3, "error": "MongoServerError: connection timeout" }
{ "level": "warn", "msg": "Non-critical exchange discarded", "deviceId": "abc123", "cwmpMethod": "Empty", "error": "MongoServerError: connection timeout" }
```
Calcul du taux de perte :
```
loss_rate = (messages_discarded / messages_captured) * 100
```
### Procédure de résolution
1. **Vérifier** la connectivité MongoDB : health check endpoint, `mongosh` ping
2. **Vérifier** les logs d'erreur pour identifier le pattern de défaillance
3. **Vérifier** l'espace disque disponible sur le serveur MongoDB
4. **Vérifier** que le TTL cleanup fonctionne (pas d'accumulation illimitée)
5. **Si MongoDB est injoignable** : vérifier le réseau, le service mongod, les limites de connexions
6. **Si MongoDB est surchargé** : identifier les opérations concurrentes, envisager un throttle
### Escalade
| Durée | Action |
|-------|--------|
| Immédiat (> 0.1%) | **Alerte critique** — Notifier l'équipe Dev immédiatement |
| 0-5 min | Diagnostic rapide : MongoDB up? Index OK? Disk space? |
| 5-15 min | Notifier Dev + Ops, vérifier infrastructure MongoDB |
| > 15 min | Escalade management, envisager rollback vers fichiers |
| Immédiat (> 1.0%) | **Alerte urgente** — Activer `FEATURE_EXCHANGE_PERSISTENCE=false`, diagnostic prioritaire |
---
## Alert 4: TTL Cleanup Lag > 1 Hour
### Seuil
| Métrique | Condition | Sévérité |
|----------|-----------|----------|
| `exchange.ttl_cleanup_lag` | > 1 heure | **Warning** |
| `exchange.ttl_cleanup_lag` | > 6 heures | **Critical** |
### Description
L'index TTL sur le champ `createdAt` (expire après 86400 secondes = 24h) est géré par un thread interne MongoDB qui s'exécute toutes les 60 secondes. Si le cleanup prend du retard, des documents expirés s'accumulent, augmentant la taille de la collection et dégradant les performances de requête.
Le "lag" est mesuré comme la différence entre le plus ancien document présent dans la collection et son heure d'expiration théorique (createdAt + 24h).
### Causes possibles
- MongoDB TTL monitor surchargé (collection très volumineuse, beaucoup de documents à supprimer)
- Index TTL supprimé ou corrompu
- MongoDB en mode de maintenance (locks, compaction)
- Charge élevée sur le serveur empêchant le thread TTL de s'exécuter
- Bug dans la configuration du TTL (mauvais champ, mauvaise valeur `expireAfterSeconds`)
### Impact
- Croissance non contrôlée de la collection `cwmp_exchanges`
- Dégradation progressive des performances de lecture (scans plus larges)
- Consommation disque accrue
- Potentiel dépassement des limites de stockage
### Détection
Vérification manuelle du lag :
```bash
# Trouver le document le plus ancien
mongosh --eval "
const oldest = db.cwmp_exchanges.find().sort({createdAt: 1}).limit(1).toArray()[0];
if (oldest) {
const expectedExpiry = new Date(oldest.createdAt.getTime() + 86400000);
const lag = Date.now() - expectedExpiry.getTime();
print('Oldest createdAt:', oldest.createdAt);
print('Expected expiry:', expectedExpiry);
print('Lag (ms):', Math.max(0, lag));
print('Lag (hours):', Math.max(0, lag / 3600000).toFixed(2));
} else {
print('Collection empty');
}
"
```
Vérification de l'index TTL :
```bash
mongosh --eval "
const indexes = db.cwmp_exchanges.getIndexes();
const ttlIndex = indexes.find(i => i.expireAfterSeconds !== undefined);
if (ttlIndex) {
print('TTL index found:', JSON.stringify(ttlIndex));
} else {
print('WARNING: No TTL index found!');
}
"
```
### Procédure de résolution
1. **Vérifier** que l'index TTL existe et est correctement configuré :
```bash
mongosh --eval "db.cwmp_exchanges.getIndexes()"
```
L'index attendu : `{ "createdAt": 1 }` avec `expireAfterSeconds: 86400`
2. **Si l'index TTL est manquant** — le recréer :
```bash
mongosh --eval "db.cwmp_exchanges.createIndex({createdAt: 1}, {expireAfterSeconds: 86400})"
```
3. **Si l'index existe mais le cleanup est en retard** :
- Vérifier la charge MongoDB : `db.serverStatus().metrics.ttl`
- Vérifier si un grand nombre de documents doivent être supprimés d'un coup
- Envisager un nettoyage manuel des plus anciens documents :
```bash
mongosh --eval "
const cutoff = new Date(Date.now() - 86400000);
const result = db.cwmp_exchanges.deleteMany({createdAt: {$lt: cutoff}});
print('Deleted:', result.deletedCount);
"
```
4. **Si MongoDB est en maintenance** : attendre la fin de l'opération, surveiller la reprise du TTL
### Escalade
| Durée | Action |
|-------|--------|
| 0-1h | Monitoring automatique (TTL thread cycle = 60s, léger retard normal) |
| 1-3h (Warning) | Vérifier l'existence de l'index TTL, vérifier la charge MongoDB |
| 3-6h (Warning) | Notifier l'équipe Dev, envisager un nettoyage manuel |
| > 6h (Critical) | Notifier Dev + DBA, nettoyage manuel obligatoire, diagnostic approfondi |
| > 24h | Escalade management — la collection grandit sans limite |
---
## Résumé des alertes
| # | Alerte | Seuil Warning | Seuil Critical | Métrique |
|---|--------|---------------|----------------|----------|
| 1 | SSE Active Connections | > 100 | > 200 | `sse.active_connections` |
| 2 | MongoDB Write Latency p95 | > 100ms | > 500ms | `mongodb.write_latency_p95` |
| 3 | Message Loss Rate | > 0.1% | > 1.0% | `exchange.loss_rate` |
| 4 | TTL Cleanup Lag | > 1 heure | > 6 heures | `exchange.ttl_cleanup_lag` |
---
## Configuration des alertes
### Variables d'environnement
| Variable | Défaut | Description |
|----------|--------|-------------|
| `ALERT_SSE_CONNECTIONS_WARNING` | `100` | Seuil warning connexions SSE |
| `ALERT_SSE_CONNECTIONS_CRITICAL` | `200` | Seuil critical connexions SSE |
| `ALERT_MONGO_LATENCY_P95_WARNING_MS` | `100` | Seuil warning latence MongoDB p95 (ms) |
| `ALERT_MONGO_LATENCY_P95_CRITICAL_MS` | `500` | Seuil critical latence MongoDB p95 (ms) |
| `ALERT_LOSS_RATE_CRITICAL_PCT` | `0.1` | Seuil critical taux de perte (%) |
| `ALERT_TTL_LAG_WARNING_HOURS` | `1` | Seuil warning lag TTL (heures) |
| `ALERT_TTL_LAG_CRITICAL_HOURS` | `6` | Seuil critical lag TTL (heures) |
### Intégration avec les systèmes d'alerte
Les alertes sont déclenchées via les logs structurés JSON, compatibles avec :
- **ELK Stack** : index pattern `adam-acs-*`, filtres sur `level: "error"` ou `level: "warn"` + tags spécifiques
- **Datadog** : log-based monitors avec conditions sur les champs JSON
- **Prometheus/Grafana** : si le endpoint `/metrics` est exposé, utiliser les alerting rules Prometheus
Exemple de règle Prometheus :
```yaml
groups:
- name: tr069-exchange-viewer
rules:
- alert: HighSSEConnections
expr: adam_acs_sse_active_connections > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High SSE connection count"
description: "{{ $value }} SSE connections active (threshold: 100)"
- alert: MongoWriteLatencyHigh
expr: histogram_quantile(0.95, adam_acs_mongodb_write_duration_seconds_bucket{collection="cwmp_exchanges"}) > 0.1
for: 5m
labels:
severity: warning
annotations:
summary: "MongoDB write latency p95 > 100ms"
description: "p95 latency: {{ $value }}s for cwmp_exchanges writes"
- alert: ExchangeMessageLoss
expr: rate(adam_acs_exchange_messages_lost_total[5m]) / rate(adam_acs_exchange_messages_captured_total[5m]) > 0.001
for: 1m
labels:
severity: critical
annotations:
summary: "Message loss rate exceeds 0.1%"
description: "{{ $value | humanizePercentage }} messages lost in last 5 minutes"
- alert: TTLCleanupLag
expr: adam_acs_exchange_ttl_lag_seconds > 3600
for: 10m
labels:
severity: warning
annotations:
summary: "TTL cleanup lag exceeds 1 hour"
description: "Oldest expired document is {{ $value | humanizeDuration }} past its TTL"
```
---
## Contacts d'escalade
| Niveau | Contact | Canal |
|--------|---------|-------|
| L1 — Warning | Équipe Dev adam-acs | Slack #adam-acs-alerts |
| L2 — Critical | Lead Dev + Ops | Slack #adam-acs-alerts + mention @oncall |
| L3 — Urgent/Prolongé | Management technique | Escalade téléphonique |
| DBA (MongoDB) | Équipe DBA | Slack #dba-support |
+180
View File
@@ -0,0 +1,180 @@
---
name: Precision Protocol System
colors:
surface: '#f8f9ff'
surface-dim: '#cbdbf5'
surface-bright: '#f8f9ff'
surface-container-lowest: '#ffffff'
surface-container-low: '#eff4ff'
surface-container: '#e5eeff'
surface-container-high: '#dce9ff'
surface-container-highest: '#d3e4fe'
on-surface: '#0b1c30'
on-surface-variant: '#45464d'
inverse-surface: '#213145'
inverse-on-surface: '#eaf1ff'
outline: '#76777d'
outline-variant: '#c6c6cd'
surface-tint: '#565e74'
primary: '#000000'
on-primary: '#ffffff'
primary-container: '#131b2e'
on-primary-container: '#7c839b'
inverse-primary: '#bec6e0'
secondary: '#006a61'
on-secondary: '#ffffff'
secondary-container: '#86f2e4'
on-secondary-container: '#006f66'
tertiary: '#000000'
on-tertiary: '#ffffff'
tertiary-container: '#0f0069'
on-tertiary-container: '#7671ff'
error: '#ba1a1a'
on-error: '#ffffff'
error-container: '#ffdad6'
on-error-container: '#93000a'
primary-fixed: '#dae2fd'
primary-fixed-dim: '#bec6e0'
on-primary-fixed: '#131b2e'
on-primary-fixed-variant: '#3f465c'
secondary-fixed: '#89f5e7'
secondary-fixed-dim: '#6bd8cb'
on-secondary-fixed: '#00201d'
on-secondary-fixed-variant: '#005049'
tertiary-fixed: '#e2dfff'
tertiary-fixed-dim: '#c3c0ff'
on-tertiary-fixed: '#0f0069'
on-tertiary-fixed-variant: '#3323cc'
background: '#f8f9ff'
on-background: '#0b1c30'
surface-variant: '#d3e4fe'
typography:
headline-lg:
fontFamily: Inter
fontSize: 30px
fontWeight: '600'
lineHeight: 38px
letterSpacing: -0.02em
headline-md:
fontFamily: Inter
fontSize: 24px
fontWeight: '600'
lineHeight: 32px
letterSpacing: -0.01em
headline-sm:
fontFamily: Inter
fontSize: 18px
fontWeight: '600'
lineHeight: 26px
body-lg:
fontFamily: Inter
fontSize: 16px
fontWeight: '400'
lineHeight: 24px
body-md:
fontFamily: Inter
fontSize: 14px
fontWeight: '400'
lineHeight: 20px
body-sm:
fontFamily: Inter
fontSize: 13px
fontWeight: '400'
lineHeight: 18px
label-md:
fontFamily: Inter
fontSize: 12px
fontWeight: '500'
lineHeight: 16px
letterSpacing: 0.05em
mono-md:
fontFamily: JetBrains Mono
fontSize: 13px
fontWeight: '400'
lineHeight: 20px
mono-sm:
fontFamily: JetBrains Mono
fontSize: 11px
fontWeight: '400'
lineHeight: 16px
rounded:
sm: 0.125rem
DEFAULT: 0.25rem
md: 0.375rem
lg: 0.5rem
xl: 0.75rem
full: 9999px
spacing:
unit: 4px
container-padding: 24px
gutter: 16px
sidebar-width: 260px
stack-sm: 8px
stack-md: 16px
stack-lg: 32px
---
## Brand & Style
This design system is engineered for high-performance network analysis and protocol monitoring. The brand personality is rooted in **precision, neutrality, and clarity**. It avoids decorative flourishes to prioritize the rapid interpretation of complex data structures.
The visual style is a blend of **Minimalism** and **Modern Corporate** aesthetics. It utilizes a spacious layout to reduce the cognitive load inherent in dense data environments. The interface conveys reliability through a systematic application of subtle borders and high-contrast typography, ensuring that critical network events are immediately visible.
## Colors
The palette is anchored in **Slate and Zinc** neutrals to provide a professional, calm environment for long-term monitoring.
* **Primary (Slate 950):** Used for core navigation, high-level headers, and text to ensure maximum contrast.
* **Neutral (Slate/Zinc):** Used for structural elements, secondary text, and inactive states.
* **Flow Accents:** Semantic colors are reserved for directional data. **Teal (ACS -> CPE)** and **Indigo (CPE -> ACS)** allow engineers to visually parse the direction of TR-069 traffic at a glance without reading text labels.
* **Success/Error:** Traditional Green and Red are used sparingly for status indicators and connection states.
## Typography
This design system utilizes a dual-font strategy. **Inter** is the workhorse for the UI, chosen for its exceptional legibility in small sizes and high-density layouts. **JetBrains Mono** is utilized exclusively for technical data, including XML, JSON payloads, and packet hex dumps.
Headlines use tighter letter spacing for a more authoritative look, while labels utilize uppercase styling and increased tracking for clear categorization in sidebars and table headers. Monospaced text is rendered with a slightly smaller font size than body text to accommodate long lines of code without excessive wrapping.
## Layout & Spacing
The layout follows a **Hybrid Grid** model. Navigation and property inspectors use fixed-width sidebars, while the main content area—typically containing packet logs or traffic graphs—uses a **Fluid Grid** to maximize horizontal real estate.
### Breakpoints
* **Desktop (1280px+):** 12-column fluid grid, 24px margins.
* **Tablet (768px - 1279px):** 8-column fluid grid, 16px margins. Sidebar collapses to icons.
* **Mobile (<767px):** 4-column fluid grid, 12px margins. Horizontal scrolling enabled for data tables.
Spacing is based on a **4px base unit**. Component internals use 8px or 12px increments to maintain a compact, "instrumental" feel without appearing cluttered.
## Elevation & Depth
To maintain a technical and minimalist aesthetic, this design system avoids heavy shadows. Instead, it relies on **Tonal Layers** and **Low-Contrast Outlines**.
* **Background (Level 0):** Slate-50. The base canvas.
* **Surface (Level 1):** White. Primary cards and panels.
* **Active/Overlay (Level 2):** White with a subtle 1px border in Slate-200 and a very soft, high-diffusion shadow (4px blur, 2% opacity) to indicate temporary elements like tooltips or dropdowns.
Visual separation between adjacent panels is achieved via 1px solid borders in Slate-200 rather than elevation, ensuring the UI remains crisp and "flat."
## Shapes
The shape language is **Soft (Level 1)**. A 4px radius is applied to buttons, input fields, and panels. This subtle rounding provides a modern touch without sacrificing the professional, rigid structure required for a technical tool.
Status indicators (pills) use a slightly larger 12px radius to distinguish them from interactive buttons, while data visualization bars use sharp 2px corners for maximum precision.
## Components
### Buttons
Primary buttons use the Slate-950 background with white text. Ghost buttons (transparent background with Slate-200 borders) are preferred for secondary actions to keep the focus on the data.
### Packet Log Entries
Logs must use a zebra-stripe pattern (White / Slate-50) for horizontal tracking. The "Flow Direction" is indicated by a 4px vertical accent bar on the far left of the row (Teal or Indigo).
### Input Fields
Fields use a 1px Slate-200 border. On focus, the border transitions to Slate-900. Labels are always positioned above the input in the `label-md` style for clarity.
### Data Displays (Code Blocks)
XML/JSON payloads are wrapped in containers with a Slate-50 background and a 1px border. They use `mono-md` typography with syntax highlighting that mirrors the flow colors (Teal for tags, Indigo for values).
### Chips & Badges
Used for protocol methods (e.g., `Inform`, `GetParameterValues`). These are small, low-contrast components (Slate-100 background) to avoid competing with the flow direction accents.
+367
View File
@@ -0,0 +1,367 @@
<!DOCTYPE html>
<html class="light" lang="en"><head>
<meta charset="utf-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>TR-069 Protocol Analyzer</title>
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&amp;family=JetBrains+Mono:wght@400;500&amp;family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&amp;display=swap" rel="stylesheet"/>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&amp;display=swap" rel="stylesheet"/>
<style>
.material-symbols-outlined {
font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 24;
vertical-align: middle;
}
/* Custom scrollbar for technical look */
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: #e2e8f0; border-radius: 10px; }
::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
.lifeline-gradient {
background: linear-gradient(to bottom, #000 0%, #000 5%, transparent 5%, transparent 100%);
background-size: 1px 20px;
}
</style>
<script id="tailwind-config">
tailwind.config = {
darkMode: "class",
theme: {
extend: {
"colors": {
"tertiary-fixed-dim": "#c3c0ff",
"surface-container-high": "#dce9ff",
"surface-container-lowest": "#ffffff",
"on-background": "#0b1c30",
"on-tertiary": "#ffffff",
"error": "#ba1a1a",
"on-primary-fixed": "#131b2e",
"surface-tint": "#565e74",
"secondary-fixed-dim": "#6bd8cb",
"secondary-container": "#86f2e4",
"surface-container": "#e5eeff",
"on-primary-fixed-variant": "#3f465c",
"outline-variant": "#c6c6cd",
"on-secondary-fixed": "#00201d",
"on-surface-variant": "#45464d",
"surface-bright": "#f8f9ff",
"on-primary": "#ffffff",
"tertiary": "#000000",
"primary-fixed-dim": "#bec6e0",
"on-tertiary-container": "#7671ff",
"surface-container-low": "#eff4ff",
"surface-container-highest": "#d3e4fe",
"surface-variant": "#d3e4fe",
"inverse-on-surface": "#eaf1ff",
"secondary": "#006a61",
"primary-fixed": "#dae2fd",
"primary": "#000000",
"tertiary-container": "#0f0069",
"on-error-container": "#93000a",
"on-tertiary-fixed-variant": "#3323cc",
"on-surface": "#0b1c30",
"on-error": "#ffffff",
"on-secondary": "#ffffff",
"surface-dim": "#cbdbf5",
"error-container": "#ffdad6",
"tertiary-fixed": "#e2dfff",
"secondary-fixed": "#89f5e7",
"background": "#f8f9ff",
"primary-container": "#131b2e",
"on-secondary-fixed-variant": "#005049",
"on-tertiary-fixed": "#0f0069",
"on-secondary-container": "#006f66",
"surface": "#f8f9ff",
"inverse-surface": "#213145",
"inverse-primary": "#bec6e0",
"outline": "#76777d"
},
"borderRadius": {
"DEFAULT": "0.125rem",
"lg": "0.25rem",
"xl": "0.5rem",
"full": "0.75rem"
},
"spacing": {
"gutter": "16px",
"stack-md": "16px",
"stack-sm": "8px",
"unit": "4px",
"stack-lg": "32px",
"sidebar-width": "260px",
"container-padding": "24px"
},
"fontFamily": {
"mono-sm": ["JetBrains Mono"],
"headline-md": ["Inter"],
"mono-md": ["JetBrains Mono"],
"headline-lg": ["Inter"],
"body-md": ["Inter"],
"body-sm": ["Inter"],
"body-lg": ["Inter"],
"label-md": ["Inter"],
"headline-sm": ["Inter"]
},
"fontSize": {
"mono-sm": ["11px", { "lineHeight": "16px", "fontWeight": "400" }],
"headline-md": ["24px", { "lineHeight": "32px", "letterSpacing": "-0.01em", "fontWeight": "600" }],
"mono-md": ["13px", { "lineHeight": "20px", "fontWeight": "400" }],
"headline-lg": ["30px", { "lineHeight": "38px", "letterSpacing": "-0.02em", "fontWeight": "600" }],
"body-md": ["14px", { "lineHeight": "20px", "fontWeight": "400" }],
"body-sm": ["13px", { "lineHeight": "18px", "fontWeight": "400" }],
"body-lg": ["16px", { "lineHeight": "24px", "fontWeight": "400" }],
"label-md": ["12px", { "lineHeight": "16px", "letterSpacing": "0.05em", "fontWeight": "500" }],
"headline-sm": ["18px", { "lineHeight": "26px", "fontWeight": "600" }]
}
},
},
}
</script>
</head>
<body class="bg-background text-on-background font-body-md overflow-hidden">
<!-- Top Navigation Shell -->
<header class="bg-surface text-on-surface font-headline-sm text-headline-sm docked full-width top-0 border-b border-outline-variant flex justify-between items-center px-gutter w-full h-16 z-50">
<div class="flex items-center gap-stack-lg">
<span class="font-headline-sm text-headline-sm font-bold text-on-surface">TR-069 Protocol Analyzer</span>
<nav class="hidden md:flex items-center h-16 gap-6">
<a class="text-on-surface-variant font-label-md text-label-md h-full flex items-center px-2 hover:bg-surface-container transition-colors duration-200" href="#">Dashboard</a>
<a class="text-on-surface-variant font-label-md text-label-md h-full flex items-center px-2 hover:bg-surface-container transition-colors duration-200" href="#">Live Stream</a>
<a class="text-primary font-bold border-b-2 border-primary h-full flex items-center px-2" href="#">Analysis</a>
<a class="text-on-surface-variant font-label-md text-label-md h-full flex items-center px-2 hover:bg-surface-container transition-colors duration-200" href="#">Config</a>
</nav>
</div>
<div class="flex items-center gap-4">
<div class="relative group">
<span class="material-symbols-outlined cursor-pointer p-2 hover:bg-surface-container-high rounded-full transition-colors duration-150" data-icon="filter_list">filter_list</span>
</div>
<span class="material-symbols-outlined cursor-pointer p-2 hover:bg-surface-container-high rounded-full transition-colors duration-150" data-icon="settings">settings</span>
<span class="material-symbols-outlined cursor-pointer p-2 hover:bg-surface-container-high rounded-full transition-colors duration-150" data-icon="help">help</span>
</div>
</header>
<div class="flex h-[calc(100vh-64px)] overflow-hidden">
<!-- Side Navigation Shell -->
<aside class="bg-surface-container-low text-primary font-label-md text-label-md docked left-0 h-full w-sidebar-width border-r border-outline-variant flex flex-col sticky top-0">
<div class="p-6">
<div class="flex items-center gap-3 mb-8">
<div class="w-10 h-10 rounded-full bg-primary-container flex items-center justify-center">
<span class="material-symbols-outlined text-surface" data-icon="router">router</span>
</div>
<div>
<div class="font-headline-sm text-[14px] font-bold text-primary">Network Monitor</div>
<div class="text-[11px] text-on-surface-variant">Active Nodes: 1,240</div>
</div>
</div>
<button class="w-full bg-primary text-on-primary py-2.5 rounded-lg font-label-md text-label-md mb-8 hover:opacity-90 transition-opacity">
New Capture
</button>
<nav class="space-y-1">
<a class="flex items-center gap-3 px-4 py-3 bg-secondary-container text-on-secondary-container font-semibold rounded-lg transition-all duration-150" href="#">
<span class="material-symbols-outlined" data-icon="history">history</span>
<span>Sessions</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg transition-all duration-150" href="#">
<span class="material-symbols-outlined" data-icon="router">router</span>
<span>Devices</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg transition-all duration-150" href="#">
<span class="material-symbols-outlined" data-icon="analytics">analytics</span>
<span>Diagnostics</span>
</a>
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg transition-all duration-150" href="#">
<span class="material-symbols-outlined" data-icon="list_alt">list_alt</span>
<span>Logs</span>
</a>
</nav>
</div>
<div class="mt-auto p-6 border-t border-outline-variant">
<a class="flex items-center gap-3 px-4 py-3 text-on-surface-variant hover:bg-surface-container-high rounded-lg transition-all duration-150" href="#">
<span class="material-symbols-outlined" data-icon="logout">logout</span>
<span>Logout</span>
</a>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 flex flex-col overflow-hidden bg-white">
<!-- Session Status Header -->
<div class="flex items-center justify-between px-8 py-4 bg-surface-container-low border-b border-outline-variant">
<div class="flex gap-8">
<div>
<span class="text-label-md font-label-md text-on-surface-variant block uppercase tracking-wider mb-1">Device Serial</span>
<span class="text-body-md font-mono-md font-medium text-on-surface">TR069-NODE-550291-B</span>
</div>
<div class="h-10 w-px bg-outline-variant"></div>
<div>
<span class="text-label-md font-label-md text-on-surface-variant block uppercase tracking-wider mb-1">IP Address</span>
<span class="text-body-md font-mono-md font-medium text-on-surface">192.168.100.42</span>
</div>
<div class="h-10 w-px bg-outline-variant"></div>
<div>
<span class="text-label-md font-label-md text-on-surface-variant block uppercase tracking-wider mb-1">Connection Status</span>
<div class="flex items-center gap-2">
<span class="w-2 h-2 rounded-full bg-secondary"></span>
<span class="text-body-md font-medium text-on-surface">CWMP Session Established</span>
</div>
</div>
</div>
<div class="flex items-center gap-2">
<span class="px-3 py-1 bg-surface-container text-on-surface-variant rounded-full text-label-md font-label-md border border-outline-variant">Session ID: 88241</span>
<button class="material-symbols-outlined text-on-surface-variant p-1" data-icon="more_vert">more_vert</button>
</div>
</div>
<!-- Sequence Diagram Content -->
<div class="flex-1 flex overflow-hidden">
<div class="flex-1 overflow-y-auto relative bg-slate-50 p-12">
<!-- Lifeline Headers -->
<div class="sticky top-0 z-10 flex justify-around mb-12 pointer-events-none">
<div class="px-10 py-3 bg-primary text-on-primary rounded font-bold shadow-sm border border-primary min-w-[120px] text-center">CPE</div>
<div class="px-10 py-3 bg-primary text-on-primary rounded font-bold shadow-sm border border-primary min-w-[120px] text-center">ACS</div>
</div>
<!-- Vertical Lifelines -->
<div class="absolute top-0 bottom-0 left-1/4 w-px bg-outline pointer-events-none opacity-20"></div>
<div class="absolute top-0 bottom-0 right-1/4 w-px bg-outline pointer-events-none opacity-20"></div>
<!-- Messages -->
<div class="relative space-y-10 pb-20">
<!-- Row: Inform -->
<div class="relative group cursor-pointer" onclick="selectMessage('inform')">
<div class="absolute left-[25%] right-[25%] flex items-center">
<div class="flex-1 h-px bg-secondary relative">
<div class="absolute right-0 -top-[5px] border-y-[6px] border-y-transparent border-l-[10px] border-l-secondary"></div>
</div>
<div class="absolute left-1/2 -translate-x-1/2 -top-6 text-center">
<span class="px-2 py-0.5 bg-secondary-container text-on-secondary-container text-label-md font-label-md rounded border border-secondary/20">Inform</span>
<span class="block text-[10px] font-mono-sm text-on-surface-variant mt-1">10:05:22.450</span>
</div>
</div>
<div class="h-8 invisible">spacer</div>
</div>
<!-- Row: InformResponse -->
<div class="relative group cursor-pointer" onclick="selectMessage('inform-response')">
<div class="absolute left-[25%] right-[25%] flex items-center">
<div class="flex-1 h-px bg-tertiary-container relative">
<div class="absolute left-0 -top-[5px] border-y-[6px] border-y-transparent border-r-[10px] border-r-tertiary-container"></div>
</div>
<div class="absolute left-1/2 -translate-x-1/2 -top-6 text-center">
<span class="px-2 py-0.5 bg-surface-container-high text-on-tertiary-fixed-variant text-label-md font-label-md rounded border border-outline">InformResponse</span>
<span class="block text-[10px] font-mono-sm text-on-surface-variant mt-1">10:05:22.612</span>
</div>
</div>
<div class="h-8 invisible">spacer</div>
</div>
<!-- Selected Row: GetParameterValues -->
<div class="relative group cursor-pointer" onclick="selectMessage('get-params')">
<!-- Selection Highlight -->
<div class="absolute -left-12 -right-12 h-16 bg-surface-container border-y border-outline-variant -mt-4 opacity-50 z-0"></div>
<div class="absolute left-[25%] right-[25%] flex items-center z-10">
<div class="flex-1 h-[2px] bg-primary relative">
<div class="absolute left-0 -top-[6px] border-y-[7px] border-y-transparent border-r-[12px] border-r-primary"></div>
</div>
<div class="absolute left-1/2 -translate-x-1/2 -top-6 text-center">
<span class="px-2 py-0.5 bg-primary text-on-primary text-label-md font-label-md rounded shadow-sm">GetParameterValues</span>
<span class="block text-[10px] font-mono-sm font-bold text-primary mt-1">10:05:23.002</span>
</div>
</div>
<div class="h-8 invisible">spacer</div>
</div>
<!-- Row: GetParameterValuesResponse -->
<div class="relative group cursor-pointer" onclick="selectMessage('get-params-response')">
<div class="absolute left-[25%] right-[25%] flex items-center">
<div class="flex-1 h-px bg-secondary relative">
<div class="absolute right-0 -top-[5px] border-y-[6px] border-y-transparent border-l-[10px] border-l-secondary"></div>
</div>
<div class="absolute left-1/2 -translate-x-1/2 -top-6 text-center">
<span class="px-2 py-0.5 bg-secondary-container text-on-secondary-container text-label-md font-label-md rounded border border-secondary/20">GetParameterValuesResponse</span>
<span class="block text-[10px] font-mono-sm text-on-surface-variant mt-1">10:05:23.189</span>
</div>
</div>
<div class="h-8 invisible">spacer</div>
</div>
<!-- Row: SetParameterValues -->
<div class="relative group cursor-pointer">
<div class="absolute left-[25%] right-[25%] flex items-center">
<div class="flex-1 h-px bg-tertiary-container relative">
<div class="absolute left-0 -top-[5px] border-y-[6px] border-y-transparent border-r-[10px] border-r-tertiary-container"></div>
</div>
<div class="absolute left-1/2 -translate-x-1/2 -top-6 text-center">
<span class="px-2 py-0.5 bg-surface-container-high text-on-tertiary-fixed-variant text-label-md font-label-md rounded border border-outline">SetParameterValues</span>
<span class="block text-[10px] font-mono-sm text-on-surface-variant mt-1">10:05:23.441</span>
</div>
</div>
<div class="h-8 invisible">spacer</div>
</div>
</div>
</div>
<!-- Inspector Panel (Right) -->
<div class="w-[450px] bg-white border-l border-outline-variant flex flex-col shadow-xl z-20">
<div class="p-4 border-b border-outline-variant bg-surface flex justify-between items-center">
<div class="flex items-center gap-2">
<span class="material-symbols-outlined text-on-surface-variant" data-icon="description">description</span>
<span class="font-headline-sm text-[16px] font-bold text-on-surface">Message Detail</span>
</div>
<div class="flex gap-2">
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors" data-icon="content_copy">content_copy</button>
<button class="material-symbols-outlined text-on-surface-variant hover:text-primary transition-colors" data-icon="close">close</button>
</div>
</div>
<div class="flex-1 overflow-auto bg-surface-container-low p-4">
<div class="flex items-center gap-2 mb-4">
<span class="text-label-md font-label-md bg-primary-container text-on-primary-container px-2 py-1 rounded">CWMP:GetParameterValues</span>
<span class="text-mono-sm text-on-surface-variant">ID: 1024_A</span>
</div>
<!-- Code Block Display -->
<div class="rounded-lg border border-outline-variant bg-white p-4 font-mono-md text-mono-md leading-relaxed shadow-inner overflow-hidden">
<pre class="whitespace-pre-wrap"><span class="text-secondary">&lt;soap:Envelope</span> <span class="text-tertiary-fixed-variant">xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"</span><span class="text-secondary">&gt;</span>
<span class="text-secondary">&lt;soap:Header&gt;</span>
<span class="text-secondary">&lt;cwmp:ID</span> <span class="text-tertiary-fixed-variant">soap:mustUnderstand="1"</span><span class="text-secondary">&gt;</span>1024_A<span class="text-secondary">&lt;/cwmp:ID&gt;</span>
<span class="text-secondary">&lt;/soap:Header&gt;</span>
<span class="text-secondary">&lt;soap:Body&gt;</span>
<span class="text-secondary">&lt;cwmp:GetParameterValues&gt;</span>
<span class="text-secondary">&lt;ParameterNames</span> <span class="text-tertiary-fixed-variant">soap:arrayType="xsd:string[3]"</span><span class="text-secondary">&gt;</span>
<span class="text-secondary">&lt;string&gt;</span>InternetGatewayDevice.DeviceInfo.HardwareVersion<span class="text-secondary">&lt;/string&gt;</span>
<span class="text-secondary">&lt;string&gt;</span>InternetGatewayDevice.DeviceInfo.SoftwareVersion<span class="text-secondary">&lt;/string&gt;</span>
<span class="text-secondary">&lt;string&gt;</span>InternetGatewayDevice.ManagementServer.ConnectionRequestURL<span class="text-secondary">&lt;/string&gt;</span>
<span class="text-secondary">&lt;/ParameterNames&gt;</span>
<span class="text-secondary">&lt;/cwmp:GetParameterValues&gt;</span>
<span class="text-secondary">&lt;/soap:Body&gt;</span>
<span class="text-secondary">&lt;/soap:Envelope&gt;</span></pre>
</div>
<div class="mt-6 space-y-4">
<h4 class="text-label-md font-label-md text-on-surface-variant uppercase tracking-wider">Analysis Summary</h4>
<div class="p-3 bg-secondary-container/20 border-l-4 border-secondary rounded-r">
<p class="text-body-sm text-on-surface">ACS is requesting device hardware and software versioning information to determine compatible firmware upgrade paths.</p>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="bg-white p-3 border border-outline-variant rounded">
<span class="text-label-md font-label-md text-on-surface-variant block mb-1">MTU Size</span>
<span class="text-body-md font-medium">1500 Bytes</span>
</div>
<div class="bg-white p-3 border border-outline-variant rounded">
<span class="text-label-md font-label-md text-on-surface-variant block mb-1">Latency</span>
<span class="text-body-md font-medium text-secondary">42ms</span>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Micro-interaction script -->
<script>
function selectMessage(id) {
// Placeholder logic for visual feedback in a prototype
console.log('Selected message context:', id);
}
// Add subtle hover tracking for interactive feel
document.querySelectorAll('.group').forEach(item => {
item.addEventListener('mouseenter', () => {
item.style.transform = 'translateX(2px)';
item.style.transition = 'transform 0.1s ease-out';
});
item.addEventListener('mouseleave', () => {
item.style.transform = 'translateX(0)';
});
});
</script>
</body></html>
Binary file not shown.

After

Width:  |  Height:  |  Size: 255 KiB

@@ -0,0 +1,486 @@
# Troubleshooting Guide: TR-069 Exchange Viewer
Ce guide documente les problèmes courants du système de visualisation des échanges TR-069/CWMP, leurs causes et solutions.
## Table des matières
1. [Déconnexions SSE](#1-déconnexions-sse)
2. [Timeouts MongoDB et récupération](#2-timeouts-mongodb-et-récupération)
3. [Diagnostic de perte de messages](#3-diagnostic-de-perte-de-messages)
4. [Vérification des index et du TTL](#4-vérification-des-index-et-du-ttl)
5. [Problèmes frontend (SequenceViewer)](#5-problèmes-frontend-sequenceviewer)
6. [Feature flags et rollback](#6-feature-flags-et-rollback)
---
## 1. Déconnexions SSE
### 1.1 Causes courantes
| Cause | Symptôme | Fréquence |
|-------|----------|-----------|
| **Timeout d'inactivité (5 min)** | Déconnexion après 5 minutes sans nouvel échange | Normal — par design |
| **Timeout absolu (2 h)** | Déconnexion après 2 heures de connexion continue | Normal — par design |
| **Proxy/reverse-proxy buffering** | Messages SSE arrivent en burst au lieu de temps réel | Courant avec nginx |
| **Perte réseau** | Indicateur "disconnected" dans le viewer | Variable |
| **SSE désactivé (feature flag)** | Erreur 503 à la connexion SSE | Intentionnel |
| **Device non tracé** | Erreur 404 à la connexion SSE | Erreur utilisateur |
### 1.2 Solutions
#### Timeout d'inactivité / absolu
Le système est conçu pour fermer les connexions inactives. Le client se reconnecte automatiquement via le mécanisme `Last-Event-ID` — aucun message n'est perdu.
Pour ajuster les timeouts :
```bash
# Augmenter le timeout d'inactivité (défaut: 300000ms = 5 min)
EXCHANGE_STORE_SSE_INACTIVITY_TIMEOUT_MS=600000
# Augmenter le timeout absolu (défaut: 7200000ms = 2h)
EXCHANGE_STORE_SSE_ABSOLUTE_TIMEOUT_MS=14400000
```
#### Proxy/reverse-proxy buffering
Si un reverse-proxy (nginx, HAProxy) est en place, il peut bufferiser les réponses SSE. Le header `X-Accel-Buffering: no` est déjà envoyé par le serveur, mais vérifiez aussi la configuration nginx :
```nginx
location /api/v1/tr069-trace/ {
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding off;
}
```
#### Client qui ne se reconnecte pas
Le composable `useExchangeStream` utilise l'`EventSource` natif avec retry automatique (3 secondes par défaut). Si la reconnexion ne fonctionne pas :
1. Vérifiez que le device est toujours en trace active
2. Vérifiez les logs serveur pour des erreurs 404 ou 503
3. Vérifiez la valeur `retry` dans la configuration SSE :
```bash
# Intervalle de reconnexion envoyé au client (défaut: 3000ms)
EXCHANGE_STORE_SSE_RETRY_INTERVAL_MS=3000
```
### 1.3 Diagnostic dans les logs
Cherchez les entrées `[SSE]` dans les logs applicatifs :
```bash
docker compose logs app | grep '\[SSE\]'
```
Messages attendus :
- `[SSE] Connection closed { deviceId: "...", reason: "inactivity" }` — timeout normal
- `[SSE] Connection closed { deviceId: "...", reason: "absolute" }` — timeout absolu normal
- `[SSE] Connection closed { deviceId: "...", reason: "client_disconnect" }` — client a fermé
- `[SSE] Error fetching missed exchanges for reconnection` — problème MongoDB lors du rattrapage
---
## 2. Timeouts MongoDB et récupération
### 2.1 Scénarios de timeout
| Scénario | Impact | Comportement |
|----------|--------|--------------|
| **Write timeout (persistance)** | Message potentiellement perdu | Retry (critique) ou discard (non-critique) |
| **Read timeout (API >5s)** | Résultats partiels retournés | `isTruncated: true` dans la réponse |
| **Connection lost** | Toutes les opérations échouent | Les sessions CWMP continuent sans log |
### 2.2 Write timeout — Retry automatique
Le `ExchangeStoreService` gère les échecs d'écriture selon la criticité du message :
**Messages critiques** (Inform, GetParameterValues, SetParameterValuesResponse, Fault, etc.) :
- 3 tentatives avec backoff exponentiel : 1s, 2s, 4s
- Si toutes échouent : log error + discard (session CWMP non impactée)
**Messages non-critiques** (Empty, Unknown) :
- Aucun retry — discard immédiat + log warning
Vérifier les échecs de retry :
```bash
docker compose logs app | grep '\[ExchangeStore\] All retries exhausted'
docker compose logs app | grep '\[ExchangeStore\] Critical message persistence failed'
```
Pour ajuster la politique de retry :
```bash
# Nombre max de tentatives (défaut: 3)
EXCHANGE_STORE_RETRY_MAX_ATTEMPTS=3
# Délai de base en ms pour le backoff (défaut: 1000)
# Résultat : 1000ms, 2000ms, 4000ms
EXCHANGE_STORE_RETRY_BASE_DELAY_MS=1000
```
### 2.3 Read timeout — Résultats partiels
Si une requête API dépasse 5 secondes, le contrôleur retourne les résultats partiels avec un flag :
```json
{
"exchanges": [...],
"metadata": {
"isTruncated": true,
"total": 4500
}
}
```
**Causes courantes :**
- Index manquant sur `(deviceId, timestamp)` — voir section 4
- Trop de documents pour le device (proche du cap 5000)
- Charge MongoDB élevée (vérifier les métriques)
Pour ajuster le timeout de lecture :
```bash
# Timeout en ms (défaut: 5000)
EXCHANGE_STORE_READ_TIMEOUT_MS=5000
```
### 2.4 Vérifier la connectivité MongoDB
```bash
# Se connecter au shell MongoDB
docker compose exec mongodb mongosh
# Vérifier le statut
db.adminCommand({ serverStatus: 1 }).connections
# Vérifier la latence
db.adminCommand({ ping: 1 })
```
---
## 3. Diagnostic de perte de messages
### 3.1 Arbre de décision
```
Message manquant dans le viewer ?
│
├─ Le device est-il en trace ?
│ └─ Non → Activer la trace d'abord. Les messages ne sont PAS persistés hors trace.
│
├─ Le feature flag FEATURE_EXCHANGE_PERSISTENCE est-il à true ?
│ └─ Non → Activer la persistance : FEATURE_EXCHANGE_PERSISTENCE=true
│
├─ Le message apparaît-il dans MongoDB mais pas dans le viewer ?
│ └─ Oui → Problème SSE (voir section 1) ou problème frontend (voir section 5)
│
├─ Le message n'apparaît PAS dans MongoDB ?
│ ├─ Vérifier les logs de retry : grep "All retries exhausted"
│ ├─ Vérifier le cap (5000 messages) : les anciens messages sont supprimés
│ └─ Vérifier le TTL (24h) : les messages >24h sont auto-supprimés
│
└─ Message tronqué (XML incomplet) ?
└─ Le rawXml dépassait 2MB → grep "rawXml exceeds 2MB" dans les logs
```
### 3.2 Vérifier les messages en base
```bash
# Se connecter au shell MongoDB
docker compose exec mongodb mongosh
# Compter les messages pour un device
use adam_acs
db.cwmp_exchanges.countDocuments({ deviceId: "<DEVICE_ID>" })
# Voir les derniers messages
db.cwmp_exchanges.find({ deviceId: "<DEVICE_ID>" }).sort({ timestamp: -1 }).limit(5).pretty()
# Voir la séquence max (dernier message)
db.cwmp_exchange_counters.findOne({ _id: "<DEVICE_ID>" })
```
### 3.3 Vérifier les logs de persistance
```bash
# Chercher les erreurs de persistance
docker compose logs app | grep '\[ExchangeStore\]'
# Messages spécifiques
docker compose logs app | grep 'Failed to persist exchange'
docker compose logs app | grep 'Non-critical message persistence failed'
docker compose logs app | grep 'All retries exhausted'
docker compose logs app | grep 'Message cap enforced'
docker compose logs app | grep 'rawXml exceeds 2MB'
```
### 3.4 Message supprimé par le cap
Le système maintient un maximum de 5000 messages par device. Quand ce cap est dépassé, les messages les plus anciens sont supprimés.
```bash
# Vérifier si le cap a été atteint
docker compose logs app | grep 'Message cap enforced'
```
Pour ajuster le cap :
```bash
# Nombre max de messages par device (défaut: 5000)
EXCHANGE_STORE_MAX_MESSAGES=5000
```
### 3.5 Message supprimé par le TTL
MongoDB supprime automatiquement les documents dont `createdAt` dépasse 24 heures (via l'index TTL).
```bash
# Vérifier les documents les plus anciens
docker compose exec mongodb mongosh --eval "
use adam_acs;
db.cwmp_exchanges.find({}, { createdAt: 1, deviceId: 1 }).sort({ createdAt: 1 }).limit(3).pretty()
"
```
---
## 4. Vérification des index et du TTL
### 4.1 Lister les index existants
```bash
docker compose exec mongodb mongosh --eval "
use adam_acs;
db.cwmp_exchanges.getIndexes().forEach(idx => printjson(idx))
"
```
**Index attendus :**
| Nom | Champs | Options |
|-----|--------|---------|
| `_id_` | `{ _id: 1 }` | — |
| `idx_deviceId_timestamp` | `{ deviceId: 1, timestamp: 1 }` | — |
| `idx_deviceId_sequenceNumber` | `{ deviceId: 1, sequenceNumber: 1 }` | `unique: true` |
| `idx_createdAt_ttl` | `{ createdAt: 1 }` | `expireAfterSeconds: 86400` |
### 4.2 Vérifier l'index TTL
```bash
docker compose exec mongodb mongosh --eval "
use adam_acs;
const indexes = db.cwmp_exchanges.getIndexes();
const ttlIdx = indexes.find(i => i.name === 'idx_createdAt_ttl');
if (ttlIdx) {
print('TTL index OK — expireAfterSeconds:', ttlIdx.expireAfterSeconds);
} else {
print('ERREUR: TTL index manquant !');
}
"
```
### 4.3 Recréer les index manuellement (si manquants)
Si des index sont manquants après un incident :
```bash
docker compose exec mongodb mongosh --eval "
use adam_acs;
// Index composite (deviceId + timestamp)
db.cwmp_exchanges.createIndex(
{ deviceId: 1, timestamp: 1 },
{ name: 'idx_deviceId_timestamp' }
);
// Index unique (deviceId + sequenceNumber)
db.cwmp_exchanges.createIndex(
{ deviceId: 1, sequenceNumber: 1 },
{ unique: true, name: 'idx_deviceId_sequenceNumber' }
);
// Index TTL (24h)
db.cwmp_exchanges.createIndex(
{ createdAt: 1 },
{ expireAfterSeconds: 86400, name: 'idx_createdAt_ttl' }
);
print('Tous les index recréés.');
"
```
### 4.4 Vérifier le statut du TTL monitor
Le TTL monitor de MongoDB tourne par défaut toutes les 60 secondes. Pour vérifier qu'il fonctionne :
```bash
docker compose exec mongodb mongosh --eval "
const status = db.adminCommand({ serverStatus: 1 });
print('TTL deletedDocuments:', status.metrics.ttl.deletedDocuments);
print('TTL passes:', status.metrics.ttl.passes);
"
```
Si `passes` n'augmente pas, le TTL monitor pourrait être bloqué (rare, nécessite un restart MongoDB).
### 4.5 Modifier le TTL
Pour changer la durée du TTL sans recréer l'index :
```bash
docker compose exec mongodb mongosh --eval "
use adam_acs;
db.runCommand({
collMod: 'cwmp_exchanges',
index: {
name: 'idx_createdAt_ttl',
expireAfterSeconds: 172800 // 48h
}
});
"
```
N'oubliez pas de mettre à jour la variable d'environnement pour cohérence :
```bash
EXCHANGE_STORE_TTL_SECONDS=172800
```
### 4.6 Vérifier la collection counters
La collection `cwmp_exchange_counters` stocke les séquences atomiques par device :
```bash
docker compose exec mongodb mongosh --eval "
use adam_acs;
db.cwmp_exchange_counters.find().forEach(doc => {
print(doc._id, '→ seq:', doc.seq);
})
"
```
Si un compteur est incohérent avec les données réelles :
```bash
# Recalculer le max sequenceNumber pour un device
docker compose exec mongodb mongosh --eval "
use adam_acs;
const deviceId = '<DEVICE_ID>';
const maxSeq = db.cwmp_exchanges.find({ deviceId }).sort({ sequenceNumber: -1 }).limit(1).toArray();
if (maxSeq.length > 0) {
db.cwmp_exchange_counters.updateOne(
{ _id: deviceId },
{ \$set: { seq: maxSeq[0].sequenceNumber } }
);
print('Counter reset to', maxSeq[0].sequenceNumber);
}
"
```
---
## 5. Problèmes frontend (SequenceViewer)
### 5.1 Le viewer ne se connecte pas au SSE
**Causes possibles :**
1. Le device n'est pas en trace → activer la trace d'abord
2. Le feature flag `FEATURE_SSE_ENABLED` est à `false` → erreur 503
3. Problème CORS si accès depuis un domaine différent
**Diagnostic :** Ouvrir la console navigateur (F12) et chercher les erreurs EventSource.
### 5.2 Les messages ne s'affichent pas en temps réel
Si les messages arrivent dans MongoDB mais pas dans le viewer :
1. Vérifier l'indicateur de connexion : doit être "connected" (vert)
2. Si "disconnected" (rouge) : voir section 1
3. Si "paused" (jaune) : l'auto-scroll est désactivé car l'utilisateur a scrollé vers le haut — cliquer "Resume auto-scroll"
### 5.3 Performance lente avec beaucoup de messages
Le viewer utilise du virtual scrolling pour >200 messages. Si la performance se dégrade :
- Le cap client est fixé à 1000 messages en mémoire (les plus anciens sont purgés côté client)
- Si le nombre total dépasse 5000 en base, les plus anciens sont purgés côté serveur
- Utiliser "Clear exchanges" pour repartir d'un état vide
### 5.4 L'inspecteur XML affiche du contenu brut (non formaté)
Si le XML n'est pas mis en forme :
- Le XML est peut-être malformé → l'inspecteur affiche le contenu brut en fallback
- Vérifier si le message a un `rawXml` vide (cas "Empty")
---
## 6. Feature flags et rollback
### 6.1 Flags disponibles
| Variable d'environnement | Défaut | Effet si `false`/`file` |
|--------------------------|--------|-------------------------|
| `FEATURE_EXCHANGE_PERSISTENCE` | `true` | Les échanges ne sont pas écrits en MongoDB |
| `FEATURE_SSE_ENABLED` | `true` | L'endpoint SSE retourne 503, le client utilise le REST polling (5s) |
| `FEATURE_EXCHANGE_API_SOURCE` | `mongodb` | Si `file`, l'API lit depuis l'ancien système de fichiers |
### 6.2 Procédure de rollback rapide
En cas de problème critique :
```bash
# 1. Désactiver la persistance (les sessions CWMP continuent normalement)
FEATURE_EXCHANGE_PERSISTENCE=false
# 2. Désactiver le SSE (le frontend utilise le polling REST)
FEATURE_SSE_ENABLED=false
# 3. Revenir à la source fichier pour l'API
FEATURE_EXCHANGE_API_SOURCE=file
```
Redémarrer l'application après modification :
```bash
docker compose restart app
```
### 6.3 Vérifier l'état actuel des flags
Les valeurs actives sont loguées au démarrage de l'application. Vous pouvez aussi interroger le health check :
```bash
curl -s http://localhost:8080/api/v1/health/tr069-exchanges | python3 -m json.tool
```
---
## Commandes utiles — Résumé rapide
```bash
# Status général
docker compose logs app --tail 20 | grep -E '\[ExchangeStore\]|\[SSE\]'
# Compter les échanges d'un device
docker compose exec mongodb mongosh --eval "use adam_acs; db.cwmp_exchanges.countDocuments({deviceId:'<ID>'})"
# Vérifier les index
docker compose exec mongodb mongosh --eval "use adam_acs; db.cwmp_exchanges.getIndexes().forEach(i=>printjson(i))"
# Vérifier le TTL monitor
docker compose exec mongodb mongosh --eval "printjson(db.adminCommand({serverStatus:1}).metrics.ttl)"
# Tester la connexion SSE manuellement
curl -N -H "Accept: text/event-stream" http://localhost:8080/api/v1/tr069-trace/<DEVICE_ID>/exchanges/stream
# Tester la reconnexion avec Last-Event-ID
curl -N -H "Accept: text/event-stream" -H "Last-Event-ID: 42" http://localhost:8080/api/v1/tr069-trace/<DEVICE_ID>/exchanges/stream
# Purger les échanges d'un device
curl -X DELETE http://localhost:8080/api/v1/tr069-trace/<DEVICE_ID>/exchanges
```