Tema
Runbook: server grafi etl serve (HTTP)
Sintesi
- Cosa fa: processo daemon che espone via HTTP la topologia dei grafi delle pipeline (nodi/archi,
PlanSpec) e serve la documentazione già costruita su/guida. All'avvio calcola gli spec di tutte le pipeline del catalogo dietl.config.ts(viagetSpec()) e li tiene in una cache in-memory immutabile; li serve poi read-only. I descriptor mostrano la configurazione reale del.envcaricato, con la variabile e l'origine di ogni valore; un'istanza di connettore che non si costruisce ricade sull'esempio da sola, e le variabili mancanti sono marcatemancante. Nessuna pipeline ricade più sull'esempio per tutto il piano. Dopo una modifica al.envva riavviato: ogni piano porta l'ora in cui la cache è stata costruita (computedAt), così un riavvio mancato si vede. Sostituisce la generazione a build-time del vecchioplans.generated.jsonlato UI. - Owner: team ETL.
- Consumatore:
noeva-etl-ui, all'URL impostato nell'app (dialog «Server ETL»; predefinitohttp://localhost:3005). - Criticità: bassa. Se è fermo, la UI non popola il selettore pipeline né il grafo (mostra lo stato di errore), ma nessun dato viene perso: al riavvio la cache viene ricostruita dal registro. Grafi e storico sono in sola lettura; l'unica scrittura è la run tracciata lanciata dalla UI (
POST /runs, solo dal loopback), che scrive sui sink come una run normale e salva report e traccia nello storico localeETL_RUNS_DIR.
Esecuzione normale
bash
pnpm run etl:serve # oppure: pnpm run etl serveNel container: node dist/cli.js serve (o l'equivalente entrypoint), con la porta esposta.
Endpoint
GET /health→{ "status": "ok" }GET /deploy→ chi è il deploy, per la UI desktop che vi si collega. Risponde senza autenticazione, perché serve a sapere dove fare il login, e porta solo dati non segreti: maiNOEVA_API_KEY.json{ "client": "acme", "noevaApiBaseUrl": "https://api.noeva.ai", "workspaces": ["<uuid-a>", "<uuid-b>"], "core": "1.0.0", "contract": 1 }clientè lo slug didefineEtlConfig,corela versione del pacchetto installato (gli stessiclientecoredel report v8),noevaApiBaseUrlèNOEVA_API_BASE_URL, senza le credenziali che vi fossero scritte (utente:password@si toglie prima di servirlo), eworkspacesi workspace serviti (NOEVA_WORKSPACE_IDS, o il soloNOEVA_WORKSPACE_ID).contractè il numero di contratto: la UI dichiara l'intervallo che supporta e fuori si blocca. Si alza a ogni cambio incompatibile di ciò che la UI legge (/deploy,/graphs, storico e eventi delle run); un campo aggiunto non lo alza. I tipi di queste risposte li esporta il pacchetto, in@noeva/etl-core/contract. Un503 { message }vuol dire un server costruito senza deploy:etl servelo passa sempre, quindi lo vede solo chi usacreateServera mano.GET /graphs→[{ "name": "...", "description": "...", "version": 1, "workspaces": ["..."] }], doveversionè la versione di pipeline corrente, l'ultima voce del changelog della pipeline, eworkspacesi workspace serviti a cui la pipeline è assegnata (ETL_PIPELINE_WORKSPACES; senza, tutti): la UI mostra quelle del workspace sceltoGET /graphs/:name→PlanSpec({ name, nodes, edges, instances?, configuration?, computedAt }),404 { message }se il nome non esiste. Sul nome di una sub-pipeline è ancora un404, col messaggio dedicato «"<nome>"è una sub-pipeline, si istanzia da un nodopipeline: si legge in GET /sub-pipelines»: una sub-pipeline non ha un piano proprio, il suo grafo si vede solo dentro le istanze di una root.GET /sub-pipelines→ l'elenco delle sub-pipeline istanziate dalle pipeline servite, in ordine di nome, a ogni profondità di annidamento. Nessun dettaglio per nome: ogni voce è già intera.
json
[
{
"name": "riepilogo-ordini",
"description": "Riepilogo degli ordini arricchiti per tier cliente: …",
"input": [
{
"id": "order_id",
"name": "order_id",
"dataType": "string",
"isKey": true
}
],
"outputs": [
{
"name": "per-tier",
"columns": [
{ "id": "tier", "name": "tier", "dataType": "string", "isKey": false }
]
},
{
"name": "fatti",
"columns": [
{
"id": "order_id",
"name": "order_id",
"dataType": "string",
"isKey": true
}
]
}
],
"params": [
{
"name": "tierMancante",
"dataType": "string",
"required": false,
"default": "unknown"
}
],
"deps": [],
"usedBy": ["orders-enriched-daily"]
}
]input e le colonne di ogni uscita sono PlanColumn, ricavate dagli schemi Zod come le colonne dei nodi. Un parametro è required: false se ha un default (che compare in default) o è opzionale. deps porta per ogni slot il tipo, lookup o action. usedBy elenca le pipeline root servite che la istanziano, anche attraverso un'altra sub-pipeline.
Nel PlanSpec di GET /graphs/:name, computedAt è l'ora di calcolo del piano, ISO 8601 UTC: quella in cui il server ha costruito la cache all'avvio, uguale per tutti i grafi e fra una richiesta e l'altra. configuration è la configurazione della pipeline: tutte le sue variabili, con valore e origine, comprese quelle che nessun nodo mostra (destinatari, tempi d'attesa); manca per le pipeline che non dichiarano un manifest delle variabili.
json
{
"name": "test-posta",
"nodes": ["…"],
"edges": ["…"],
"configuration": [
{ "env": "TEST_POSTA_CARTELLA", "origine": "default", "value": "INBOX" },
{ "env": "TEST_POSTA_CARTELLA_LOCALE", "origine": "mancante" },
{
"env": "TEST_POSTA_DESTINATARIO",
"origine": "env",
"value": "prova@esempio"
}
],
"computedAt": "2026-10-06T09:30:00.000Z"
}Una variabile ha value solo con origine env o default; di una segreta c'è solo lo stato (impostata), mai il valore.
Lo storico delle run e le run tracciate stanno sulle rotte /runs. Le rotte del report sono aperte come quelle dei grafi; lancio e tracce rispondono solo dalla macchina stessa, 403 altrimenti:
GET /runs,GET /runs/:id,GET /runs/:id/events→ storico e stream SSE;POST /runs→ lancia una run tracciata,202 { runId }; il corpo è{ pipeline, trace, params? }, conparamsi parametri di lancio che il runner valida contro la dichiarazione della pipeline;GET/DELETE /runs/:id/trace→ legge o elimina la traccia di una run (La traccia di una run).
Dietro un reverse proxy sulla stessa macchina le richieste arrivano dal loopback, e il controllo sull'indirizzo non protegge più: non esporre etl serve così se si usano le run tracciate.
La documentazione: /guida
La serve il deploy, già costruita. Il pulsante Guida dell'app desktop apre /guida/ del server collegato nel browser di sistema.
GET /guida/…→ la documentazione del cliente, il sito cheetl docs buildcostruisce indocs/.vitepress/distdel repo cliente. Se manca,GET /guida/risponde con una pagina «Documentazione del cliente non costruita» che porta alla guida del core.GET /guida/core/…→ la guida del core, quella del pacchetto installato (dist/guida/), costruita col prefisso/guida/core/: è la guida della versione del core che gira.
Gli URL sono quelli puliti di VitePress (/guida/core/nodes/lookup è nodes/lookup.html), e una pagina che non c'è riceve il 404.html del suo sito. etl serve non costruisce niente: guarda i due siti all'avvio, e se uno manca lo dice nel log e continua.
Configurazione (env)
ETL_HTTP_PORT— porta di ascolto (default 3005, distinta danoeva-server-api= 3002; sotto PM2 9010, vedi Variabili d'ambiente del core).ETL_HTTP_CORS_ORIGIN— origine consentita da CORS (default*). In produzione impostare l'origine della UI.ETL_PIPELINE_WORKSPACES— facoltativa, filtro fra i workspace dello stesso cliente. Se impostata,/graphsespone solo le pipeline del catalogo assegnate ai workspace serviti dall'istanza (NOEVA_WORKSPACE_ID/NOEVA_WORKSPACE_IDS); le altre rispondono404. Lo stesso filtro vale per/sub-pipelines: una sub-pipeline compare se la usa almeno una pipeline servita, eusedByelenca solo quelle; altrimenti non esiste nemmeno per nome, e/graphs/:namerisponde col404generico. Le sub-pipeline non si assegnano: il loro nome nella variabile ferma l'avvio. Formato e regole nel runbook del worker.ETL_RUNS_DIR,ETL_RUNS_KEEP,ETL_TRACES_KEEP— storico locale delle run e retention di report e tracce. ConETL_RUNS_DIR=""lo storico è spento e le rotte/runsrispondono503.ETL_STATE_DIR— stato di pipeline delle run tracciate lanciate da qui, nello stesso file locale dietl run(stato incrementale).
Dettagli e default in .env.example e in La traccia di una run.
Nessuna autenticazione: espone solo topologia + descriptor non sensibili, l'identità non segreta del deploy di GET /deploy e i due siti statici di /guida. La documentazione del cliente comprende i suoi ADR e le schede delle sue pipeline: chi espone etl serve fuori dalla rete del cliente espone anche quella. La protezione in prod è la restrizione dell'origine. Fanno eccezione il lancio delle run tracciate e le tracce, che rispondono solo dal loopback (vedi sopra).
Verifica rapida
bash
curl -s localhost:3005/health
curl -s localhost:3005/deploy
curl -s localhost:3005/graphs
curl -s localhost:3005/graphs/orders-enriched-daily
curl -s localhost:3005/sub-pipelines
curl -sI localhost:3005/guida/ localhost:3005/guida/core/Diagnosi
- La UI non carica i grafi → verificare che il server sia su e raggiungibile (
curldiretto isola server vs UI) e che l'URL nel dialog «Server ETL» dell'app punti all'host e alla porta giusti («Verifica connessione» fa lo stessoGET /health). Se la UI è su un altro PC, provare ilcurlda quel PC e controllare il firewall della macchina del server (portaETL_HTTP_PORT). - Grafi e storico ok, ma l'avanzamento live arriva a scatti → lo stream SSE (
EventSource) è soggetto a CORS, a differenza delle altre richieste.ETL_HTTP_CORS_ORIGINdeve essere*oppure l'origine dell'app (http://tauri.localhostsu Windows,tauri://localhostsu macOS/Linux); altrimenti la UI ripiega sul polling. - Il pannello mostra valori vecchi dopo una modifica al
.env→ il server legge il.envuna volta sola, all'avvio. Se l'ora di calcolo del piano (computedAtinGET /graphs/:name, in UTC) è precedente alla modifica, il server non è stato riavviato: riavviarlo e ricaricare il grafo. 403suPOST /runso su/runs/:id/trace→ la richiesta non arriva dalla macchina del server. Lancio e tracce rispondono solo dal loopback: l'app desktop deve girare sulla stessa macchina.503sulle rotte/runs→ lo storico locale è spento (ETL_RUNS_DIR=""), quindi non c'è dove salvare report e tracce.404su un grafo atteso → il nome non è nel catalogo dietl.config.ts, oppureETL_PIPELINE_WORKSPACESnon lo assegna a nessun workspace dell'istanza. Il log di avvio elenca le pipeline di ogni workspace; confrontare conGET /graphs.404con «è una sub-pipeline» → il nome è quello di una sub-pipeline, che non ha un piano proprio: si legge inGET /sub-pipelines, e il suo grafo nel piano di una root che la istanzia (usedBy).- Una sub-pipeline attesa manca da
/sub-pipelines→ nessuna pipeline servita la istanzia: è visibile solo dove lo è una root che la usa. - Nel log
Avviso: documentazione del cliente non costruita (…)→ nella cartella fra parentesi non c'è il sito del cliente, e/guida/mostra la pagina che lo dice. Il sito si costruisce al deploy conpnpm etl docs buildnel repo cliente, prima di riavviareetl serve, che guarda i siti solo all'avvio. - Nel log
Avviso: guida del core non costruita (…)→/guida/core/risponde404. Nel pacchetto pubblicato la guida c'è sempre: l'avviso vuol dire un'installazione rotta, da reinstallare. - L'avvio si ferma con «Due sub-pipeline diverse si chiamano …» → due root istanziano due definizioni diverse con lo stesso nome. Il test
src/sub-pipelines/list.test.tslo impedisce prima del rilascio; il nome va reso unico.
Deploy
Il server va deployato come servizio distinto dal worker (stesso codice/immagine, comando diverso), con la porta esposta. Aggiornare di conseguenza il Dockerfile del pacchetto (deploy/Dockerfile) e, per ECR, deploy/deploy-ecr.sh (runbook).

