Syntaxe eventql¶
eventql est le petit langage de requête utilisé pour filtrer les événements de
Xolo, dans l'explorateur (/orgs/{slug}/events) comme dans les règles d'alerte.
Sa syntaxe s'inspire de LogQL de
Grafana Loki : un sélecteur d'étiquettes suivi d'un pipeline de filtres
optionnel.
Le paquet de référence est internal/core/eventql.
{type="auth.login.failed", severity=~"warning|error"} | provider="oidc" |~ "timeout"
└──────────────── sélecteur ────────────────────────┘ └─── filtres du pipeline ───┘
Modèle d'un événement¶
Chaque événement possède un ensemble de champs. Une partie est indexée (les « étiquettes », interrogeables dans le sélecteur) ; le reste est constitué d'attributs clé/valeur libres et du message.
| Champ | Type | Interrogeable via | Description |
|---|---|---|---|
type |
étiquette | sélecteur | Type pointé, ex. auth.login.failed, proxy.request |
source |
étiquette | sélecteur | platform ou le nom du plugin émetteur |
severity |
étiquette | sélecteur | info, warning ou error |
org |
étiquette | sélecteur | ID de l'organisation (souvent déjà contraint par la page) |
user |
étiquette | sélecteur | ID de l'utilisateur concerné (vide = événement global) |
| attributs | clé/valeur | filtre \| clé … |
Paires arbitraires propres à chaque type d'événement |
| message | texte libre | filtres de ligne \|=, \|~, … |
Message lisible de l'événement |
Note. Seules les cinq étiquettes ci-dessus sont autorisées dans le sélecteur. Toute autre clé y provoque une erreur de compilation. Les clés libres se filtrent dans le pipeline (
| clé="valeur").
Sélecteur d'étiquettes¶
Le sélecteur est obligatoire dès qu'il y a une requête, et se place entre accolades. Il contient zéro ou plusieurs matchers séparés par des virgules :
Un sélecteur vide {} (ou une requête entièrement vide) correspond à tous
les événements (dans le périmètre de visibilité courant).
Opérateurs¶
| Opérateur | Signification | Exemple |
|---|---|---|
= |
égal | {type="proxy.request"} |
!= |
différent | {severity!="info"} |
=~ |
correspond à l'expression régulière | {type=~"auth\\..*"} |
!~ |
ne correspond pas à l'expression régulière | {source!~"plugin\\..*"} |
Les valeurs sont toujours entre guillemets doubles.
Pipeline de filtres¶
Après le sélecteur, on enchaîne des étages de filtrage. Il en existe deux familles : les filtres d'attributs et les filtres de ligne (sur le message). Ils peuvent se combiner et se répéter librement ; tous doivent être satisfaits (ET logique).
Filtres d'attributs¶
Introduits par une barre verticale | suivie d'un nom d'attribut, d'un
opérateur et d'une valeur. Les opérateurs sont les mêmes que pour le sélecteur
(=, !=, =~, !~).
Un attribut absent est traité comme une chaîne vide. Ainsi
| model="gpt-4o"exclut les événements sans attributmodel, tandis que| model!="gpt-4o"les inclut (car"" != "gpt-4o").
Filtres de ligne (sur le message)¶
Ils s'appliquent au champ message. Contrairement aux filtres d'attributs, ils
ne sont pas précédés d'un nom.
| Opérateur | Signification | Exemple |
|---|---|---|
\|= |
le message contient la sous-chaîne | \|= "timeout" |
!= |
le message ne contient pas la sous-chaîne | != "success" |
\|~ |
le message correspond à l'expression régulière | \|~ "time.?out" |
!~ |
le message ne correspond pas à l'expression régulière | !~ "^ok" |
Chaînes et échappements¶
Les valeurs sont entre guillemets doubles. Les séquences d'échappement suivantes sont reconnues :
| Séquence | Résultat |
|---|---|
\" |
guillemet double littéral |
\\ |
antislash littéral |
\n |
saut de ligne |
\t |
tabulation |
Expressions régulières¶
Les opérateurs =~ / !~ (étiquettes, attributs et lignes) utilisent le moteur
d'expressions régulières standard de Go
(RE2).
- La correspondance est non ancrée :
=~"error"correspond à toute valeur contenanterror. Utilisez^…$pour une correspondance exacte. - Une expression invalide fait échouer la compilation de la requête (un message d'erreur est affiché dans l'explorateur).
Grammaire (EBNF simplifiée)¶
query := selector pipeline?
selector := '{' ( matcher (',' matcher)* )? '}'
matcher := IDENT op STRING
op := '=' | '!=' | '=~' | '!~'
pipeline := stage*
stage := lineFilter | labelFilter
lineFilter := ('|=' | '|~' | '!=' | '!~') STRING
labelFilter := '|' IDENT op STRING
IDENTcommence par une lettre ou_, suivi de lettres, chiffres,_,.,-ou:.STRINGest une chaîne entre guillemets doubles (voir « Chaînes et échappements »).- Une entrée vide compile vers une requête qui correspond à tout.
Comment la requête est évaluée¶
L'implémentation reste fidèle à l'esprit de Loki : les étiquettes sont indexées, le reste est scanné.
- Les matchers d'étiquettes en
=/!=et la plage temporelle sont poussés dans la requête SQL (colonnes indexées), ce qui borne rapidement le jeu de lignes. - Les filtres d'attributs, les filtres de ligne et toute expression régulière sont évalués en mémoire sur les lignes ainsi présélectionnées.
Cette même requête compilée sert à deux usages : le filtrage dans l'explorateur
et l'évaluation périodique des règles d'alerte (count(fenêtre) sur les
événements correspondants).
Événements de base¶
Événements de la plateforme¶
Émis par Xolo lui-même, avec source="platform".
proxy.request et auth.login.failed sont émis dans le chemin de requête. Les
événements de configuration (CRUD sur les entités) sont émis par des
décorateurs de stores : toute création / modification / suppression via
n'importe quel appelant (UI ou API) produit l'événement correspondant, sans
duplication de code dans les handlers.
Activité & authentification
type |
severity |
Portée | Émis quand | Attributs |
|---|---|---|---|---|
proxy.request |
info |
utilisateur | Une requête proxy aboutit | model, auth_token_id, prompt_tokens, completion_tokens, total_tokens |
auth.login.failed |
warning |
globale | Une connexion échoue (ex. compte déjà existant avec un autre fournisseur) | email, provider, reason |
pipeline.trace |
info (configurable) |
utilisateur | Un nœud trace d'un pipeline s'exécute |
node_id, label, port.<nom> pour chaque port connecté |
request.blocked |
warning |
utilisateur | Un nœud block d'un pipeline refuse la requête |
node_id, label, reason |
Configuration (CRUD) — tous globaux à l'organisation (user vide), avec les
attributs actor et actor_id (l'utilisateur ayant effectué l'action) en plus
de ceux listés :
| Entité | type (created / updated / deleted) |
Attributs |
|---|---|---|
| Fournisseur | provider.created · provider.updated · provider.deleted |
provider_id, provider_name |
| Modèle | model.created · model.updated · model.deleted |
model_id, model_name |
| Modèle virtuel | virtual-model.created · virtual-model.updated · virtual-model.deleted |
virtual_model_id, virtual_model_name |
| Middleware | middleware.created · middleware.updated · middleware.deleted |
middleware_id, middleware_name |
| Rôle | role.created · role.updated · role.deleted |
role_id, role_name |
| Application | application.created · application.updated · application.deleted |
application_id, application_name |
| Jeton d'application | application-token.created · application-token.deleted |
token_id, label, application_id, application_name |
| Invitation | invite.created · invite.deleted |
invite_id, role, email |
| Membre | member.added · member.updated · member.removed |
membership_id, member_user_id (+ role_count pour updated) |
Sévérité :
created/updated/added→info;deleted/removed→warning. Les rôles builtin (owner/admin/member) ne produisent pas d'événement.Les événements de configuration étant globaux (
uservide), ils ne sont visibles qu'avec la portée « Tous » ou « Globaux » de l'explorateur (permissionevents:read:all). Ils ne sont émis que pour des actions initiées par un utilisateur (le seeding système, les migrations et les tests n'en génèrent pas).
Événements des plugins¶
Émis par les plugins. L'hôte force source au nom du plugin et préfixe le type
par plugin.<nom>. (voir le tutoriel plugin, section Emitting Events).
type |
severity |
Émis quand | Attributs |
|---|---|---|---|
plugin.pseudonymizer.sensitive-data.detected |
warning |
Le plugin pseudonymizer détecte/pseudonymise des données sensibles |
entities, types (ex. EMAIL:2,PER:1), removed_attachments, language |
plugin.pseudonymizer.sensitive-data.leak |
error |
Le mode strict du plugin pseudonymizer détecte une fuite résiduelle après anonymisation |
leak_count, leak_<type> |
plugin.pseudonymizer.attachment.blocked |
warning |
Le plugin pseudonymizer refuse une requête portant une pièce jointe non pseudonymisable |
attachments, reasons |
plugin.pseudonymizer.request.blocked |
error |
Le plugin pseudonymizer refuse une requête en stratégie hash faute de clé HMAC exploitable sur le nœud (fail-closed) |
reason, error, strategy |
plugin.pseudonymizer.passthrough |
error |
Le plugin pseudonymizer laisse passer une requête sans la pseudonymiser : configuration invalide, modèle NER indisponible (hors-ligne, cache vide), échec d'anonymisation d'un contenu |
reason, error, contents |
plugin.time-restriction.request.blocked |
warning |
Le plugin time-restriction bloque une requête hors des plages autorisées |
reason |
Pour cibler l'ensemble des événements de plugins : {type=~"plugin\\..*"} (ou
{source!="platform"}).
Exemples¶
| Requête | Intention |
| ---------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------- |
| {} | Tous les événements visibles |
| {type="proxy.request"} | Toutes les requêtes proxy |
| {severity=~"warning | error"} | Tout ce qui n'est pas informatif |
| {type="auth.login.failed"} \| provider="oidc" | Échecs de connexion via OIDC |
| {source!="platform"} | Uniquement les événements émis par des plugins |
| {type=~"provider\\..*"} | Créations / suppressions de fournisseurs |
| {type="proxy.request"} \| model=~"gpt-4.*" \|~ "timeout" | Requêtes vers un modèle GPT‑4 dont le message mentionne un timeout |
| {} \| actor_id="c8p2..." | Événements de configuration déclenchés par un acteur donné |
Portée des alertes¶
Une alerte a une portée qui détermine les événements qu'elle évalue :
| Portée | Évalue | Permission requise |
|---|---|---|
org |
Tous les événements de l'organisation | events:write |
personal |
Uniquement les propres événements du créateur | events:alerts:own |
La portée est déduite des droits à la création : un utilisateur disposant de
events:write crée des alertes d'organisation ; sinon (avec events:alerts:own)
il crée des alertes personnelles sur ses propres événements. Chacun ne voit et ne
gère que les alertes et incidents qui le concernent (les alertes personnelles
sont privées à leur propriétaire). Consulter ses propres événements dans
l'explorateur ne requiert aucune permission particulière.
Exemple d'usage dans une alerte¶
Règle « brute-force » : plus de 5 échecs de connexion sur 5 minutes.
Requête : {type="auth.login.failed"}
Agrégation : count
Fenêtre : 5m
Condition : > 5
Portée : org (nécessite events:write)
Exemple d'alerte personnelle : un utilisateur veut être alerté au-delà de
100 requêtes proxy sur une heure (count(1h) > 100 sur {type="proxy.request"},
portée personal) — l'alerte n'évalue que ses propres requêtes.