Configuration
Chaque réglage est une option, et chaque option a une variable d’environnement de même sens. Les quelques réglages trop structurés pour une ligne de commande vivent dans un seul fichier JSON. Cette page les couvre tous.
Deux façons de dire la même chose
Lancez mcpd sans argument et il fonctionne : il écoute sur
0.0.0.0:4317, garde les données dans ./mcpd.db,
masque les secrets et conserve chaque trace indéfiniment. Tout ce qui suit
change l’une de ces décisions.
Une option l’emporte toujours sur sa variable d’environnement. Les deux sont globales : elles sont acceptées avant comme après une sous-commande.
Référence
| Option | Variable | Valeur par défaut |
|---|---|---|
--listen | MCPD_LISTEN | 0.0.0.0:4317 |
--data | MCPD_DATA | ./mcpd.db |
--project | MCPD_PROJECT | default |
--retention-days | MCPD_RETENTION_DAYS | non définie : tout est conservé |
--ingest-key | MCPD_INGEST_KEYS | aucune : l’ingestion est ouverte |
--read-key | MCPD_READ_KEYS | aucune : la lecture est ouverte |
--write-key | MCPD_WRITE_KEYS | aucune : les clés d’ingestion sont acceptées |
--admin-key | MCPD_ADMIN_KEYS | aucune : les clés de lecture sont acceptées |
--execute-key | MCPD_EXECUTE_KEYS | aucune : les clés d’écriture sont acceptées |
--worker-key | MCPD_WORKER_KEYS | aucune : les clés d’écriture sont acceptées |
--runs-data | MCPD_RUNS_DATA | le chemin de --data avec .runs avant son extension |
--trusted-identity-header | MCPD_TRUSTED_IDENTITY_HEADER | aucun : aucun sujet n’est enregistré |
--trusted-proxy-cidr | MCPD_TRUSTED_PROXY_CIDRS | aucun : aucune adresse ne peut poser cet en-tête |
--sample | MCPD_SAMPLE | non défini : toutes les traces sont gardées |
--quota-per-minute | MCPD_QUOTA_PER_MINUTE | non défini : pas de plafond |
--no-redact | MCPD_NO_REDACT | inactif : le masquage est actif |
--redact-key | MCPD_REDACT_KEY | une clé intégrée, qui est publique |
--redact-key-contains | MCPD_REDACT_KEY_CONTAINS | aucune : les règles intégrées seules |
--redact-max-string | MCPD_REDACT_MAX_STRING | 128 KiB |
--cors-origin | MCPD_CORS_ORIGINS | aucune : même origine seulement |
--request-timeout-seconds | MCPD_REQUEST_TIMEOUT_SECONDS | 30 |
--max-body-bytes | MCPD_MAX_BODY_BYTES | 33554432, soit 32 MiB |
--max-batch-signals | MCPD_MAX_BATCH_SIGNALS | 10000 |
--log-format | MCPD_LOG_FORMAT | text |
--clickhouse-url | MCPD_CLICKHOUSE_URL | inactif : le fichier embarqué est utilisé |
--clickhouse-database | MCPD_CLICKHOUSE_DATABASE | mcpd |
--clickhouse-user | MCPD_CLICKHOUSE_USER | aucun |
--clickhouse-password | MCPD_CLICKHOUSE_PASSWORD | aucun |
--postgres-url | MCPD_POSTGRES_URL | inactif : le fichier embarqué est utilisé |
--config-file | MCPD_CONFIG_FILE | aucun |
--forward-endpoint | MCPD_FORWARD_ENDPOINT | inactif : rien n’est transféré |
--forward-header | MCPD_FORWARD_HEADERS | aucun |
| aucune | MCPD_LOG | mcpd=info,mcpd_server=info,tower_http=warn |
--listen
L’adresse à écouter. Un seul port sert les endpoints OTLP, l’endpoint d’ingestion natif, l’API de requête, les sondes, les métriques et l’interface : c’est donc la seule adresse que mcpd possède.
Écouter sur une adresse de bouclage est le choix discret : mcpd n’avertit
d’une ingestion ouverte que si l’adresse écoutée n’en est pas une. Sur
0.0.0.0 sans aucune clé, il avertit deux fois au démarrage, une
fois pour l’ingestion et une fois pour la lecture.
--data
Chemin du fichier SQLite. Les répertoires parents sont créés s’ils
n’existent pas. Passez :memory: pour un stockage qui ne laisse
rien derrière lui, ce qui convient à une démonstration ou à un test et ne
convient pas à ce que vous voudrez regarder demain.
--project
Le projet dans lequel arrive un signal quand l’émetteur n’en a nommé aucun.
Un projet est la frontière de cloisonnement : chaque requête est limitée à
un seul. La passe de conservation s’applique à lui et à chaque projet du
bloc projects ; un projet qui ne figure ni dans l’un ni dans
l’autre n’est jamais élagué, ce qui compte si vos émetteurs nomment
eux-mêmes le leur.
--retention-days
Supprime les signaux plus vieux que ce nombre de jours. Non définie, elle conserve tout, ce qui est le bon défaut sur un portable et, partout ailleurs, une décision à prendre volontairement.
La fenêtre va de 1 à 36500 jours, ici comme dans le
retention_days d’un projet. Zéro est refusé au démarrage au
lieu d’être lu comme une absence de rétention : il placerait la limite à
maintenant, et la première passe tourne au démarrage, donc il supprimerait
tous les signaux du projet. Pour tout conserver, ne définissez pas le
réglage.
La passe tourne toutes les heures plutôt qu’au moment de la requête : une grosse suppression ne tombe donc jamais à l’intérieur de la requête d’un utilisateur. Les spans, les traces, les logs et les métriques sont coupés selon la date à laquelle mcpd les a reçus ; les scores, les retours et les notes d’aspect selon leur propre horodatage. Supprimer des lignes ne réduit pas la taille du fichier. Voir Exploitation.
--ingest-key, --read-key, --write-key, --admin-key, --execute-key et --worker-key
Six portées, parce qu’elles portent des risques réellement différents.
Chaque endpoint déclare celle dont il a besoin dans
spec/mcpd.spec.json, son gestionnaire est marqué en
conséquence, et make check compare les deux : la portée
documentée ici est bien celle qu’applique le serveur en marche.
| Portée | Définie par | Ce qu’elle ouvre |
|---|---|---|
ingest | --ingest-key | Les quatre endpoints d’ingestion. Ne lit rien. |
read | --read-key | L’API de requête : traces, spans, logs, métriques, scores, analyses. |
write | --write-key | Les scores, les retours, les datasets, les expériences, les acquittements, et la capture d’une exécution dans un dataset. |
admin | --admin-key | Supprimer une trace, lire le journal d’audit, et tout ce qu’ouvrent les deux portées ci-dessous. |
execute | --execute-key | Démarrer, annuler et signaler une exécution, et lui publier un événement. |
worker | --worker-key | Enregistrer des définitions, réclamer des étapes et rapporter leur issue. Ne lit aucune trace. |
La séparation qui compte le plus est celle entre ingest et tout
le reste. Une clé d’ingestion vit dans chaque processus qui émet un span, y
compris ceux qui exécutent du code que vous n’avez pas écrit. Donner à cette
population une clé qui relit aussi chaque prompt stocké est une erreur que
mcpd ne rend pas facile : une clé d’ingestion présentée à l’API de requête
est refusée.
write est séparée d’ingest pour la même raison, à
plus petite échelle. Capturer une exécution dans un dataset lit
l’entrée et la sortie de cette exécution : c’est une lecture déguisée en
écriture, et elle n’a pas sa place sur la clé que porte votre flotte.
admin est séparée de read parce que supprimer une
exécution n’est pas la lire. Tant que vous ne posez pas
--admin-key, une clé de lecture supprime autant qu’elle lit, et
mcpd le dit au démarrage.
execute et worker sont les deux portées de la
surface d’exécution. Démarrer une exécution fait travailler le worker de
quelqu’un et dépense de l’argent, ce que l’annotation d’une trace ne fait
pas : ce n’est donc pas write. Une clé de worker vit dans le
processus qui exécute le corps des étapes, celui qui détient d’habitude la
clé du fournisseur : elle ouvre le protocole de bail et rien qui lise un
prompt stocké. Tant que vous ne posez pas les deux options, les clés
d’écriture sont acceptées pour les deux, et une clé d’administration l’est
toujours.
Mettre à niveau sans interruption
Un --write-key non défini accepte les clés d’ingestion, et un
--admin-key non défini accepte les clés de lecture. Un
déploiement configuré avant l’existence de ces deux portées garde donc
exactement la portée qu’il avait, et rien ne casse à la mise à niveau. Posez
les deux options pour prendre les droits plus étroits, et
mcpd config vous dit dans quel état vous êtes : il affiche
key required quand la portée a ses propres clés, et
key required (ingest keys) ou
key required (read keys) quand elle les hérite.
Répétez n’importe laquelle des six options pour plusieurs clés : c’est
ainsi qu’on en remplace une sans interruption. Les variables
d’environnement prennent une liste séparée par des virgules. Un appelant
présente une clé sous la forme Authorization: Bearer KEY ou
X-Mcpd-Key: KEY, et la comparaison se fait en temps constant :
ni la longueur ni la position de la première différence ne sont observables.
Une clé que mcpd ne connaît pas donne un 401. Une clé que mcpd
connaît, utilisée sur un endpoint que sa portée ne couvre pas, donne un
403. La distinction vaut la peine d’être tenue : un 401 vous
envoie chercher une faute de frappe dans une clé parfaitement correcte.
Sans aucune clé configurée, toutes les portées sont ouvertes. C’est volontaire, parce que le premier lancement ne doit exiger aucune clé, et c’est la raison d’être de l’avertissement au démarrage.
Notez ce que les clés ne couvrent pas : /healthz,
/readyz, /metrics et l’interface elle-même sont
servis sans clé dans cette version. Placez-les derrière votre propre ingress
si cela compte pour vous.
Une entrée vide est écartée avant que quoi que ce soit lise la liste. Une liste définie qui ne contient aucune clé, comme MCPD_EXECUTE_KEYS= ou une valeur vide dans un Secret Kubernetes, empêche le démon de démarrer, et le refus nomme l’option et la variable ; omettez la variable pour laisser la portée non définie, et une requête qui envoie un en-tête de clé vide ne présente aucune clé. Une clé qui est encore un exemple des fichiers de deploy/, toute valeur commençant par REPLACE_WITH ou replace-me en majuscules ou en minuscules, empêche le démon de démarrer ; le refus nomme l’option ou la liste du projet, jamais la valeur. Il en va de même pour --redact-key. Générez chaque clé avec openssl rand -hex 32. Sans --write-key, --execute-key ni --worker-key, les clés d’ingestion lancent aussi des exécutions et réclament des étapes, et mcpd le signale au démarrage et dans mcpd doctor.
Le journal d’audit
Les gestionnaires qui renvoient des payloads enregistrent qui a lu quoi :
GET /v1/api/traces/{id}, les recherches de spans et de logs,
les retours, les éléments d’un dataset, la capture et la suppression.
Chaque ligne porte l’heure, une empreinte courte de la clé qui a agi,
l’action, le projet, la ressource et si l’autorisation l’a permis. La clé
elle-même n’est jamais notée.
Lisez le journal avec GET /v1/api/audit, qui demande la portée
admin. Il n’est pas élagué par --retention-days :
un journal qui se supprimerait au même rythme que les données qu’il décrit
ne pourrait pas dire qui a lu les données désormais parties. Écrire une
ligne ne fait jamais échouer la lecture qu’elle décrit ; une ligne qui ne
peut pas être écrite est signalée et comptée dans
mcpd_audit_write_failures_total sur /metrics, ce
qui mérite une alerte.
--sample
Une fraction entre 0.0 et 1.0. Une valeur hors de cet intervalle est une
erreur au démarrage, pas un rognage silencieux : --sample must be
between 0.0 and 1.0, got 2.
La décision est prise en hachant l’identifiant de trace : une flotte d’instances mcpd derrière un répartiteur de charge garde et jette donc les mêmes traces sans se parler. Elle est prise une fois par trace et héritée par chaque span de cette trace dans le lot, parce qu’une trace enregistrée à moitié ressemble à un bogue dans le système que vous observez.
Le hachage lit les derniers octets de l’identifiant de trace. Certains producteurs dérivent les premiers octets d’un horodatage, et un échantillonneur qui les regarderait se corrélerait à l’heure de la journée et jetterait des heures entières sans rien en dire.
Le type de politique porte aussi « toujours garder les erreurs » et « toujours garder au-delà de », mais cela relève d’une décision en queue, prise une fois la trace terminée. Cette version n’échantillonne qu’en tête, et aucune option n’active l’échantillonnage en queue. Échantillonnage détaille le compromis.
--no-redact, --redact-key, --redact-key-contains et --redact-max-string
Le masquage est actif par défaut et tourne avant toute écriture : une valeur qui n’aurait jamais dû être persistée ne l’est jamais. La supprimer ensuite n’est pas la même garantie. Trois jeux de règles sont intégrés, comparés à des sous-chaînes en minuscules du nom d’une clé :
| Règle | Appliquée quand le nom de la clé contient | Ce qui est stocké |
|---|---|---|
| Masque | api_key, apikey, api-key,
secret, password, passwd,
access_token, refresh_token,
id_token, bearer,
authorization, auth_token,
private_key, client_secret,
session_token, credential,
signature, x-api-key | [redacted] |
| Partiel | email, phone, ssn,
credit_card, card_number,
iban |
Les deux premiers et les deux derniers caractères,
ab…yz. Une valeur de six caractères ou moins est
remplacée entièrement plutôt que de laisser fuir presque tout
d’elle-même
|
| Hachage | user_id, userid | sha256: et 12 octets d’une empreinte à clé, pour que
vous puissiez encore grouper par utilisateur sans en stocker un
|
Une clé qui correspond et qui contient un objet ou un tableau voit aussi
tout ce qui est en dessous masqué :
{"credentials": {"token": "..."}} ne passe donc
pas au travers sur le seul nom imbriqué. Toute chaîne de plus de 128 KiB est
tronquée avec un marqueur donnant le nombre d’octets, ce qui borne l’effet
d’un prompt emballé sur votre disque.
--redact-key est la clé de la règle de hachage. Sous une même
clé, des entrées égales donnent la même empreinte, et c’est ce qui fait
marcher le regroupement ; changer la clé rompt ce lien volontairement.
Posez-la dans tout déploiement qui hache : le défaut intégré est dans le
code source, donc des empreintes non reclés sont devinables.
--redact-key-contains ajoute votre propre règle de masquage
par-dessus les trois règles intégrées. Les règles intégrées connaissent les
noms sous lesquels on range d’habitude des secrets ; elles ne connaissent
pas le champ où vos propres outils mettent le numéro de compte d’un client.
La valeur est comparée comme le sont les règles intégrées, en sous-chaîne
minuscule du nom de la clé : --redact-key-contains ticket masque
donc ticket_id comme supportTicket, et avec eux
tout ce qui est imbriqué sous une clé qui correspond. Répétez l’option pour
en avoir plusieurs, ou donnez à MCPD_REDACT_KEY_CONTAINS une
liste séparée par des virgules.
--redact-max-string abaisse le plafond de longueur depuis ses
128 KiB intégrés. Une chaîne plus longue que le plafond est coupée là, avec
un marqueur disant combien d’octets ont été jetés, avant d’être stockée.
--no-redact éteint toute la chaîne, plafond de longueur
compris. En variable d’environnement, c’est
MCPD_NO_REDACT=true. Cela n’a de sens que si quelque chose en
amont masque déjà, et le daemon refuse de démarrer si l’une des deux
options ci-dessus est posée à côté, parce qu’elles n’auraient rien sur quoi
s’appliquer.
--runs-data
Où vit le stockage des exécutions : les définitions, le journal des
tentatives d’étape, chaque échéance et chaque attente. C’est un fichier
embarqué quel que soit le stockage des signaux : sur Postgres ou ClickHouse,
les exécutions tiennent toujours dans un fichier à côté du daemon, et la
surface d’exécution est servie par une seule réplique. Par défaut, c’est le
chemin de --data avec .runs avant l’extension, soit
./mcpd.runs.db pour le défaut, et :memory: quand
--data l’est. --runs-data off ne sert aucune
exécution : chaque endpoint d’exécution répond 503 en nommant
cette option, et le reste du daemon est inchangé.
Les délais du stockage sont dans le fichier de configuration sous
runs, en millisecondes entières : lease_ttl_ms,
reclaim_window_ms, stall_window_ms,
idempotency_window_ms, park_ttl_ms et
max_depth. Les valeurs par défaut, un bail de 30 secondes et
une fenêtre de reprise de 30 secondes, conviennent à un daemon sur une
machine, et ce sont celles qu’un worker relit quand il s’enregistre.
--cors-origin
Quelles origines de navigateur peuvent appeler l’API de requête depuis un
autre site. Sans aucune, une requête d’origine croisée n’obtient pas
d’en-tête access-control-allow-origin et le navigateur refuse
de remettre la réponse à la page. C’est le défaut, et c’est ce qui empêche
un daemon sur votre portable, avec une API de lecture ouverte, d’être lu par
n’importe quelle page web ouverte dans le même navigateur. L’interface
fournie est servie depuis l’origine du daemon lui-même et n’a jamais besoin
de cette option.
Servir l’interface depuis ailleurs, un CDN ou un portail interne, est le cas
pour lequel cette option existe : nommez cette origine, schéma et hôte,
par exemple --cors-origin https://console.example. Répétez-la
pour en avoir plusieurs, ou donnez à MCPD_CORS_ORIGINS une
liste séparée par des virgules. Une valeur qui n’est pas une origine,
console.example ou une valeur finissant par une barre oblique,
refuse le démarrage en nommant l’option, plutôt que de vous laisser avec une
interface incapable de se connecter et aucune raison à cela.
--cors-origin '*' autorise toutes les origines, ce que les
versions antérieures faisaient sans rien demander.
/healthz, /readyz et /metrics ne sont
pas régis par cette option. Ils ne portent aucune donnée stockée, et
l’interface sonde /healthz avant toute autre chose : ils
restent donc lisibles depuis n’importe quelle origine.
--request-timeout-seconds
La durée maximale d’une requête, lecture du corps comprise, avant que le
daemon ne réponde à sa place. Un gestionnaire qui dépasse l’échéance reçoit
une réponse 503 dont le corps porte timeout et le
nom de cette option. Le défaut est de 30 secondes, assez long pour qu’un
gros lot soit lu et stocké, et assez court pour qu’un backend de stockage
qui accepte les connexions et ne répond jamais soit signalé plutôt
qu’attendu. On ne peut pas le désactiver ; relevez-le si une opération a
réellement besoin de plus.
--max-body-bytes et --max-batch-signals
Les deux bornes d’une requête. --max-body-bytes est le plus
gros corps que le daemon lit, 32 MiB par défaut, et c’est ce qui décide de
la mémoire qu’une requête peut prendre avant qu’aucune part n’en ait été
décodée. Un corps plus gros est refusé par un 413 qui nomme les
deux nombres, et un corps compressé est tenu au même chiffre une fois
décompressé : un corps qui gonfle d’un facteur mille est donc refusé plutôt
que gonflé.
--max-batch-signals est le nombre de signaux qu’une requête peut
porter une fois décodée, 10 000 par défaut. Il est compté sur toute la
requête : un tableau de lots et un flux NDJSON sont tenus au même nombre
qu’un lot unique. Rien n’est stocké quand une requête le dépasse : le refus
vient avant la première écriture, pas après la moitié.
Les deux sont aussi des clés du fichier de configuration,
max_body_bytes et max_batch_signals, et l’option
l’emporte là où les deux sont posées. Aucune ne peut être désactivée, et
zéro est refusé au démarrage plutôt que lu comme « pas de limite » : un
plafond de corps à zéro refuse toutes les requêtes, donc la lecture qui
paraît généreuse est celle qui met l’ingestion à terre.
--log-format et MCPD_LOG
text pour un terminal, json pour un pipeline de
logs. MCPD_LOG prend une chaîne de filtre tracing
et n’a pas d’option, parce que c’est une commande de débogage plutôt qu’une
décision de déploiement. Mettez-la à mcpd_server=debug quand
vous voulez voir le détail requête par requête.
Où s’appliquent les deux limites de requête
Le plafond de corps s’applique à toute route qui lit un corps, l’API de requête comprise. Le plafond de signaux s’applique aux portes d’ingestion, et voici ce qu’il compte à chacune d’elles.
| Porte | Ce que compte le plafond de signaux |
|---|---|
POST /v1/ingest | Chaque span, log, métrique, score, retour et note d’aspect de la requête, additionnés sur l’ensemble, que cela soit arrivé en un lot, en tableau de lots ou en flux de lignes NDJSON |
POST /v1/traces | Les spans envoyés par l’émetteur, comptés avant leur conversion, ce qui est le nombre sur lequel l’émetteur peut agir |
POST /v1/logs | Les enregistrements de log de l’export |
POST /v1/metrics | Les points de données de l’export, sur toutes les métriques qu’il contient |
Sous-commandes
| Commande | Ce qu’elle fait |
|---|---|
mcpd serve | Lance le daemon. C’est ce qui se passe sans aucune sous-commande |
mcpd demo | Écrit une exécution d’exemple réaliste dans le stockage, puis sert, pour que l’interface ait quelque chose dedans la première fois que vous l’ouvrez |
mcpd config | Affiche la configuration que cette invocation utiliserait, puis quitte |
mcpd mcp | Sert le Model Context Protocol sur stdio, pour qu’un agent puisse lire ses propres traces depuis un daemon en marche |
mcpd doctor | Note ce déploiement et rapporte ce qui est mal configuré. Sort avec le code 2 en cas de faute, pour pouvoir bloquer un déploiement. Voir Exploitation |
mcpd config existe pour la discussion que vous êtes sur le point
d’avoir avec un conteneur. Options, variables d’environnement et valeurs par
défaut finissent toutes au même endroit, et ceci affiche cet endroit sans
démarrer de serveur ni toucher au fichier de données :
$ MCPD_INGEST_KEYS=k1,k2 mcpd --retention-days 30 --sample 0.1 configlisten 0.0.0.0:4317storage sqlite at ./mcpd.dbconfig file nonelog format textproject defaultprojects noneretention 30 daysingest auth key requiredread auth openwrite auth key required (ingest keys)admin auth openexecute auth key required (ingest keys)worker auth key required (ingest keys)runs sqlite at ./mcpd.runs.dbrun settings lease_ttl_ms 30000, reclaim_window_ms 30000, stall_window_ms 300000, idempotency_window_ms 86400000, park_ttl_ms 86400000, max_depth 8, max_step_repeats 16identity offsampling deterministic 0.1quota unlimitedredaction onredact key built in, which is public; set --redact-keycors same-origin onlytimeout 30s per requestlimits 32 MiB per request, 10000 signals per batchderived duration and token metricsaspects offmonitors noneobjectives nonejudge offforwarding off
Lisez les six lignes d’authentification avant de déployer quoi que ce
soit. open veut dire ouvert à qui peut atteindre le port. Une
ligne entre parenthèses nomme la liste plus large dont cette portée hérite,
et c’est l’état dans lequel démarre un déploiement mis à niveau depuis une
version plus ancienne.
Un déploiement configuré
Rien ici n’est propre à mcpd : les variables sont de l’environnement ordinaire, elles marchent donc pareil dans Compose, dans un manifeste Kubernetes et dans une unité systemd.
compose
services:
mcpd:
image: ghcr.io/mcpd/mcpd
ports: ['4317:4317']
volumes: ['mcpd-data:/data']
environment:
MCPD_LISTEN: 0.0.0.0:4317
MCPD_DATA: /data/mcpd.db
MCPD_PROJECT: checkout
MCPD_RETENTION_DAYS: '30'
MCPD_INGEST_KEYS: write-key-one,write-key-two
MCPD_READ_KEYS: read-key
MCPD_REDACT_KEY: a-long-random-string
MCPD_LOG_FORMAT: json
MCPD_LOG: mcpd=info,mcpd_server=warn
volumes:
mcpd-data: Les mêmes réglages en options, pour un shell ou un fichier d’unité :
mcpd --listen 0.0.0.0:4317 --data /data/mcpd.db --retention-days 30 --ingest-key $KEY --log-format json Ce que lisent les SDK
Ces variables sont lues par le processus de votre agent, pas par le daemon. Elles sont listées ici parce que les deux ensembles partagent des noms, et qu’un conteneur qui fait tourner un agent et mcpd est configuré par un seul environnement.
| Variable | Valeur par défaut |
|---|---|
MCPD_ENDPOINT | http://localhost:4317 |
MCPD_API_KEY | non définie |
MCPD_PROJECT | default |
MCPD_ENVIRONMENT | non définie |
MCPD_SERVICE_NAME | non définie |
MCPD_SERVICE_VERSION | non définie |
MCPD_SAMPLE | 1.0 |
MCPD_ENABLED | true |
MCPD_CAPTURE_PAYLOADS | true |
MCPD_LOG_LEVEL | debug |
MCPD_PROJECT et MCPD_SAMPLE veulent dire la même
chose des deux côtés, ce qui est en général ce que vous voulez : posez-les
une fois et l’agent et le daemon s’accordent sur le projet.
L’échantillonnage est celui auquel il faut réfléchir. Un SDK jette une trace
avant de l’envoyer, ce qui économise le réseau. Le daemon la jette avant de
la stocker, et c’est le seul endroit où le taux peut être imposé à des
émetteurs que vous ne contrôlez pas. Posez le taux à un seul endroit, pas
aux deux, sinon vous filtrez un flux déjà filtré.
MCPD_ENABLED=false fait de chaque appel du SDK une opération
vide qui renvoie tout de même des objets valides : mcpd peut donc être
éteint dans les tests sans changer une ligne de code.
MCPD_CAPTURE_PAYLOADS=false garde les prompts et les complétions
dans le processus : les spans arrivent toujours avec leur chronologie, leur
consommation, leur modèle et leurs erreurs, mais input et
output sont jetés avant d’être mis en file, si bien qu’ils ne
traversent jamais le réseau. Le masquage sur le daemon est l’autre bout du
même fil, et le mauvais bout pour une équipe qui ne doit pas envoyer ces
octets du tout. Le détail par langage est sur chaque
page de SDK.
--config-file
Un fichier JSON qui tient les réglages qui n’entrent pas sur une ligne de commande : les moniteurs, les objectifs de niveau de service, l’intervalle auquel ils sont évalués, le transfert, le juge, les délais du stockage des exécutions, les deux limites de requête et le bloc par projet ci-dessous. JSON plutôt que YAML ou TOML parce que mcpd analyse déjà du JSON pour tout le reste : le fichier n’ajoute donc aucune dépendance ni aucune seconde syntaxe à apprendre. Il se monte tel quel en ConfigMap Kubernetes.
max_body_bytes et max_batch_signals sont ici en
plus de la ligne de commande, pour qu’une flotte les garde à côté des
moniteurs avec lesquels elle est déployée. Une option bat le fichier, et
c’est ainsi qu’une instance prend une borne différente sans que le fichier
partagé soit modifié pour tout le monde.
Une clé inconnue fait échouer le démarrage au lieu d’être ignorée, parce qu’une faute de frappe qui ne surveille silencieusement rien est le pire résultat possible.
Il en va de même d’une valeur avec laquelle un minuteur d’arrière-plan ne
peut pas tourner : un objective qui n’est pas au moins 0 et
inférieur à 1, un window_days d’objectif hors de 1 à 365, un
monitor_interval_seconds au-delà de 86400, un
aspects.window_seconds hors de 1 à 31536000, un
aspects.interval_seconds au-delà de 604800 et un
judge.timeout_seconds hors de 1 à 300. Chaque refus nomme la
clé et la plage.
{
"max_body_bytes": 33554432,
"max_batch_signals": 10000,
"monitor_interval_seconds": 60,
"monitors": [
{
"id": "error-rate",
"name": "Error rate above 5%",
"project": "checkout",
"watch": "error_rate",
"compare": "above",
"threshold": 0.05,
"window_seconds": 300,
"min_samples": 20,
"webhook": "https://hooks.example.com/mcpd"
}
],
"forward": {
"endpoint": "https://otlp.example.com",
"headers": [["Authorization", "Bearer sk-..."]],
"batch_size": 256,
"enabled": true
}
} Les projets, et les clés qui les atteignent
Un projet est la clé de cloisonnement. Lister des clés sous l’un d’eux est ce qui rend cela vrai plutôt que décoratif : une clé posée dans le bloc d’un projet atteint ce projet et aucun autre, si bien qu’un seul daemon sert plusieurs équipes sans qu’aucune puisse lire les prompts des autres.
{
"projects": {
"checkout": {
"ingest_keys": ["ck-write-..."],
"read_keys": ["ck-read-..."],
"admin_keys": ["ck-admin-..."],
"retention_days": 30,
"sample": 0.25
},
"support": {
"ingest_keys": ["sp-write-..."],
"read_keys": ["sp-read-..."],
"retention_days": 90
}
}
}
Les six listes existent par projet, sous ingest_keys,
read_keys, write_keys, admin_keys,
execute_keys et worker_keys,
et elles se rabattent de la même façon : un projet sans
write_keys accepte ses propres clés d’ingestion, et un projet
sans admin_keys accepte ses propres clés de lecture. Le bloc
support ci-dessus ne pose ni l’une ni l’autre : il se comporte
donc exactement comme avant l’existence des deux portées.
Une clé posée par l’une des six options de premier niveau atteint tous les projets. C’est le chemin mono-locataire, et il reste le chemin simple : si vous n’ouvrez jamais ce bloc, vous ne rencontrez jamais rien de tout cela.
retention_days et sample se rabattent sur les
options globales quand un projet ne les pose pas. Les deux sont par projet
pour la même raison que les clés : l’exigence de quatre-vingt-dix jours d’une
équipe ne doit pas forcer la conservation des données de tout le monde, et le
trafic d’une équipe ne doit pas décider de l’échantillonnage d’une autre.
Un lot peut nommer plusieurs projets. Une clé d’ingestion cloisonnée écrit les signaux qu’elle atteint et refuse les autres, en signalant le refus dans la réponse au lieu de les jeter en silence : une flotte pointée vers le mauvais projet est donc visible depuis la réponse qu’elle lit déjà.
Une requête de lecture sur un projet que votre clé n’atteint pas est refusée sans dire si ce projet existe. Sur un daemon partagé, les noms de projet sont souvent des noms d’équipes ou de clients.
Les moniteurs et les objectifs de niveau de service sont traités dans
Moniteurs et alertes ; les objectifs
vont dans un tableau slos à côté de monitors et sont
évalués au même intervalle. Le bloc judge active la notation
par modèle et est traité dans
Tests de régression. Il est inactif
tant que vous ne le posez pas, et un scorer par juge ne tourne jamais à moins
qu’une requête ne le nomme, parce que noter dépense de l’argent à chaque
exécution lue.
{
"judge": {
"endpoint": "http://localhost:11434/v1",
"model": "qwen2.5:14b",
"api_key": "",
"criteria": {
"answer_relevance": "Does the answer address the question that was asked?"
}
}
} monitor_interval_seconds fixe la fréquence d’évaluation des moniteurs et des objectifs. Il vaut 60 par défaut, et une valeur sous 5 est relevée à 5.
derived_metrics
Les métriques que le démon dérive des spans qu’il stocke. Les deux moitiés sont actives quand le bloc est absent. durations écrit un point de durée pour chaque span terminé d’agent, d’outil, de workflow, de génération de modèle ou de processeur. tokens écrit jusqu’à treize compteurs de tokens pour chaque span de génération de modèle qui a déclaré son usage. Mettez l’une ou l’autre à false pour cesser de l’écrire ; un champ omis reste actif. Aucune option ne le règle. Les points dérivés sont ajoutés après le plafond par projet et ne comptent pas dans le nombre accepted d’une réponse d’ingestion.
{
"derived_metrics": { "durations": true, "tokens": false }
} aspects
Si le démon regroupe les notes d’aspect en thèmes, et sous quelles bornes. Désactivé quand le bloc est absent. enabled active la passe, sans modèle ni clé d’accès, car le démon écrit lui-même l’aspect outcome. interval_seconds fixe la fréquence d’une passe, 300 par défaut, et une valeur sous 30 est relevée à 30. window_seconds fixe jusqu’où une passe lit les notes, 86400 par défaut. disabled nomme les aspects qu’aucune passe ne regroupe ; les notes déjà enregistrées sous l’un d’eux restent lisibles.
{
"aspects": {
"enabled": true,
"interval_seconds": 300,
"window_seconds": 86400,
"disabled": ["sentiment"]
}
} --forward-endpoint
Recopie chaque span stocké vers un autre endpoint OTLP/HTTP. Donnez l’URL de
base ; /v1/traces est ajouté. L’écriture locale a toujours lieu
d’abord : une destination en panne ne vous coûte donc rien.
Cette option bat le bloc forward du fichier de configuration,
si bien qu’une instance peut être pointée ailleurs sans modifier une
ConfigMap partagée. Voir
Transfert.
--forward-header
Un en-tête envoyé avec chaque lot transféré, écrit sous la forme
Nom: valeur. Répétez l’option pour en avoir plus d’un. La
variable d’environnement prend une liste séparée par des virgules.
Une valeur sans deux-points est refusée au démarrage, avec la forme qui
fonctionne, au lieu d’être ignorée. Un en-tête à la manière d’OpenTelemetry,
key=value, s’écrit ici key: value.
--clickhouse-url
L’endpoint HTTP d’un serveur ClickHouse, par exemple
http://clickhouse:8123. Le poser fait passer le stockage du
fichier embarqué à ClickHouse, et --data cesse d’être lu. Le
schéma est créé au démarrage.
C’est le réglage qui lève les limites d’un seul écrivain et d’une seule réplique du stockage embarqué. Prenez-le quand la conservation se mesure en mois de trafic de production, et pas avant : Exploitation détaille le compromis.
--clickhouse-database, --clickhouse-user, --clickhouse-password
La base dans laquelle créer les tables, et les identifiants pour l’atteindre. Un utilisateur sans mot de passe est envoyé sans mot de passe, ce qui convient à un conteneur sur un réseau privé et ne convient nulle part ailleurs.
--postgres-url
Une chaîne de connexion Postgres, par exemple
postgres://mcpd:secret@db:5432/mcpd. La poser fait passer le
stockage du fichier embarqué à Postgres, et --data cesse d’être
lu. Le schéma est créé au démarrage.
C’est l’option intermédiaire. Elle lève les mêmes limites d’un seul écrivain
et d’une seule réplique que ClickHouse, sans vous demander de faire tourner
une base que vous n’avez pas déjà. Nommer à la fois celle-ci et
--clickhouse-url fait échouer le démarrage au lieu d’en choisir
une.
La migration prend un verrou consultatif : plusieurs répliques qui démarrent ensemble sont donc sans danger, et un démarrage qui n’a rien à appliquer ne touche aucun schéma.
Pour TLS, ajoutez sslmode=require à la chaîne de connexion.
mcpd chiffre alors la connexion et vérifie toujours le certificat du serveur.
Il le vérifie auprès des autorités de certification publiques, ou de celle de
sslrootcert=/chemin/vers/ca.pem, ce dont a besoin un service géré
qui a sa propre autorité. verify-ca et verify-full
sont lus comme require. Avec prefer, la valeur par
défaut, ou disable, la connexion n’est pas chiffrée.
Enregistrer qui, et pas seulement quelle clé
Le journal d’audit enregistre une empreinte de la clé qui a agi. Cela répond à « une clé d’administration a supprimé cette exécution » et pas à « quelle personne l’a fait ». mcpd n’a pas de fournisseur d’identité, donc l’identité doit venir de l’ingress déjà placé devant lui.
Donnez-lui l’en-tête que pose votre proxy, et les adresses autorisées à le poser :
prendre l’identité d’un ingress auquel vous faites confiance
mcpd serve \
--trusted-identity-header x-forwarded-user \
--trusted-proxy-cidr 10.42.0.0/16 Les deux moitiés sont obligatoires, et la liste d’adresses est la raison pour laquelle c’est sûr. Un en-tête est une chaîne que peut envoyer quiconque sait ouvrir une socket vers le port, donc mcpd ne le lit que depuis une adresse que vous avez nommée. Une requête venue d’ailleurs est servie normalement et sa ligne d’audit ne porte aucun sujet, ce qui est la réponse honnête plutôt qu’une supposition. La valeur est aussi bornée en longueur et débarrassée de ses caractères de contrôle, parce qu’un en-tête reste sous l’influence d’un attaquant même quand le proxy ne l’est pas.
Sans aucune des deux options, rien ne change et aucun sujet n’est jamais enregistré. C’est le défaut.
Un projet ne peut pas dépenser tout le daemon
Un projet qui inonde mcpd coûte à tous les autres projets qui s’y trouvent :
le disque se remplit, la conservation ne suit plus, et l’équipe qui n’a rien
fait de mal est celle qui perd des exécutions.
--quota-per-minute est le plafond qui arrête cela.
un plafond pour chaque projet, et un plus grand pour l’un d’eux
mcpd serve --quota-per-minute 10000
Non défini, il n’y a pas de plafond : un déploiement qui ne le pose jamais se
comporte exactement comme avant l’existence des quotas. Posé, il couvre trois
choses : chaque projet du bloc projects qui ne nomme pas son
propre quota_per_minute, le projet nommé par
--project, et chaque projet que la configuration ne
liste pas du tout.
C’est ce dernier point qui en fait un plafond plutôt qu’une suggestion. Le nom du projet arrive à l’intérieur du corps de la requête : un émetteur qui choisit un nom qu’aucun fichier de configuration ne liste n’était donc facturé à personne et pouvait stocker sans borne. Une limite dont l’émetteur peut sortir en inventant une chaîne n’est pas une limite.
Tous les projets non listés partagent un seul compteur au lieu d’en avoir un
chacun. Un compteur par nom serait une allocation que n’importe quel émetteur
pourrait faire grossir en inventant des noms, ce qui est le même trou sous
une autre forme. Le partage veut dire qu’une inondation étalée sur mille noms
inventés est bornée au total par ce nombre, et qu’un projet qui mérite sa
propre allocation doit être nommé dans projects, où il en reçoit
une.
Ce que le plafond borne réellement
Le compteur est du genre pondéré sur deux fenêtres : le compte de cette minute, plus celui de la minute précédente pondéré par la part qu’en couvre encore la fenêtre qui se termine maintenant. Il tient le débit soutenu au plafond et permet en plus une rafale allant jusqu’à un plafond entier, parce qu’un SDK qui vide une file pleine arrive en une rafale et qu’un compteur qui la refuserait refuserait du trafic ordinaire.
Ces deux points ont ensemble une conséquence arithmétique qu’il vaut mieux dire que cacher. Un projet peut stocker un peu moins du double de son plafond à l’intérieur d’un même intervalle de 60 secondes : un plafond entier tout à la fin d’une fenêtre, et un plafond entier à nouveau tout à la fin de la suivante. Ce n’est pas un artefact de l’approximation. Tout limiteur qui laisse un projet dépenser son plafond d’un coup, puis son plafond à nouveau chaque minute, permet cela au passage d’une frontière, et la seule façon de le supprimer est de refuser la rafale.
La fenêtre est d’une minute et n’est pas configurable. L’option nomme l’unité : un second réglage qui changerait le sens de « par minute » rendrait le premier nombre illisible.
Ce qui arrive à l’excédent
Les signaux au-dessus du plafond sont refusés et signalés, jamais jetés en silence. Le lot n’est pas refusé en entier : mcpd stocke ce qui rentre et vous dit ce qui n’est pas rentré.
- Les spans passent par trace entière. Une demi-exécution a des spans dont personne n’a stocké le parent, ce qui se lit comme une exécution cassée plutôt que comme une exécution manquante. Une trace trop grande pour ce qu’il reste est sautée et la suivante essayée, si bien qu’une grosse exécution ne dépense pas le reste de l’allocation pour rien.
- Les logs, les métriques, les scores et les retours tiennent seuls : ils remplissent donc ce qui reste dans l’ordre d’arrivée.
- Seul ce qui est stocké est facturé. L’élagage garde des traces entières et peut donc stocker moins de signaux qu’il n’en avait le droit, et la différence est rendue. Sans cela, un projet dont les exécutions sont plus grandes que son allocation restante paierait jusqu’à zéro sans rien stocker.
- Les autres projets de la même requête ne sont pas touchés. Un projet au-dessus de son plafond ne coûte pas à un autre les signaux qu’il a envoyés à côté.
La réponse porte le compte dans rejected et une ligne dans
errors nommant le projet et son plafond. Un projet que la
configuration n’a jamais listé reçoit une phrase différente, parce
qu’apprendre que le projet analytics-v2 est au-dessus de son
plafond vous envoie chercher un réglage qui n’existe pas, alors que la
réponse est d’ajouter le projet :
$ curl -sX POST "$MCPD/v1/ingest" -H content-type:application/json -d @batch.json | jq{ "accepted": 5, "rejected": 3, "duplicate": 0, "errors": [ { "signal": "span", "index": 0, "message": "3 signal(s) named project analytics-v2, which no configuration lists. Every unlisted project shares one ceiling of 5 per minute and it was full, so they were not stored. Add analytics-v2 to the `projects` block to give it an allowance of its own, or raise --quota-per-minute." } ]}
Un refus est compté dans mcpd_signals_over_quota_total aussi
bien que dans mcpd_signals_rejected_total : une flotte que l’on
limite se distingue donc d’une flotte qui envoie des données mal formées.
Voir Exploitation.
Deux choses à vérifier quand vous le posez
Le plafond couvre toutes les portes qui stockent un signal : celles par lot,
/v1/ingest, /v1/traces, /v1/logs et
/v1/metrics, et celles à un seul enregistrement,
POST /v1/api/scores et POST /v1/api/feedback. Les
deux répondent différemment à un projet au-dessus de son plafond, et c’est
voulu. Une porte par lot stocke ce qui rentre et signale le reste comme
rejected avec un motif, parce qu’un lot peut nommer plusieurs
projets et que refuser la requête entière punirait ceux qui sont dans leur
plafond. Une porte à un seul enregistrement n’a pas de réponse partielle à
donner : elle refuse donc avec un 429 et un en-tête
Retry-After portant le nombre de secondes avant que le compteur
de ce projet n’admette un signal de plus. Le nombre vient du compteur
lui-même : un client qui l’honore revient quand il y a de la place, et non
quand un délai fixe est écoulé.
Une porte d’écriture n’est pas facturée dessus :
POST /v1/api/score, qui fait tourner des scorers sur des
exécutions stockées. Elle remet directement au stockage les scores qu’elle a
produits : une passe atterrit donc sur le disque pour un projet dont le
plafond est déjà dépensé, et ces lignes ne sont comptées ni par
mcpd_signals_over_quota_total ni par
mcpd_signals_unbounded_total. C’est une porte que vous ouvrez
vous-même plutôt qu’une porte vers laquelle une flotte pointe : traitez donc
le plafond comme une borne sur ce que les émetteurs peuvent pousser, pas sur
tout ce qui peut atteindre le disque.
Un plafond inférieur à --max-batch-signals, soit 10 000 par
défaut, veut dire qu’un seul lot légal peut dépasser l’allocation d’une
minute entière et sera élagué quelle que soit la bonne tenue de l’émetteur.
mcpd doctor note cela sous limits et nomme les
projets concernés. Il avertit aussi quand un plafond est posé par projet sans
plafond de premier niveau à côté, ce qui est la configuration qui laisse tout
projet non listé sans borne.
mcpd config affiche les projets limités par leur nom, pour que
vous puissiez relire ce que le réglage couvre au lieu de le déduire. Il
n’affiche que les plafonds nommés : confirmez donc le plafond partagé depuis
l’option elle-même.