# Observer

> Objectif : voir le trafic par opération, pas par URL brute.

Guide - Depuis la version 0.2.0 - Flexway v1.6.3 - https://flexway.io/docs/operations-observability/

> Guide d'exploitation : configurer, déployer et observer la passerelle. Ce que fait la capacité côté produit : [guide produit](https://flexway.io/docs/product-overview/) ; l'appeler : [guide développeur](https://flexway.io/docs/developer-overview/).

**Objectif** : voir le trafic par opération, pas par URL brute.

**Commande**

```bash
curl -s localhost:9090/metrics | grep '^flexway_'     # le port d'administration, jamais celui du trafic
```

```yaml
observability:
  otlp_endpoint: tempo.observ:4317    # facultatif, hôte:port ou URL (http://, https://) ; sinon OTEL_EXPORTER_OTLP_ENDPOINT, sinon rien n'est poussé
```

Traces et métriques partent vers ce point. Un point qui ne prend que des traces (Tempo, Jaeger), ou un
collecteur qui scrute déjà `/metrics`, appelle `OTEL_METRICS_EXPORTER=none` : les métriques restent
alors sur `/metrics`, poussées nulle part.

Chaque réplica se nomme dans la ressource qu'il pousse : `service.instance.id` vaut `POD_NAME` (à poser
par l'API descendante, `metadata.name`) ou, à défaut, le nom d'hôte, qui est le nom du pod dans
Kubernetes. Un backend qui traduit l'OTLP en séries Prometheus en fait le label `instance` ; sans lui,
deux réplicas écriraient la même série et les compteurs sauteraient de l'un à l'autre.
`OTEL_RESOURCE_ATTRIBUTES` est lu en dernier et peut ajouter des attributs ou remplacer celui-ci.

| Métrique | Pour répondre à |
| --- | --- |
| `flexway_requests_total` | Combien, par `operation`, `method`, `status_class`, `upstream`, `tenant` (borné) |
| `flexway_request_duration_seconds` | Combien de temps, hors flux - la durée d'un SSE noierait la latence |
| `flexway_upstream_duration_seconds` | Combien de ce temps est celui de l'amont |
| `flexway_requests_in_flight` | Ce qui est en cours, par amont - le signal de mise à l'échelle |
| `flexway_retries_total` | Combien de reprises, par amont et motif (`connection`, `status_502`, `status_503`) |
| `flexway_inference_tokens_total` | Combien de tokens chaque tenant consomme, par backend et sens (`input`, `output`) - ce que rapporte le backend ; par `agent` depuis `1.2.0` : un agent qu'un override `ai` déclare, `_other` pour tout autre, `none` sans agent - jamais un sujet ; par `capability` et `metered_by` depuis `1.5.0` - une consommation n'additionne que `metered_by!="upstream"` (une série d'avant `1.5.0` n'a pas l'étiquette) |
| `flexway_inference_calls_total` | Combien d'appels d'inférence servis - complétions, embeddings, méthodes gRPC mesurées -, par tenant, agent, client, backend, `capability` et `metered_by` |
| `flexway_inference_cost_EUR_total`, `flexway_inference_fallback_total` | Combien d'euros, par tenant, agent et backend qui a servi ; combien d'appels passés à un backend de repli, par `from`, `to`, `cause` et, depuis `1.2.0`, `tenant` |
| `flexway_quota_requests_total` | Combien de requêtes chaque politique `quota` a comptées pour chaque tenant - le consommé de son quota, par réplique ; `flexway_quota_exceeded_total` compte les refus, sans tenant |
| `flexway_build_info` | Combien de répliques, et quelle version chacune sert (`version`, `commit`) - pendant un déploiement progressif |
| `flexway_streams_open` | Combien de flux sont ouverts, par `operation` et `protocol` (`sse`, `websocket`, `grpc`, `http`) - jamais dans la latence |
| `flexway_server_in_flight`, `flexway_server_in_flight_limit`, `flexway_server_shedding` | La réplique face à son plafond en vol (déclaré ou dérivé de sa mémoire), et si elle se protège (1) - délesteur ou limite adaptative sous son plafond |
| `flexway_config_last_reload_successful` | 1 si le dernier rechargement de la réplique a été appliqué (ou aucun depuis son démarrage), 0 s'il a été refusé : la console la dit « À surveiller » tant qu'elle sert une configuration en retard |
| `flexway_server_draining` | 1 dès que la réplique draine pour s'arrêter, porté par son dernier envoi : la console la dit « Retirée », pas muette |
| `flexway_circuit_state`, `flexway_circuit_retry_at_seconds`, `flexway_circuit_transitions_total` | L'état du coupe-circuit de chaque amont, lu à la collecte (plus jamais figé sans trafic), son prochain essai, ses transitions (`from`, `to`) - un cycle plus court qu'une collecte compte quand même |
| `flexway_upstream_connections`, `flexway_upstream_errors_total` | Les connexions ouvertes vers chaque amont ; les réponses que la passerelle a écrites pour lui, par `code` (`upstream_timeout`, `upstream_unavailable`, `upstream_error`) |

**Journal d'évènements** - `GET /events`, sur le port d'administration : les derniers
rechargements (avec la cause d'un refus), transitions de coupe-circuit et débuts ou fins de
délestage de **cette** réplique, le plus récent d'abord ; 128 de chaque sorte, en mémoire, perdus au
redémarrage. C'est ce que la vue d'ensemble de la console lit - jamais les journaux.

```bash
curl -s localhost:9090/events | jq '.events[:5]'
```

```bash
curl -s localhost:8428/api/v1/query --data-urlencode 'query=sum(flexway_requests_total)'
# tableau de bord : http://localhost:3000/d/flexway-gateway ; traces : Explore > Tempo, service flexway
```

**Vérif** : une trace traverse edge -> passerelle -> amont **sans rupture de contexte**, même sans
collecteur côté passerelle. Les métriques portent l'opération, jamais le chemin concret : un
`_unmatched` qui grimpe est du trafic vers des routes non déclarées. Aucun log ne contient de
donnée personnelle en clair.

**Lire un arrêt.** Au `SIGTERM`, `readyz` passe au rouge **d'abord** - le temps que l'orchestrateur
retire la réplique -, puis ce qui est en cours se termine, flux compris, dans la limite de
`server.shutdown.drain`. Le journal le dit en une ligne :

```json
{"level":"INFO","msg":"shutting down: readiness withdrawn, draining in-flight requests","readiness_delay":5000000000,"drain":20000000000}
```

Au-delà de `drain`, le processus coupe ce qui reste, le dit (`the drain ran out: the requests still in
flight are cut`) et sort proprement, après sa dernière poussée OTLP : un `terminationGracePeriodSeconds`
Kubernetes - ou un `stop_grace_period` Docker Compose, 10 s par défaut - doit donc dépasser `drain` de
quelques secondes. L'arrêt est annoncé (`flexway_server_draining = 1`) par une poussée **immédiate** au
`SIGTERM` : même un processus tué avant la fin de son drain est dit « Retirée » par la console.

*Métriques et propagation implémentées depuis `0.2.0` ; le tenant est une dimension depuis `0.3.0`.*

**Journal d'accès, échantillonnage** (`1.0.0`) - `observability.access_log: true` écrit une ligne
`access` par requête, avec le **gabarit** de chemin (jamais le chemin reçu). `observability.sampling:
{ max_per_second: 50 }` plafonne les traces racines par réplique, sans couper une trace venue d'amont.

**Niveau des journaux** (`1.1.x`) - `observability.log_level: debug` montre ce que fait chaque
requête (opération, gabarit, amont, statut, durée), sans redémarrer : le niveau suit le
rechargement, et une ligne `log level changed` le dit. Revenir à `info` après le diagnostic.
Chaque ligne porte `trace_id` quand la requête a une trace ; un arrêt au démarrage est une ligne
`FATAL`.

**Simuler** (`1.0.0`) - « que ferait cette configuration ? » sans la déployer, sur le port
d'administration ; rien n'est appelé, débité ni compté :

```sh
curl -s localhost:9090/simulate -d '{"method":"GET","host":"api.example.com","path":"/orders?status=open",
  "identity":{"issuer":"customers","subject":"alice","tenant":"acme","scopes":["orders:read"]}}'
```

La réponse donne le verdict (`served`, ou `refused` et son code), l'opération, la chaîne et la
requête que l'amont recevrait.

**Auditer les décisions** (`1.0.0`) - `audit: { decisions: refusals }` (ou `all`) écrit une ligne
`authz_decision` par décision sur une opération déclarée, sur la sortie standard mais marquée
`log.type: audit` pour que votre pipeline la range à part. Elle nomme l'opération et son gabarit de
chemin (jamais le chemin reçu), le statut et le code de refus, le mécanisme, l'émetteur, le tenant,
les agents, et le sujet **haché**. Chaque ligne porte `seq`, `prev` et `hash` : une ligne retirée ou
modifiée se voit. Avec `audit: { key: "${AUDIT_KEY}" }`, ces empreintes sont des HMAC - sans la clé,
qui peut écrire tout le flux peut aussi le réécrire.

**Jetons opaques** (`1.0.0`) - un fournisseur qui émet des références plutôt que des JWT se déclare
avec `opaque_tokens` (un émetteur au plus, `client` exigé) : chaque jeton est introspecté (RFC 7662)
et soumis aux mêmes règles qu'un JWT ; la réponse est gardée `cache` (30 s par défaut), jamais
au-delà de son `exp` - le délai avant qu'une révocation se voie.
