# Microservizi

Tutti i servizi devono rispettare i [12factor](https://12factor.net/), descritti in modo anche più dettagliato nell'articolo [An illustrated guide to 12 Factor Apps](https://www.redhat.com/architect/12-factor-app)

## Packaging

### Obiettivo

Ogni microservizio deve poter essere **eseguito facilmente in locale**, anche da chi non conosce a fondo il progetto. L’obiettivo del packaging è:

* facilitare l’avvio dell’applicativo;
* semplificare lo sviluppo e il debugging;
* rendere accessibile un ambiente funzionante con un semplice `docker-compose up`.

### Struttura del repository

Nella root del repository **devono essere presenti** i seguenti file:

| File                     | Descrizione                                                                                       |
| ------------------------ | ------------------------------------------------------------------------------------------------- |
| `Dockerfile`             | Build dell’immagine base del servizio                                                             |
| `docker-compose.yml`     | Definisce i container minimi per l’esecuzione in produzione o ambienti CI                         |
| `docker-compose.dev.yml` | Contiene configurazioni utili per lo sviluppo locale (es. `build`, mount volumi, strumenti debug) |
| `.dockerignore`          | (opzionale) Esclude file e cartelle non rilevanti dalla build                                     |

***

### Principi e buone pratiche

* Il file `docker-compose.yml` deve essere **autosufficiente**: chi lo scarica deve poter eseguire il servizio senza dover clonare l’intero repository.
* Le immagini Docker utilizzate devono essere:
  * disponibili su un **registry pubblico** (es. DockerHub, GitHub Container Registry ecc.);
  * oppure facilmente costruibili in locale tramite override.

> ⚠️ Non includere la chiave `build:` nel `docker-compose.yml` principale, per evitare conflitti nei contesti di esecuzione remota (es. CI/CD).

### Sviluppo locale

Per lo sviluppo in locale:

1. **Copia o rinomina** il file `docker-compose.dev.yml` in `docker-compose.override.yml`
2. Esegui:

   ```bash
   docker-compose up
   ```
3. Docker gestirà automaticamente il **merge tra `docker-compose.yml` e `docker-compose.override.yml`** (vedi: [Docker Docs – Multiple Compose Files](https://docs.docker.com/compose/extends/)).

> ✅ In questo modo, chiunque può avviare l’ambiente di sviluppo **senza modificare i file originali**.

### Esempio di uso dei file

**`docker-compose.yml` (semplificato)**

```yaml
version: "3.9"
services:
  myservice:
    image: registry.gitlab.com/opencity-labs/myservice:1.2.3
    ports:
      - "8080:8080"
```

**`docker-compose.dev.yml`**

```yaml
version: "3.9"
services:
  myservice:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./:/app
    environment:
      - ENVIRONMENT=local
```

Dopo aver rinominato `docker-compose.dev.yml` in `docker-compose.override.yml`, il comando:

```bash
docker-compose up
```

...utilizzerà **in automatico** l’immagine `build` locale con i volumi montati per lo sviluppo.

## Terminazione Pulita

### Obiettivo

Ogni microservizio deve essere in grado di **gestire correttamente la terminazione**, in modo da:

* garantire la **consistenza dei dati**;
* evitare la perdita di eventi o operazioni incomplete;
* chiudere le risorse in uso in maniera sicura (DB, Kafka, file, socket...).

La gestione corretta della terminazione è essenziale sia in **ambiente Docker** che in **Kubernetes**, dove il segnale di terminazione (`SIGTERM`) viene inviato prima di un restart o di un downscaling.

### Segnali da gestire

| Segnale   | Significato                           | Comportamento atteso                      |
| --------- | ------------------------------------- | ----------------------------------------- |
| `SIGTERM` | Richiesta di terminazione **gentile** | Il servizio avvia lo shutdown controllato |
| `SIGKILL` | Terminazione **forzata** e immediata  | Non gestibile dal codice; da evitare      |

> ✅ Il servizio **DEVE** catturare `SIGTERM` e avviare una sequenza di terminazione **controllata**.

***

### Comportamento atteso

Alla ricezione di `SIGTERM`, il servizio deve:

* smettere di accettare nuove richieste o eventi;
* completare le operazioni critiche in corso;
* liberare risorse (connessioni, file, lock...);
* loggare la terminazione a livello `INFO`;
* uscire con **exit code 0** entro un tempo ragionevole.

### Checklist – Terminazione Pulita

| Verifica                           | Descrizione                                               | Esito |
| ---------------------------------- | --------------------------------------------------------- | ----- |
| **Cattura `SIGTERM`**              | Il servizio intercetta correttamente il segnale `SIGTERM` | ☐     |
| **Terminazione controllata**       | Avvia una sequenza di chiusura ordinata                   | ☐     |
| **Timeout ragionevole**            | Si chiude in max 10s (o valore configurato)               | ☐     |
| **Operazioni critiche completate** | Nessuna perdita o corruzione dati                         | ☐     |
| **Chiusura delle risorse**         | Socket, consumer, file, connessioni DB...                 | ☐     |
| **Log di terminazione**            | Esempio: `"SIGTERM ricevuto, shutdown in corso..."`       | ☐     |
| **Nessuna nuova richiesta**        | I listener vengono disattivati                            | ☐     |
| **Exit code 0**                    | Il processo si chiude correttamente                       | ☐     |

### Esempio Python – `uvicorn + asyncio`

```python
import asyncio
import signal
import uvicorn
from myservice import app

APP_HOST = "0.0.0.0"
APP_PORT = 8080

def run_server():
    """Esegue il server intercettando SIGINT e SIGTERM per uno shutdown pulito"""
    config = uvicorn.Config(
        app,
        host=APP_HOST,
        port=int(APP_PORT),
        proxy_headers=True,
        forwarded_allow_ips='*',
        access_log=False,
    )
    server = uvicorn.Server(config)

    loop = asyncio.get_event_loop()

    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, lambda: asyncio.create_task(server.shutdown()))

    try:
        loop.run_until_complete(server.serve())
    except asyncio.CancelledError:
        pass  # Evita traceback in console

if __name__ == '__main__':
    run_server()
```

### Esempio Go – `signal.NotifyContext`

```go
package main

import (
    "context"
    "log"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("pong"))
    })

    srv := &http.Server{
        Addr:    ":8080",
        Handler: mux,
    }

    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    defer stop()

    go func() {
        log.Println("Server in avvio su :8080")
        if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Errore durante l'avvio del server: %v", err)
        }
    }()

    <-ctx.Done()
    log.Println("SIGTERM ricevuto, avvio terminazione...")

    shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Fatalf("Errore durante la terminazione: %v", err)
    }

    log.Println("Terminazione completata.")
}
```

### Script di test automatico

Usare questo script per validare la terminazione pulita in locale o integrarlo nella CI.

```bash
#!/bin/bash
set -euo pipefail

SERVICE_NAME="test_shutdown_service"
IMAGE_NAME="your-image-name"
LOG_FILE="shutdown_test.log"

echo "▶️ Avvio container..."
docker run --rm --name "$SERVICE_NAME" -d "$IMAGE_NAME" > /dev/null

sleep 2  # attesa minima per stabilità

echo "🛑 Invio SIGTERM..."
docker kill --signal=SIGTERM "$SERVICE_NAME"

sleep 2

echo "🔍 Verifica terminazione..."
if docker ps -a --format '{{.Names}}' | grep -q "$SERVICE_NAME"; then
  echo "❌ Il container è ancora in esecuzione"
  exit 1
else
  echo "✅ Terminazione completata correttamente"
fi
```

* Se si usa `docker-compose`, si può integrare il test nel CI lanciando:

  ```bash
  docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
  docker-compose kill -s SIGTERM
  ```

## Configurazione

### Principi

La **configurazione dei microservizi** deve avvenire:

* principalmente tramite **variabili d’ambiente**;
* opzionalmente tramite:
  * file `.env` (per ambienti locali);
  * parametri CLI (per job schedulati o debug interattivo).

### Best practice

| Metodo               | Contesto di utilizzo                           |
| -------------------- | ---------------------------------------------- |
| Variabili d’ambiente | Default per tutti i deployment (prod/dev/test) |
| `.env` file          | Sviluppo locale o test manuali                 |
| CLI parametri        | Script cron, job schedulati, test/debug        |

> ❗ Le configurazioni non devono essere hardcoded nel codice.\
> Utilizzare sempre default sicuri e sovrascrivibili.

## Healthcheck

### Obiettivo

Permettere al sistema di orchestrazione (Docker/Kubernetes) di verificare che il servizio sia vivo e funzionante.

### HTTP microservizi

* Deve esporre un **endpoint `/status`**:
  * `200 OK` se tutto è funzionante;
  * codice diverso in caso di errore.

### Altri microservizi

* Se il servizio **non è HTTP**, l’healthcheck può essere:
  * la presenza di un file di stato;
  * la verifica di un processo in esecuzione.

> ✅ L’healthcheck **DEVE essere incluso nel `Dockerfile`**.

## Logging

### **Principi generali**

* Tutti i microservizi devono implementare un sistema di log coerente, strutturato e facilmente aggregabile.
* Riferimento normativo: [**Allegato 4 delle Linee Guida di Interoperabilità AgID**](https://www.agid.gov.it/sites/agid/files/2024-07/Linee_guida_interoperabilit%C3%A0PA_All4_Raccomandazioni-di_implementazione.pdf).
* I log devono supportare almeno i livelli: `DEBUG`, `INFO`, `ERROR`. Una gestione completa prevede sei livelli (vedi sotto).
* Ogni **log di errore o anomalia** deve essere su **una singola riga**, facilmente parsabile (plaintext `key=value` o JSON).
* Evitare log verbosi e non strutturati: ostacolano il monitoraggio e la correlazione cross-microservizio.

> ⚠️ **Non mischiare log di tipo HTTP e stacktrace multilinea**.
>
> * Scrivere i log a **singola riga su `stdout`**.
> * Scrivere stacktrace (se necessario) separatamente su `stderr`.

### **Formato e contenuto dei log**

* **Preferibile** l’uso di log in **formato JSON puro**, ma solo se interamente strutturato.
* È **vietato**:
  * Mischiare testo libero e JSON nello stesso log.
  * Includere payload JSON grezzi come stringa in un campo JSON (es. loggare interamente l’evento Kafka).

#### **Campi richiesti in ogni log rilevante**

| Campo                                | Obbligatorio | Descrizione                                                                                        |
| ------------------------------------ | ------------ | -------------------------------------------------------------------------------------------------- |
| `level`                              | ✅            | Definisce il tipo di log (INFO, ERROR, DEBUG, CRITICAL, WARNING, ecc.)                             |
| `timestamp`                          | ✅            | In UTC, formato [RFC 3339](https://datatracker.ietf.org/doc/html/rfc3339), con `T` e `Z` maiuscoli |
| `environment`                        | ✅            | Definisce l'ambiente in cui il microservizio è in esecuzione                                       |
| `event_id`                           | ⚠️           | Identificatore univoco dell’evento (se applicabile)                                                |
| `event_type`                         | ✅            | Tipo di evento o operazione ricevuta                                                               |
| `call_type`                          | ⚠️           | Tipo chiamata: `HTTP`, `CLI`, `EVENT`                                                              |
| `topic`                              | ⚠️           | Kafka topic da cui è stato letto l’evento (se applicabile)                                         |
| `log_message`                        | ✅            | Messaggio del log (errore o info)                                                                  |
| `client_ip`                          | ⚠️           | IP della chiamata in ingresso. Usare `X-Forwarded-For` quando presente                             |
| `tenant`                             | ✅            | identificativo univoco dell'ente a partire dal quale è stato generato il log                       |
| `provider`, `application`, `service` | ✅            | Contesto operativo (se disponibili)                                                                |
| `user_id`                            | ❌            | Se presente, deve essere un riferimento anonimo (es. ID utente). **Mai dati personali** in chiaro  |

All’avvio, ogni servizio **DEVE loggare** la versione in esecuzione e l’ambiente (es. `local`, `boat-qa`, `boat-prod`).

***

#### Livelli di log

<table><thead><tr><th>Level</th><th width="413">Description</th><th>Required</th></tr></thead><tbody><tr><td>CRITICAL</td><td>Fallimento grave, con perdita di dati o che comporta lo l'uscita dal flusso di esecuzione (shutdown) del servizio per impossibilità a proseguire o perché è più <em>safe</em> non proseguire.</td><td>No</td></tr><tr><td>ERROR</td><td>Anomalia che non è possibile gestire e che avrà effetti sul risultato. A ERROR, il <em>rate</em> dei log che si alza dovrebbe essere indice che ci sono problemi per i quali è importante attrarre l'attenzione degli amministratori. Attenzione a non confondere errori che si verificano su sistemi esterni e che non sono nostri errori. Date sempre per scontato che una riga di errore prodotta dovrebbe sempre essere letta da qualcuno, altrimenti meglio esporre un livello di warning o non loggare proprio. <strong>Una anomalia dovrebbe sempre dare origine a una e una sola riga di log.</strong></td><td>Si</td></tr><tr><td>WARNING</td><td>Anomalia che non avrebbe dobuto presentarsi ma che è stata gestita correttamente per non comportare errori nel nostro servizio. Ad esempio ho ricevuto un evento con una versione negativa, ma l'ho ignorato. Oppure non sono riuscito a collegarmi al db la prima volta, ma solo dopo 2 tentativi.</td><td>Si</td></tr><tr><td>INFO</td><td>Comunica un cambiamento di stato del servizio. Un evento significativo produce una riga di log per ogni evento o chiamata ricevuta dall'applicativo. Se una transazione o un evento gestito presenta errori NON si deve produrre la riga di INFO e la riga di ERROR, ma solo la seconda.</td><td>Si</td></tr><tr><td>DEBUG</td><td>Questo è il livello in cui comunicare informazioni diagnostiche, non necessarie se non si sta investigando un errore specifico. Devono essere informazioni utili a comprendere errori, non semplicemente a comprendere il flusso interno del software, per il quale esiste il livello apposito (TRACE). Una informazione utile è dare tutto il contesto che permette di comprendere perché il servizio si sta comportando in un certo modo.</td><td>No</td></tr><tr><td>TRACE</td><td>Questo livello è inteso per tracciare il flusso interno del servizio, in modo molto dettagliato. Per esempio si può inserire un log a questo livello per capire in quale ramo del codice siamo finiti con l'esecuzione, oppure si può inserire una riga all'inizio e una alla fine di certe porzioni di codice rilevanti.</td><td>No</td></tr></tbody></table>

> ✅ **Ogni anomalia deve generare una e una sola riga di log a INFO, i dettagli vanno a DEBUG.**

***

#### Esempi di log ben formattati

**JSON strutturato**

```json
{
  "level": "ERROR",
  "timestamp": "2025-04-18T10:27:52Z",
  "environment": "boat-prod",
  "event_id": "abcd-1234",
  "event_type": "PaymentRequestReceived",
  "topic": "pagopa-incoming-events",
  "version": "1.3.2",
  "environment": "boat-prod",
  "tenant": "9bee12a4-...",
  "application": "50b4f598-...",
  "service": "0a28427f-...",
  "provider": "sicraweb-wsprotocollodm",
  "client_ip": "10.12.3.1",
  "user_id": "user_92348",
  "status": "error",
  "message": "Impossibile trovare dati di logon validi per l'utente"
}
```

**Plaintext `key=value`**

```
ERROR 2025-04-18T10:27:52Z event_id=abcd-1234 tracker=252603771125040 provider=sicraweb-wsprotocollodm tenant=9bee12a4 application=50b4f598 service=0a28427f topic=pagopa-incoming-events microservice=payment-handler version=1.3.2 environment=boat-prod client_ip=10.12.3.1 user_id=user_92348 error="Impossibile trovare dati di logon validi per l'utente; nested exception: Connection aborted (Connection reset by peer)"
```

### Checklist di qualità per log `ERROR`

Usare questa checklist in review e PR:

* [ ] Il log è su una singola riga?
* [ ] Include `event_id`, `event_type`, `topic`, `microservice`, `error`?
* [ ] I dati personali sono esclusi?
* [ ] Il messaggio è leggibile e utile senza stacktrace?
* [ ] È facilmente aggregabile da dashboard/log viewer?

## Monitoring errori

### Obiettivo

Raccogliere centralmente gli errori per individuare rapidamente problemi in produzione.

### Requisiti

* Ogni microservizio **DEVE integrare** [**Sentry**](https://sentry.io/).
* Una volta configurato:
  * abilitare **l’integrazione con Sentry**;
  * definire **regole di alert** personalizzate, se quelle standard non bastano.

> 📬 Usare `SENTRY_DSN` come variabile d’ambiente per collegare il servizio a Sentry.

## Monitoring metriche

### Obiettivo

Esportare metriche di stato e performance per il monitoraggio continuo.

### Endpoint Prometheus

* Se HTTP, il microservizio **DEVE esporre** un endpoint `/metrics` in formato Prometheus.

### Cosa monitorare

| Tipo                    | Obbligatorio | Descrizione                                                       |
| ----------------------- | ------------ | ----------------------------------------------------------------- |
| **Error counters**      | ✅            | Numero errori critici/intermittenti                               |
| **Response histograms** | ✅            | Tempi di risposta di servizi esterni                              |
| **Status code metrics** | ⚠️           | Solo se utili (es. 500 frequenti) – altrimenti usiamo Traefik     |
| **404 o bot traffic**   | ❌            | **NON** deve essere esposto come metrica – produce rumore inutile |

## Cron <a href="#user-content-cron" id="user-content-cron"></a>

### Principio

Non reinventare un sistema di schedulazione dentro l’applicazione. Usare invece l’orchestratore.

### Linee guida

* **NON** implementare schedulatori interni (es. cron manuale, loop time-based).
* Il servizio deve essere **idempotente**: rieseguibile sugli stessi dati **senza effetti collaterali**.

### Esecuzione corretta

Implementare task cron come **comandi CLI**, eseguibili con la stessa immagine Docker:

```bash
docker run your-image-name python cron_jobs/protocol_batch.py
```

Il job verrà poi schedulato tramite **clustered cron** (es. [`crazy-max/cronjob`](https://github.com/crazy-max/diun/blob/master/.github/workflows/cron.yml)).

## Parametri e file di configurazione

### Principio

Niente overengineering: Docker impone già un mapping tra esterno e interno.

### Best practice

| Elemento         | Regola                                                                                                           |
| ---------------- | ---------------------------------------------------------------------------------------------------------------- |
| **Path interni** | Usa path fissi tipo `/data`, senza preoccuparsi del mapping esterno                                              |
| **Configs**      | Usare [**Docker Swarm Configs**](https://docs.docker.com/engine/swarm/configs/) per file di configurazione       |
| **Secrets**      | Usare [**Docker Swarm Secrets** ](https://docs.docker.com/engine/swarm/secrets/)per credenziali e dati sensibili |
| **NO volumes**   | Evitare l’uso di volume mapping per la configurazione                                                            |

> ✅ Questo approccio garantisce sicurezza, semplicità e prevedibilità.

## Continuous Integration

### Obiettivo

Standardizzare la CI tra i progetti, garantendo efficienza, velocità e qualità.

### Requisiti

* Ogni progetto **DEVE includere la CI condivisa**:

```yaml
stages:
 - build
 - test
 - deploy
 - review
 - dast
 - staging
 - canary
 - production
 - incremental rollout 10%
 - incremental rollout 25%
 - incremental rollout 50%
 - incremental rollout 100%
 - performance
 - cleanup

include:
  - project: 'opencity-labs/product'
    ref: main
    file: '.gitlab/ci/devops.yml'
```

* Eventuali step personalizzati (es. test, lint, coverage) **vanno inseriti dopo** lo `include`.
* Devono **riutilizzare la build già fatta** tramite [artifact](https://docs.gitlab.com/ee/ci/jobs/job_artifacts.html), evitando rebuild inutili.

### Performance CI

* Durata **massima** della CI: **15 minuti**
* Durata **consigliata**: **≤ 5 minuti**

> ⚡ Ottimizzare usando:
>
> * build [multi-stage](https://docs.docker.com/build/building/multi-stage/) Docker;
> * [cache di GitLab](https://docs.gitlab.com/ee/ci/caching/);
> * test selettivi.

### Test `publiccode.yml`

Per progetti pubblicati su [Developers Italia](https://developers.italia.it/):

```yaml
publiccode:
  stage: test
  allow_failure: false
  image:
    name: italia/publiccode-parser-go
    entrypoint: [""]
  script:
    - publiccode-parser /dev/stdin < publiccode.yml
  only:
    - master
```
