Ir al contenido
mcpd
sin daemon
Código

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 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

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_ENDPOINThttp://localhost:4317
MCPD_API_KEYsin definir
MCPD_PROJECTdefault
MCPD_ENVIRONMENTsin definir
MCPD_SERVICE_NAMEsin definir
MCPD_SERVICE_VERSIONsin definir
MCPD_SAMPLE1.0
MCPD_ENABLEDtrue
MCPD_CAPTURE_PAYLOADStrue
MCPD_LOG_LEVELdebug

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:

ocho señales a un proyecto que nadie configuró, con el tope compartido en cinco
$ 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.