Aller au contenu
mcpd
aucun daemon
Source

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 config
$ 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_ENDPOINThttp://localhost:4317
MCPD_API_KEYnon définie
MCPD_PROJECTdefault
MCPD_ENVIRONMENTnon définie
MCPD_SERVICE_NAMEnon définie
MCPD_SERVICE_VERSIONnon définie
MCPD_SAMPLE1.0
MCPD_ENABLEDtrue
MCPD_CAPTURE_PAYLOADStrue
MCPD_LOG_LEVELdebug

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 :

huit signaux vers un projet que personne n’a configuré, avec le plafond partagé à cinq
$ 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.