Configuración
Cada ajuste es un flag, y cada flag tiene una variable de entorno con el mismo significado. Los pocos ajustes demasiado estructurados para una línea de comandos están en un único archivo JSON. Esta página los recoge todos.
Dos formas de decir lo mismo
Ejecuta mcpd sin argumentos y funciona: escucha en
0.0.0.0:4317, guarda los datos en ./mcpd.db, oculta
los secretos y almacena cada traza para siempre. Todo lo que sigue cambia
una de esas decisiones.
Un flag siempre gana a su variable de entorno. Los dos son globales, así que se aceptan antes o después de un subcomando.
Referencia
| Flag | Variable | Por defecto |
|---|---|---|
--listen | MCPD_LISTEN | 0.0.0.0:4317 |
--data | MCPD_DATA | ./mcpd.db |
--project | MCPD_PROJECT | default |
--retention-days | MCPD_RETENTION_DAYS | sin definir: se conserva todo |
--ingest-key | MCPD_INGEST_KEYS | ninguna: la ingesta está abierta |
--read-key | MCPD_READ_KEYS | ninguna: la lectura está abierta |
--write-key | MCPD_WRITE_KEYS | ninguna: se aceptan las claves de ingesta |
--admin-key | MCPD_ADMIN_KEYS | ninguna: se aceptan las claves de lectura |
--execute-key | MCPD_EXECUTE_KEYS | ninguna: se aceptan las claves de escritura |
--worker-key | MCPD_WORKER_KEYS | ninguna: se aceptan las claves de escritura |
--runs-data | MCPD_RUNS_DATA | la ruta de --data con .runs antes de la extensión |
--trusted-identity-header | MCPD_TRUSTED_IDENTITY_HEADER | ninguna: no se registra ningún sujeto |
--trusted-proxy-cidr | MCPD_TRUSTED_PROXY_CIDRS | ninguno: ninguna dirección puede fijar esa cabecera |
--sample | MCPD_SAMPLE | sin definir: se conserva cada traza |
--quota-per-minute | MCPD_QUOTA_PER_MINUTE | sin definir: sin tope |
--no-redact | MCPD_NO_REDACT | desactivado: la ocultación está activada |
--redact-key | MCPD_REDACT_KEY | una clave incorporada, que es pública |
--redact-key-contains | MCPD_REDACT_KEY_CONTAINS | ninguna: solo las reglas incorporadas |
--redact-max-string | MCPD_REDACT_MAX_STRING | 128 KiB |
--cors-origin | MCPD_CORS_ORIGINS | ninguno: solo el mismo origen |
--request-timeout-seconds | MCPD_REQUEST_TIMEOUT_SECONDS | 30 |
--max-body-bytes | MCPD_MAX_BODY_BYTES | 33554432, que son 32 MiB |
--max-batch-signals | MCPD_MAX_BATCH_SIGNALS | 10000 |
--log-format | MCPD_LOG_FORMAT | text |
--clickhouse-url | MCPD_CLICKHOUSE_URL | desactivado: se usa el archivo incorporado |
--clickhouse-database | MCPD_CLICKHOUSE_DATABASE | mcpd |
--clickhouse-user | MCPD_CLICKHOUSE_USER | ninguno |
--clickhouse-password | MCPD_CLICKHOUSE_PASSWORD | ninguna |
--postgres-url | MCPD_POSTGRES_URL | desactivado: se usa el archivo incorporado |
--config-file | MCPD_CONFIG_FILE | ninguno |
--forward-endpoint | MCPD_FORWARD_ENDPOINT | desactivado: no se reenvía nada |
--forward-header | MCPD_FORWARD_HEADERS | ninguna |
| ninguno | MCPD_LOG | mcpd=info,mcpd_server=info,tower_http=warn |
--listen
La dirección a la que enlazar. Un solo puerto sirve los endpoints OTLP, el endpoint de ingesta nativo, la API de consulta, las sondas, las métricas y la interfaz, así que esta es la única dirección que tiene mcpd.
Enlazar una dirección de loopback es la opción discreta: mcpd solo avisa de
que la ingesta está abierta cuando la dirección que enlazó no es de
loopback. En 0.0.0.0 y sin claves, avisa dos veces al arrancar,
una por la ingesta y otra por la lectura.
--data
Ruta del archivo SQLite. Los directorios superiores se crean si no existen.
Pasa :memory: para un almacén que no deja nada tras de sí, que
es lo correcto para una demostración o una prueba y lo equivocado para
cualquier cosa que quieras mirar mañana.
--project
El proyecto en el que cae una señal cuando el emisor no nombró ninguno. Un
proyecto es la frontera de tenencia: cada consulta se limita a uno. La
pasada de retención se ejecuta sobre él y sobre cada proyecto del bloque
projects; un proyecto que no aparece en ninguno de los dos
sitios no se recorta nunca, lo que importa si tus emisores nombran el suyo.
--retention-days
Borra las señales con más días que este número. Sin definir se conserva todo, que es el valor por defecto correcto para un portátil y una decisión que conviene tomar a conciencia en cualquier otro sitio.
La ventana va de 1 a 36500 días, aquí y en el retention_days de
un proyecto. El cero se rechaza al arrancar en lugar de leerse como ausencia
de retención: pondría el corte en este instante, y la primera pasada se
ejecuta al arrancar, así que borraría todas las señales del proyecto. Para
conservarlo todo, no definas el ajuste.
La pasada se ejecuta con un temporizador horario y no en el momento de la consulta, así que un borrado grande nunca cae dentro de la solicitud de un usuario. Los spans, las trazas, los logs y las métricas se cortan por el momento en que mcpd los recibió; las puntuaciones, el feedback y las notas de aspecto, por su propia marca de tiempo. Borrar filas no encoge el archivo. Consulta Operación.
--ingest-key, --read-key, --write-key, --admin-key, --execute-key y --worker-key
Seis ámbitos, porque conllevan riesgos realmente distintos. Cada endpoint
declara el que necesita en spec/mcpd.spec.json, su manejador
lleva la etiqueta que le corresponde, y make check compara los
dos, así que el ámbito documentado aquí es el ámbito que aplica el servidor
en marcha.
| Ámbito | Se fija con | Qué abre |
|---|---|---|
ingest | --ingest-key | Los cuatro endpoints de ingesta. No lee nada. |
read | --read-key | La API de consulta: trazas, spans, logs, métricas, puntuaciones, analítica. |
write | --write-key | Puntuaciones, feedback, datasets, experimentos, acuses de recibo y capturar una ejecución en un dataset. |
admin | --admin-key | Borrar una traza, leer el registro de auditoría y todo lo que abren los dos ámbitos de abajo. |
execute | --execute-key | Iniciar, cancelar y señalizar una ejecución, y publicar un evento en una. |
worker | --worker-key | Registrar definiciones, reclamar pasos e informar de su resultado. No lee ninguna traza. |
La separación que más importa es ingest frente a todo lo demás.
Una clave de ingesta vive en cada proceso que emite un span, incluidos los
que ejecutan código que no escribiste tú. Dar a esa población una credencial
que además pueda leer cada prompt almacenado es un error que mcpd no pone
fácil: una clave de ingesta presentada a la API de consulta se rechaza.
write está separado de ingest por la misma razón a
menor escala. Capturar una ejecución en un dataset lee la entrada y la
salida de esa ejecución, así que es una lectura vestida de escritura y no le
corresponde estar en la clave que lleva tu flota.
admin está separado de read porque borrar una
ejecución no es leerla. Hasta que fijes --admin-key, una clave
de lectura borra además de leer, y mcpd lo dice al arrancar.
execute y worker son los dos ámbitos de la
superficie de ejecuciones. Iniciar una ejecución hace que el worker de
alguien trabaje y gaste dinero, cosa que anotar una traza no hace, así que
no es write. Una clave de worker vive en el proceso que ejecuta
los cuerpos de los pasos, que suele ser el proceso que guarda la credencial
del proveedor, así que abre el protocolo de leases y nada que lea un prompt
almacenado. Hasta que fijes los dos flags, se aceptan las claves de
escritura para ambos, y una clave de administración siempre se acepta.
Actualizar sin una caída
Un --write-key sin definir acepta las claves de ingesta, y un
--admin-key sin definir acepta las claves de lectura. Un
despliegue configurado antes de que existieran estos dos ámbitos conserva
por tanto exactamente el alcance que tenía, y nada se rompe al actualizar.
Fija los dos flags para tomar los permisos más estrechos, y
mcpd config te dice en qué estado estás: imprime
key required cuando el ámbito tiene claves propias y
key required (ingest keys) o
key required (read keys) cuando las está heredando.
Repite cualquiera de los seis flags para varias claves, que es como se
rota una sin una caída. Las variables de entorno toman una lista separada
por comas. Quien llama presenta una clave como
Authorization: Bearer KEY o como
X-Mcpd-Key: KEY, y la comparación es de tiempo constante, así
que no se observa ni la longitud ni la posición de la primera diferencia.
Una clave que mcpd no conoce es un 401. Una clave que mcpd sí
conoce, usada en un endpoint que su ámbito no cubre, es un 403.
Vale la pena mantener los dos separados: un 401 te manda a buscar una
errata en una clave que es perfectamente correcta.
Sin ninguna clave configurada, todos los ámbitos están abiertos. Es deliberado, porque la primera ejecución no debe exigir una credencial, y por eso existe el aviso de arranque.
Fíjate en lo que las claves no cubren: /healthz,
/readyz, /metrics y la propia interfaz se sirven
sin clave en esta versión. Ponlos detrás de tu propio ingress si eso te
importa.
Una entrada vacía se descarta antes de que nada lea la lista. Una lista fijada que no contiene ninguna clave, como MCPD_EXECUTE_KEYS= o un valor vacío en un Secret de Kubernetes, impide que el daemon arranque, y el rechazo nombra el flag y la variable; omite la variable para dejar el ámbito sin fijar, y una solicitud que envía una cabecera de clave vacía no presenta ninguna clave. Una clave que sigue siendo un marcador de los archivos de ejemplo de deploy/, cualquier valor que empiece por REPLACE_WITH o replace-me en mayúsculas o minúsculas, impide que el daemon arranque; el rechazo nombra el flag o la lista del proyecto y nunca el valor. Lo mismo vale para --redact-key. Genera cada clave con openssl rand -hex 32. Sin --write-key, --execute-key ni --worker-key, las claves de ingesta también inician ejecuciones y reclaman pasos, y mcpd lo advierte al arrancar y en mcpd doctor.
El registro de auditoría
Los manejadores que devuelven payloads anotan quién leyó qué:
GET /v1/api/traces/{id}, las búsquedas de spans y de logs,
el feedback, los elementos de un dataset, la captura y el borrado. Cada fila
lleva la hora, un resumen corto de la clave que actuó, la acción, el
proyecto, el recurso y si la autorización lo permitió. La clave en sí no se
escribe nunca.
Lee el registro con GET /v1/api/audit, que exige el ámbito de
administración. --retention-days no lo recorta: un registro que
se borrara a sí mismo con el mismo calendario que los datos que describe no
podría decir quién leyó los datos que ya no están. Escribir una fila nunca
hace fallar la lectura que describe; una fila que no se puede escribir se
anota en el log y se cuenta como
mcpd_audit_write_failures_total en /metrics, lo
que merece una alerta.
--sample
Una fracción entre 0.0 y 1.0. Un valor fuera de ese rango es un error de
arranque, no un ajuste silencioso: --sample must be between 0.0 and
1.0, got 2.
La decisión se toma calculando el hash del id de la traza, así que una flota de instancias de mcpd detrás de un balanceador conserva y descarta las mismas trazas sin hablar entre ellas. Se toma una vez por traza y la heredan todos los spans de esa traza en el lote, porque una traza registrada a medias parece un fallo del sistema que estás observando.
El hash lee los bytes finales del id de la traza. Algunos productores derivan los bytes iniciales de una marca de tiempo, y un muestreador que mirara esos se correlacionaría con la hora del día y descartaría horas enteras sin decir nada.
El tipo de política también lleva «conservar siempre los errores» y «conservar siempre lo más lento que», pero eso se aplica a una decisión de cola, tomada una vez que la traza ha terminado. Esta versión muestrea solo en cabecera, y ningún flag activa el muestreo de cola. Muestreo cubre el compromiso.
--no-redact, --redact-key, --redact-key-contains y --redact-max-string
La ocultación está activada por defecto y se ejecuta antes de escribir nada, así que un valor que nunca debió persistirse nunca lo hace. Borrarlo después no es la misma garantía. Vienen incorporados tres conjuntos de reglas, que se comparan con subcadenas en minúsculas del nombre de una clave:
| Regla | Se aplica cuando la clave contiene | Qué se almacena |
|---|---|---|
| Máscara | 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] |
| Parcial | email, phone, ssn,
credit_card, card_number,
iban |
Los dos primeros caracteres y los dos últimos,
ab…yz. Un valor de seis caracteres o menos se
enmascara entero en lugar de filtrar casi todo
|
| Hash | user_id, userid | sha256: y 12 bytes de un resumen con clave, para que
puedas seguir agrupando por usuario sin almacenar ninguno
|
Una clave que coincide y contiene un objeto o un array oculta también todo
lo que hay debajo, así que {"credentials": {"token": "..."}}
no se cuela solo por el nombre anidado. Cualquier cadena suelta de más de
128 KiB se trunca con una marca que dice cuántos bytes son, lo que acota lo
que un prompt desbocado le hace a tu disco.
--redact-key es la clave de la regla de hash. Las entradas
iguales dan el mismo hash bajo una misma clave, que es lo que hace que
agrupar funcione, y rotar la clave rompe ese vínculo a propósito. Fíjala en
cualquier despliegue que haga hash: el valor por defecto incorporado está en
el código fuente, así que los resúmenes sin clave propia son adivinables.
--redact-key-contains añade una regla de enmascarado tuya
encima de las tres incorporadas. Las reglas incorporadas conocen los nombres
bajo los que se suelen guardar las credenciales; no conocen el campo donde
tus propias herramientas ponen el número de cuenta de un cliente. El valor
se compara igual que en las reglas incorporadas, como subcadena en
minúsculas de la clave, así que
--redact-key-contains ticket enmascara por igual
ticket_id y supportTicket, y con ellos todo lo
anidado bajo una clave que coincide. Repite el flag para varias, o fija
MCPD_REDACT_KEY_CONTAINS con una lista separada por comas.
--redact-max-string baja el límite de longitud desde los
128 KiB incorporados. Una cadena más larga que el límite se corta ahí, con
una marca que dice cuántos bytes se descartaron, antes de almacenarla.
--no-redact apaga toda la cadena, incluido el límite de
longitud. Como variable de entorno es
MCPD_NO_REDACT=true. Solo tiene sentido cuando algo aguas
arriba ya oculta, y el daemon se niega a arrancar si alguna de las dos
adiciones anteriores está fijada a su lado, porque no habría nada a lo que
aplicarlas.
--runs-data
Dónde está el almacén de ejecuciones: las definiciones, el diario de
intentos de cada paso, cada plazo y cada espera. Es un archivo incorporado
sea cual sea el almacén de señales, así que con Postgres o ClickHouse las
ejecuciones siguen estando en un archivo junto al daemon, y una sola réplica
sirve la superficie de ejecuciones. Por defecto es la ruta de
--data con .runs antes de la extensión,
./mcpd.runs.db en el caso por defecto, y :memory:
cuando --data lo es. --runs-data off no sirve
ninguna ejecución: cada endpoint de ejecuciones responde 503
nombrando este flag, y el resto del daemon no cambia.
Los tiempos del almacén están en el archivo de configuración bajo
runs, en milisegundos enteros: lease_ttl_ms,
reclaim_window_ms, stall_window_ms,
idempotency_window_ms, park_ttl_ms y
max_depth. Los valores por defecto, un lease de 30 segundos y
una ventana de reclamación de 30 segundos, son los correctos para un daemon
en una máquina y son los que un worker lee de vuelta cuando se registra.
--cors-origin
Qué orígenes de navegador pueden llamar a la API de consulta desde otro
sitio. Sin ninguno fijado, una solicitud de origen cruzado no recibe
cabecera access-control-allow-origin y el navegador se niega a
entregarle la respuesta a la página. Ese es el valor por defecto, y es lo
que impide que un daemon en tu portátil con la API de lectura abierta sea
legible por cualquier página abierta en el mismo navegador. La interfaz
incluida se sirve desde el propio origen del daemon y nunca necesita este
flag.
Servir la interfaz desde otro sitio, una CDN o un portal interno, es el caso
para el que existe este flag: nombra ese origen, con esquema y host, por
ejemplo --cors-origin https://console.example. Repítelo para
varios, o fija MCPD_CORS_ORIGINS con una lista separada por
comas. Un valor que no es un origen, console.example o uno con
barra final, impide el arranque nombrando el flag en lugar de dejarte con
una interfaz que no conecta y sin motivo aparente.
--cors-origin '*' permite todos los orígenes, que es lo que
hacían las versiones anteriores sin preguntar.
/healthz, /readyz y /metrics no se
rigen por este flag. No llevan datos almacenados, y la interfaz sondea
/healthz antes de hacer nada más, así que siguen siendo
legibles desde cualquier origen.
--request-timeout-seconds
Lo máximo que puede tardar una solicitud, incluida la lectura del cuerpo,
antes de que el daemon responda por ella. A un manejador que pasa del plazo
se le responde con un 503 cuyo cuerpo lleva
timeout y el nombre de este flag. El valor por defecto es de 30
segundos, tiempo suficiente para leer y almacenar un lote grande, y lo
bastante corto para que un almacén que acepta conexiones y no responde nunca
se informe en lugar de esperarlo. No se puede desactivar; súbelo si una
operación necesita de verdad más tiempo.
--max-body-bytes y --max-batch-signals
Los dos límites de una solicitud. --max-body-bytes es el cuerpo
más grande que lee el daemon, 32 MiB por defecto, y es lo que decide cuánta
memoria puede ocupar una solicitud antes de decodificar nada de ella. Un
cuerpo mayor se rechaza con 413 nombrando los dos números, y
uno comprimido se somete a la misma cifra después de descomprimirlo, así que
un cuerpo que se expande mil veces se rechaza en lugar de expandirse.
--max-batch-signals es cuántas señales puede llevar una
solicitud una vez decodificada, 10,000 por defecto. Se cuenta sobre toda la
solicitud, así que un array de lotes y un flujo NDJSON se someten al mismo
número que un solo lote. No se almacena nada cuando una solicitud lo supera:
el rechazo llega antes de la primera escritura y no después de la mitad.
Los dos son también claves del archivo de configuración,
max_body_bytes y max_batch_signals, y el flag gana
donde se fijan los dos. Ninguno se puede desactivar, y el cero se rechaza en
el arranque en lugar de leerse como «sin límite»: un tope de cuerpo de cero
rechaza todas las solicitudes, así que la lectura que parece generosa es la
que tumba la ingesta.
--log-format y MCPD_LOG
text para un terminal, json para un pipeline de
logs. MCPD_LOG toma una cadena de filtro de
tracing y no tiene flag, porque es un control de depuración y
no una decisión de despliegue. Fíjalo en mcpd_server=debug
cuando quieras ver el detalle de cada solicitud.
Dónde se aplican los dos límites de solicitud
El tope de cuerpo se aplica a cada ruta que lee un cuerpo, la API de consulta incluida. El tope de señales se aplica a las puertas de ingesta, y esto es lo que cuenta en cada una de ellas.
| Puerta | Qué cuenta el tope de señales |
|---|---|
POST /v1/ingest | Cada span, log, métrica, puntuación, pieza de feedback y nota de aspecto de la solicitud, sumados en todo su conjunto, tanto si llegó como un lote, como un array de lotes o como un flujo de líneas NDJSON |
POST /v1/traces | Los spans que envió el emisor, contados antes de mapearlos, que es el número sobre el que el emisor puede actuar |
POST /v1/logs | Los registros de log de la exportación |
POST /v1/metrics | Los puntos de datos de la exportación, en todas sus métricas |
Subcomandos
| Comando | Qué hace |
|---|---|
mcpd serve | Ejecuta el daemon. Es lo que ocurre sin ningún subcomando |
mcpd demo | Escribe una ejecución de muestra realista en el almacén y después sirve, para que la interfaz tenga algo dentro la primera vez que la abres |
mcpd config | Imprime la configuración que usaría esta invocación y sale |
mcpd mcp | Sirve el Model Context Protocol sobre stdio, para que un agente pueda leer sus propias trazas desde un daemon en marcha |
mcpd doctor | Califica este despliegue e informa de lo que está mal configurado. Sale con 2 ante un fallo, así que puede servir de puerta para un despliegue. Consulta Operación |
mcpd config existe para la discusión que estás a punto de tener
con un contenedor. Los flags, las variables de entorno y los valores por
defecto acaban todos en un mismo sitio, y esto imprime ese sitio sin
arrancar un servidor ni tocar el archivo de datos:
$ 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
Lee las seis líneas de autenticación antes de desplegar nada.
open significa abierto a quien pueda alcanzar el puerto. Una
línea entre paréntesis nombra la lista más amplia de la que ese ámbito está
heredando, que es el estado en el que arranca un despliegue actualizado
desde una versión anterior.
Un despliegue configurado
Nada de esto es propio de mcpd: las variables son entorno corriente, así que funcionan igual en Compose, en un manifiesto de Kubernetes y en una unidad de 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: Los mismos ajustes como flags, para una shell o un archivo de unidad:
mcpd --listen 0.0.0.0:4317 --data /data/mcpd.db --retention-days 30 --ingest-key $KEY --log-format json Qué leen los SDK
Estas variables las lee el proceso de tu agente, no el daemon. Se listan aquí porque los dos conjuntos comparten nombres, y un contenedor que ejecuta a la vez un agente y mcpd se configura con un único entorno.
| Variable | Por defecto |
|---|---|
MCPD_ENDPOINT | http://localhost:4317 |
MCPD_API_KEY | sin definir |
MCPD_PROJECT | default |
MCPD_ENVIRONMENT | sin definir |
MCPD_SERVICE_NAME | sin definir |
MCPD_SERVICE_VERSION | sin definir |
MCPD_SAMPLE | 1.0 |
MCPD_ENABLED | true |
MCPD_CAPTURE_PAYLOADS | true |
MCPD_LOG_LEVEL | debug |
MCPD_PROJECT y MCPD_SAMPLE significan lo mismo en
los dos lados, que suele ser lo que quieres: fíjalos una vez y el agente y
el daemon coinciden en el proyecto. El muestreo es lo que hay que pensar. Un
SDK descarta una traza antes de enviarla, lo que ahorra red. El daemon la
descarta antes de almacenarla, que es el único sitio donde se puede imponer
la tasa a emisores que no controlas. Fija la tasa en un sitio, no en los
dos, o estarás filtrando un flujo ya filtrado.
MCPD_ENABLED=false convierte cada llamada del SDK en una
operación vacía que sigue devolviendo objetos válidos, así que mcpd se puede
apagar en las pruebas sin cambiar nada de código.
MCPD_CAPTURE_PAYLOADS=false mantiene los prompts y las
completions dentro del proceso: los spans siguen llegando con su
tiempo, su uso, su modelo y sus errores, pero input y
output se descartan antes de encolarlos, así que nunca cruzan
la red. La ocultación en el daemon es el otro extremo del mismo cable, y el
extremo equivocado para un equipo que no debe enviar los bytes en absoluto.
El detalle de cada lenguaje está en su
página de SDK.
--config-file
Un archivo JSON con los ajustes que no caben en una línea de comandos: monitores, objetivos de nivel de servicio, el intervalo con el que se evalúan, el reenvío, el juez, los tiempos del almacén de ejecuciones, los dos límites de solicitud y el bloque por proyecto de más abajo. JSON en lugar de YAML o TOML porque mcpd ya analiza JSON para todo lo demás, así que el archivo no añade ninguna dependencia ni una segunda sintaxis que aprender. Se monta tal cual como un ConfigMap de Kubernetes.
max_body_bytes y max_batch_signals están aquí
además de en la línea de comandos, para que una flota pueda tenerlos junto a
los monitores con los que se despliegan. Un flag gana al archivo, que es como
una instancia toma un límite distinto sin que se edite el archivo compartido
de todos.
Una clave desconocida impide el arranque en lugar de ignorarse, porque una errata que en silencio no vigila nada es el peor desenlace disponible.
Lo mismo pasa con un valor con el que un temporizador de fondo no puede
funcionar: un objective que no sea al menos 0 y menor que 1, un
window_days de un objetivo fuera de 1 a 365, un
monitor_interval_seconds mayor que 86400, un
aspects.window_seconds fuera de 1 a 31536000, un
aspects.interval_seconds mayor que 604800 y un
judge.timeout_seconds fuera de 1 a 300. Cada rechazo nombra la
clave y el rango.
{
"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
}
} Los proyectos, y las claves que llegan a ellos
El proyecto es la clave de tenencia. Listar claves bajo uno es lo que hace que eso sea cierto y no decorativo: una clave del bloque de un proyecto llega a ese proyecto y a ningún otro, así que un solo daemon sirve a varios equipos sin que ninguno pueda leer los prompts de los demás.
{
"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
}
}
}
Las seis listas existen por proyecto, como ingest_keys,
read_keys, write_keys, admin_keys,
execute_keys y worker_keys, y recurren al mismo respaldo: un proyecto sin
write_keys acepta sus propias claves de ingesta, y uno sin
admin_keys acepta sus propias claves de lectura. El bloque
support de arriba no fija ninguna de las dos, así que se
comporta exactamente como antes de que existieran los dos ámbitos.
Una clave fijada con cualquiera de los seis flags de nivel superior llega a todos los proyectos. Ese es el camino de un solo inquilino, y sigue siendo el sencillo: si nunca abres este bloque, nunca te encuentras con nada de él.
retention_days y sample recurren a los flags
globales cuando un proyecto no los fija. Los dos son por proyecto por la
misma razón que las claves: el requisito de noventa días de un equipo no
debería obligar a conservar los datos de todos los demás, y el tráfico de un
equipo no debería decidir cómo se muestrea a otro.
Un lote puede nombrar varios proyectos. Una clave de ingesta con ámbito escribe las señales a las que llega y rechaza el resto, informando del rechazo en la respuesta en lugar de descartarlas en silencio, así que una flota apuntada al proyecto equivocado se ve en la respuesta que ya está leyendo.
Una solicitud de lectura de un proyecto al que tu clave no llega se rechaza sin decir si ese proyecto existe. En un daemon compartido, los nombres de los proyectos suelen ser los nombres de equipos o de clientes.
Los monitores y los objetivos de nivel de servicio se tratan en
Monitores y alertas;
los objetivos van en un array slos junto a
monitors y se evalúan con el mismo temporizador. El bloque
judge activa la puntuación calificada por un modelo y se trata
en Pruebas de regresión.
Está apagado hasta que lo fijes, y un evaluador con juez no se ejecuta nunca
salvo que una solicitud lo nombre, porque calificar gasta dinero en cada
ejecución que lee.
{
"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 es cada cuánto se evalúan los monitores y los objetivos. Vale 60 por defecto, y un valor por debajo de 5 se sube a 5.
derived_metrics
Qué métricas deriva el daemon de los spans que guarda. Las dos mitades están activas cuando el bloque no está. durations escribe un punto de duración por cada span terminado de agente, herramienta, workflow, generación de modelo o procesador. tokens escribe hasta trece contadores de tokens por cada span de generación de modelo que informó su uso. Pon cualquiera de los dos a false para dejar de escribirlo; un campo omitido sigue activo. No hay flag para esto. Los puntos derivados se añaden después del techo por proyecto y no cuentan en el número accepted que devuelve una respuesta de ingesta.
{
"derived_metrics": { "durations": true, "tokens": false }
} aspects
Si el daemon agrupa las notas de aspecto en temas, y con qué límites. Apagado cuando el bloque no está. enabled enciende la pasada y no necesita modelo ni credencial, porque el daemon escribe él mismo el aspecto outcome. interval_seconds es cada cuánto corre una pasada, 300 por defecto, y un valor por debajo de 30 se sube a 30. window_seconds es hasta dónde lee notas una pasada, 86400 por defecto. disabled nombra los aspectos que ninguna pasada agrupa; las notas ya guardadas bajo uno siguen siendo legibles.
{
"aspects": {
"enabled": true,
"interval_seconds": 300,
"window_seconds": 86400,
"disabled": ["sentiment"]
}
} --forward-endpoint
Copia cada span almacenado a otro endpoint OTLP/HTTP. Da la URL base; se le
añade /v1/traces. La escritura local ocurre siempre primero,
así que un destino caído no te cuesta nada.
Este flag gana al bloque forward del archivo de configuración,
así que se puede apuntar una instancia a otro sitio sin editar un ConfigMap
compartido. Consulta
Reenvío.
--forward-header
Una cabecera que se envía con cada lote reenviado, escrita como
Name: value. Repite el flag para más de una. La variable de
entorno toma una lista separada por comas.
Un valor sin dos puntos se rechaza al arrancar, mostrando la forma que
funciona, en lugar de descartarse. Una cabecera al estilo de OpenTelemetry,
key=value, aquí se escribe key: value.
--clickhouse-url
El endpoint HTTP de un servidor ClickHouse, por ejemplo
http://clickhouse:8123. Fijarlo cambia el almacén del archivo
incorporado a ClickHouse, y --data deja de leerse. El esquema
se crea al arrancar.
Este es el ajuste que quita los límites de un solo escritor y una sola réplica del almacén incorporado. Recurre a él cuando la retención se mida en meses de tráfico de producción, y no antes: Operación cubre el compromiso.
--clickhouse-database, --clickhouse-user, --clickhouse-password
La base de datos en la que crear las tablas, y las credenciales con las que llegar a ella. Un usuario sin contraseña se envía sin ella, lo que es correcto para un contenedor en una red privada y equivocado en cualquier otro sitio.
--postgres-url
Una cadena de conexión de Postgres, por ejemplo
postgres://mcpd:secret@db:5432/mcpd. Fijarla cambia el almacén
del archivo incorporado a Postgres, y --data deja de leerse. El
esquema se crea al arrancar.
Esta es la opción intermedia. Quita los mismos límites de un solo escritor y
una sola réplica que ClickHouse, sin pedirte que ejecutes una base de datos
que no tengas ya. Nombrar a la vez esta y --clickhouse-url
impide el arranque en lugar de elegir una.
La migración toma un bloqueo consultivo, así que varias réplicas arrancando a la vez es seguro, y un arranque sin nada que aplicar no toca el esquema en absoluto.
Para TLS, añade sslmode=require a la cadena de conexión. mcpd
cifra entonces la conexión y siempre verifica el certificado del servidor. Lo
comprueba contra las autoridades de certificación públicas, o contra la de
sslrootcert=/ruta/a/ca.pem, que es lo que necesita un servicio
gestionado con su propia autoridad. verify-ca y
verify-full se leen como require. Con
prefer, el valor por defecto, o con disable, la
conexión no se cifra.
Registrar quién, no solo qué clave
El registro de auditoría anota un resumen de la clave que actuó. Eso responde «una clave de administración borró esta ejecución» y no «qué persona lo hizo». mcpd no tiene proveedor de identidad, así que la identidad tiene que venir del ingress que ya está delante.
Dale la cabecera que fija tu proxy, y las direcciones que pueden fijarla:
toma la identidad de un ingress en el que confías
mcpd serve \
--trusted-identity-header x-forwarded-user \
--trusted-proxy-cidr 10.42.0.0/16 Las dos mitades son obligatorias, y la lista de direcciones es la razón de que esto sea seguro. Una cabecera es una cadena que puede enviar cualquiera capaz de abrir un socket contra el puerto, así que mcpd la lee solo desde una dirección que hayas nombrado. Una solicitud de cualquier otro sitio se sirve con normalidad y su fila de auditoría no lleva sujeto, que es la respuesta honesta en lugar de una suposición. El valor también tiene un tope de longitud y se le quitan los caracteres de control, porque una cabecera está influida por el atacante aunque el proxy no lo esté.
Sin ninguno de los dos flags, nada cambia y no se registra nunca un sujeto. Ese es el valor por defecto.
Un proyecto no puede gastarse el daemon entero
Un proyecto que inunda mcpd les cuesta a todos los demás proyectos que hay
en él: el disco se llena, la retención no da abasto, y el equipo que no hizo
nada mal es el que pierde ejecuciones. --quota-per-minute es el
tope que lo impide.
un tope para cada proyecto, y uno mayor para uno de ellos
mcpd serve --quota-per-minute 10000
Sin definir significa sin tope, así que un despliegue que no lo fija nunca
se comporta exactamente como antes de que existieran las cuotas. Fijado,
cubre tres cosas: cada proyecto del bloque projects que no
nombra su propio quota_per_minute, el proyecto nombrado por
--project, y cada proyecto que la configuración no
lista en absoluto.
Ese último es lo que lo convierte en un tope y no en una sugerencia. El nombre del proyecto llega dentro del cuerpo de la solicitud, así que a un emisor que elige un nombre que ningún archivo de configuración lista antes no se le cargaba nada y podía almacenar sin límite. Un límite del que el emisor puede salirse inventando una cadena no es un límite.
Todos los proyectos no listados comparten un solo contador en lugar de tener
uno cada uno. Un contador por nombre sería una reserva que cualquier emisor
podría dirigir inventando nombres, que es el mismo agujero con otra forma.
Compartirlo significa que una inundación repartida entre mil nombres
inventados queda acotada en total por este número, y significa que un
proyecto que merezca su propia asignación debería nombrarse en
projects, donde la obtiene.
Qué acota realmente el tope
El contador es del tipo ponderado de dos ventanas: la cuenta de este minuto, más la del minuto anterior ponderada por la parte de él que todavía cubre la ventana que termina ahora. Mantiene la tasa sostenida en el tope y permite además una ráfaga de hasta un tope entero, porque un SDK que vacía una cola llena llega como una sola ráfaga y un contador que la rechazara rechazaría tráfico corriente.
Esas dos cosas juntas tienen una consecuencia aritmética que conviene decir en lugar de esconder. Un proyecto puede almacenar algo menos del doble de su tope dentro de un intervalo de 60 segundos: un tope entero justo al final de una ventana, y otro tope entero justo al final de la siguiente. Eso no es un artefacto de la aproximación. Cualquier limitador que deje a un proyecto gastar su tope de golpe y otra vez su tope cada minuto después permite eso al cruzar una frontera, y la única forma de quitarlo es rechazar la ráfaga.
La ventana es de un minuto y no es configurable. El flag nombra la unidad, así que un segundo ajuste que cambiara lo que significa «por minuto» haría ilegible el primer número.
Qué pasa con el exceso
Las señales por encima del tope se rechazan y se informan, no se descartan nunca en silencio. El lote no se rechaza entero: mcpd almacena lo que cabe y te dice lo que no.
- Los spans van por traza entera. Media ejecución tiene spans cuyo padre no almacenó nadie, lo que se lee como una ejecución rota y no como una que falta. Una traza demasiado grande para lo que queda se salta y se prueba la siguiente, así que una ejecución grande no gasta en nada el resto de la asignación.
- Los logs, las métricas, las puntuaciones y el feedback se sostienen solos, así que llenan lo que quede en orden de llegada.
- Solo se cobra lo que se almacena. El recorte conserva trazas enteras y por eso puede almacenar menos señales de las que tenía permitidas, y la diferencia se devuelve. Sin eso, un proyecto cuyas ejecuciones son mayores que su asignación restante pagaría hasta llegar a cero sin almacenar nada.
- Los demás proyectos de la misma solicitud quedan intactos. Un proyecto por encima de su tope no le cuesta a otro las señales que envió a su lado.
La respuesta lleva la cuenta en rejected y una línea en
errors que nombra el proyecto y su tope. Un proyecto que la
configuración nunca listó recibe una frase distinta, porque que te digan que
el proyecto analytics-v2 está por encima de su tope te manda a
buscar un ajuste que no existe cuando la respuesta es añadir el proyecto:
$ 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 rechazo se cuenta en mcpd_signals_over_quota_total además de
en mcpd_signals_rejected_total, así que una flota a la que se
está limitando se distingue de una flota que envía datos mal formados.
Consulta Operación.
Dos cosas que comprobar cuando lo fijas
El tope cubre todas las puertas que almacenan una señal: las de lote,
/v1/ingest, /v1/traces, /v1/logs y
/v1/metrics, y las de registro único,
POST /v1/api/scores y POST /v1/api/feedback. Las
dos responden de forma distinta a un proyecto por encima de su tope, a
propósito. Una puerta de lote almacena lo que cabe e informa del resto como
rejected con un motivo, porque un lote puede nombrar varios
proyectos y rechazar la solicitud entera castigaría a los que están dentro
de su tope. Una puerta de registro único no tiene ninguna respuesta parcial
que dar, así que rechaza con 429 y una cabecera
Retry-After que lleva los segundos que faltan para que el
contador de ese proyecto admita otra señal. El número sale del propio
contador, así que un cliente que lo respeta vuelve cuando hay sitio y no
cuando se cumple un retardo fijo.
Hay una puerta de escritura que no se le carga: POST /v1/api/score,
que ejecuta evaluadores sobre ejecuciones almacenadas. Entrega las
puntuaciones que produjo directamente al almacenamiento, así que una pasada
cae en disco para un proyecto cuyo tope ya está gastado, y esas filas no las
cuenta ni mcpd_signals_over_quota_total ni
mcpd_signals_unbounded_total. Es una puerta que abres tú y no
una a la que apunta una flota, así que trata el tope como un límite de lo
que los emisores pueden empujar hacia dentro, no de todo lo que puede llegar
al disco.
Un tope por debajo de --max-batch-signals, 10,000 por defecto,
significa que un lote legal puede superar la asignación de todo un minuto y
se recorta por muy bien que se comporte el emisor. mcpd doctor
califica esto bajo limits y nombra los proyectos a los que se
aplica. También avisa cuando se fija un tope por proyecto y no se fija uno
de nivel superior a su lado, que es la configuración que deja sin límite a
todos los proyectos no listados.
mcpd config imprime por su nombre los proyectos limitados, para
que puedas leer qué cubre el ajuste en lugar de inferirlo. Imprime solo los
topes nombrados, así que confirma el compartido con el propio flag.