Skip to content

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 di etl.config.ts (via getSpec()) e li tiene in una cache in-memory immutabile; li serve poi read-only. I descriptor mostrano la configurazione reale del .env caricato, 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 marcate mancante. Nessuna pipeline ricade più sull'esempio per tutto il piano. Dopo una modifica al .env va 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 vecchio plans.generated.json lato UI.
  • Owner: team ETL.
  • Consumatore: noeva-etl-ui, all'URL impostato nell'app (dialog «Server ETL»; predefinito http://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 locale ETL_RUNS_DIR.

Esecuzione normale ​

bash
pnpm run etl:serve         # oppure: pnpm run etl serve

Nel 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: mai NOEVA_API_KEY.

    json
    {
      "client": "acme",
      "noevaApiBaseUrl": "https://api.noeva.ai",
      "workspaces": ["<uuid-a>", "<uuid-b>"],
      "core": "1.0.0",
      "contract": 1
    }

    client è lo slug di defineEtlConfig, core la versione del pacchetto installato (gli stessi client e core del report v8), noevaApiBaseUrl è NOEVA_API_BASE_URL, senza le credenziali che vi fossero scritte (utente:password@ si toglie prima di servirlo), e workspaces i workspace serviti (NOEVA_WORKSPACE_IDS, o il solo NOEVA_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. Un 503 { message } vuol dire un server costruito senza deploy: etl serve lo passa sempre, quindi lo vede solo chi usa createServer a mano.

  • GET /graphs → [{ "name": "...", "description": "...", "version": 1, "workspaces": ["..."] }], dove version è la versione di pipeline corrente, l'ultima voce del changelog della pipeline, e workspaces i workspace serviti a cui la pipeline è assegnata (ETL_PIPELINE_WORKSPACES; senza, tutti): la UI mostra quelle del workspace scelto

  • GET /graphs/:name → PlanSpec ({ name, nodes, edges, instances?, configuration?, computedAt }), 404 { message } se il nome non esiste. Sul nome di una sub-pipeline è ancora un 404, col messaggio dedicato «"<nome>" è una sub-pipeline, si istanzia da un nodo pipeline: 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? }, con params i 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 che etl docs build costruisce in docs/.vitepress/dist del 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 da noeva-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, /graphs espone solo le pipeline del catalogo assegnate ai workspace serviti dall'istanza (NOEVA_WORKSPACE_ID / NOEVA_WORKSPACE_IDS); le altre rispondono 404. Lo stesso filtro vale per /sub-pipelines: una sub-pipeline compare se la usa almeno una pipeline servita, e usedBy elenca solo quelle; altrimenti non esiste nemmeno per nome, e /graphs/:name risponde col 404 generico. 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. Con ETL_RUNS_DIR="" lo storico è spento e le rotte /runs rispondono 503.
  • ETL_STATE_DIR — stato di pipeline delle run tracciate lanciate da qui, nello stesso file locale di etl 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 (curl diretto 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 stesso GET /health). Se la UI è su un altro PC, provare il curl da quel PC e controllare il firewall della macchina del server (porta ETL_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_ORIGIN deve essere * oppure l'origine dell'app (http://tauri.localhost su Windows, tauri://localhost su macOS/Linux); altrimenti la UI ripiega sul polling.
  • Il pannello mostra valori vecchi dopo una modifica al .env → il server legge il .env una volta sola, all'avvio. Se l'ora di calcolo del piano (computedAt in GET /graphs/:name, in UTC) è precedente alla modifica, il server non è stato riavviato: riavviarlo e ricaricare il grafo.
  • 403 su POST /runs o 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.
  • 503 sulle rotte /runs → lo storico locale è spento (ETL_RUNS_DIR=""), quindi non c'è dove salvare report e tracce.
  • 404 su un grafo atteso → il nome non è nel catalogo di etl.config.ts, oppure ETL_PIPELINE_WORKSPACES non lo assegna a nessun workspace dell'istanza. Il log di avvio elenca le pipeline di ogni workspace; confrontare con GET /graphs.
  • 404 con «è una sub-pipeline» → il nome è quello di una sub-pipeline, che non ha un piano proprio: si legge in GET /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 con pnpm etl docs build nel repo cliente, prima di riavviare etl serve, che guarda i siti solo all'avvio.
  • Nel log Avviso: guida del core non costruita (…) → /guida/core/ risponde 404. 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.ts lo 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).

Noeva è un marchio registrato di 4D S.R.L.