Ir para o conteúdo
mcpd
sem daemon
Código

Configuração

Cada ajuste é uma opção, e cada opção tem uma variável de ambiente com o mesmo significado. As poucas estruturadas demais para uma linha de comando ficam em um único arquivo JSON. Esta página traz todas.

Duas formas de dizer a mesma coisa

Rode o mcpd sem argumento nenhum e ele funciona: escuta em 0.0.0.0:4317, guarda os dados em ./mcpd.db, oculta segredos e armazena todo trace para sempre. Tudo o que vem abaixo muda uma dessas decisões.

Uma opção sempre vence a variável de ambiente correspondente. As duas são globais, então são aceitas antes ou depois de um subcomando.

Referência

Opção Variável Padrão
--listen MCPD_LISTEN 0.0.0.0:4317
--data MCPD_DATA ./mcpd.db
--project MCPD_PROJECT default
--retention-days MCPD_RETENTION_DAYS não definido: guarda tudo
--ingest-key MCPD_INGEST_KEYS nenhuma: a ingestão é aberta
--read-key MCPD_READ_KEYS nenhuma: a leitura é aberta
--write-key MCPD_WRITE_KEYS nenhuma: as chaves de ingest são aceitas
--admin-key MCPD_ADMIN_KEYS nenhuma: as chaves de leitura são aceitas
--execute-key MCPD_EXECUTE_KEYS nenhuma: as chaves de escrita são aceitas
--worker-key MCPD_WORKER_KEYS nenhuma: as chaves de escrita são aceitas
--runs-data MCPD_RUNS_DATA o caminho de --data com .runs antes da extensão
--trusted-identity-header MCPD_TRUSTED_IDENTITY_HEADER nenhum: nenhum sujeito é registrado
--trusted-proxy-cidr MCPD_TRUSTED_PROXY_CIDRS nenhum: nenhum endereço pode definir esse cabeçalho
--sample MCPD_SAMPLE não definido: guarda todo trace
--quota-per-minute MCPD_QUOTA_PER_MINUTE não definido: sem teto
--no-redact MCPD_NO_REDACT desligado: a ocultação está ligada
--redact-key MCPD_REDACT_KEY uma chave embutida, que é pública
--redact-key-contains MCPD_REDACT_KEY_CONTAINS nenhuma: apenas as regras embutidas
--redact-max-string MCPD_REDACT_MAX_STRING 128 KiB
--cors-origin MCPD_CORS_ORIGINS nenhuma: apenas a mesma origem
--request-timeout-seconds MCPD_REQUEST_TIMEOUT_SECONDS 30
--max-body-bytes MCPD_MAX_BODY_BYTES 33554432, que são 32 MiB
--max-batch-signals MCPD_MAX_BATCH_SIGNALS 10000
--log-format MCPD_LOG_FORMAT text
--clickhouse-url MCPD_CLICKHOUSE_URL desligado: o arquivo embutido é usado
--clickhouse-database MCPD_CLICKHOUSE_DATABASE mcpd
--clickhouse-user MCPD_CLICKHOUSE_USER nenhum
--clickhouse-password MCPD_CLICKHOUSE_PASSWORD nenhuma
--postgres-url MCPD_POSTGRES_URL desligado: o arquivo embutido é usado
--config-file MCPD_CONFIG_FILE nenhum
--forward-endpoint MCPD_FORWARD_ENDPOINT desligado: nada é encaminhado
--forward-header MCPD_FORWARD_HEADERS nenhum
nenhuma MCPD_LOG mcpd=info,mcpd_server=info,tower_http=warn

--listen

O endereço em que escutar. Uma única porta serve os endpoints OTLP, o endpoint de ingestão nativo, a API de consulta, as sondas, as métricas e a interface, então este é o único endereço que o mcpd tem.

Escutar em um endereço de loopback é a escolha discreta: o mcpd só avisa sobre ingestão aberta quando o endereço em que escuta não é loopback. Em 0.0.0.0 e sem chave nenhuma definida, ele avisa duas vezes na partida, uma pela ingestão e outra pela leitura.

--data

Caminho do arquivo SQLite. Os diretórios pai são criados se não existirem. Passe :memory: para um armazenamento que não deixa nada para trás, o que é certo para uma demonstração ou um teste e errado para qualquer coisa que você queira ver amanhã.

--project

O projeto em que um sinal cai quando o remetente não nomeou nenhum. Um projeto é a fronteira de isolamento: toda consulta é limitada a um. A passagem de retenção roda sobre ele e sobre todo projeto do bloco projects; um projeto que não aparece em nenhum dos dois lugares nunca é aparado, o que importa se seus remetentes nomeiam o projeto deles.

--retention-days

Apaga sinais mais velhos que esta quantidade de dias. Não definido, guarda tudo, que é o padrão correto para um notebook e uma decisão que vale tomar de propósito em qualquer outro lugar.

A janela vai de 1 a 36500 dias, aqui e no retention_days de um projeto. Zero é recusado na partida em vez de ser lido como ausência de retenção: ele poria o corte em agora, e a primeira passada roda na partida, então apagaria todos os sinais do projeto. Para guardar tudo, deixe o ajuste de fora.

A passagem roda em um temporizador de hora em hora, e não no momento da consulta, então um apagamento grande nunca cai dentro da requisição de um usuário. Spans, traces, logs e métricas são cortados pela hora em que o mcpd os recebeu; notas, feedback e notas de aspecto, pelo carimbo de tempo deles. Apagar linhas não encolhe o arquivo. Veja Como executar.

--ingest-key, --read-key, --write-key, --admin-key, --execute-key e --worker-key

Seis escopos, porque eles carregam riscos genuinamente diferentes. Cada endpoint declara o escopo de que precisa em spec/mcpd.spec.json, o handler correspondente é marcado com o mesmo escopo, e make check compara os dois, então um escopo documentado aqui é o escopo que o servidor em execução aplica.

Escopo Definido com O que abre
ingest --ingest-key Os quatro endpoints de ingestão. Não lê nada.
read --read-key A API de consulta: traces, spans, logs, métricas, notas, análises.
write --write-key Notas, feedback, datasets, experimentos, reconhecimentos e a captura de uma execução para um dataset.
admin --admin-key Apagar um trace, ler o log de auditoria e tudo o que os dois escopos abaixo abrem.
execute --execute-key Iniciar, cancelar e sinalizar uma execução, e publicar um evento nela.
worker --worker-key Registrar definições, reivindicar etapas e relatar o resultado delas. Não lê trace nenhum.

A separação que mais importa é ingest contra todo o resto. Uma chave de ingest vive em todo processo que emite um span, inclusive nos que rodam código que você não escreveu. Dar a essa população uma credencial que também consegue ler de volta todo prompt armazenado é um erro que o mcpd não torna fácil: uma chave de ingest apresentada à API de consulta é recusada.

write é separado de ingest pela mesma razão, em escala menor. Capturar uma execução para um dataset lê a entrada e a saída dessa execução, ou seja, é uma leitura vestida de escrita e não pertence à chave que a sua frota carrega.

admin é separado de read porque apagar uma execução não é lê-la. Até você definir --admin-key, uma chave de leitura apaga além de ler, e o mcpd diz isso na partida.

execute e worker são os dois escopos da superfície de execução. Iniciar uma execução faz o worker de alguém trabalhar e gastar dinheiro, o que anotar um trace não faz, então não é write. Uma chave de worker vive no processo que roda os corpos das etapas, que normalmente é o processo que guarda a credencial do provedor, então ela abre o protocolo de lease e nada que leia um prompt armazenado. Até você definir as duas opções, as chaves de escrita são aceitas para ambos, e uma chave de admin sempre é.

Atualizar sem parada

Um --write-key não definido aceita as chaves de ingest, e um --admin-key não definido aceita as chaves de leitura. Uma implantação configurada antes de esses dois escopos existirem mantém, então, exatamente o alcance que tinha, e nada quebra na atualização. Defina as duas opções para assumir as concessões mais estreitas; mcpd config diz em qual estado você está: imprime key required quando o escopo tem chaves próprias e key required (ingest keys) ou key required (read keys) quando ele as está herdando.

Repita qualquer uma das seis opções para ter várias chaves, que é como se gira uma chave sem parada. As variáveis de ambiente aceitam uma lista separada por vírgulas. Quem chama apresenta a chave como Authorization: Bearer KEY ou como X-Mcpd-Key: KEY, e a comparação é de tempo constante, então nem o comprimento nem a posição da primeira diferença são observáveis.

Uma chave que o mcpd não conhece é um 401. Uma chave que o mcpd conhece, usada em um endpoint que o escopo dela não cobre, é um 403. Vale manter os dois separados: um 401 manda você caçar um erro de digitação em uma chave que está perfeitamente correta.

Sem chave nenhuma configurada, todo escopo fica aberto. Isso é de propósito, porque a primeira execução não pode exigir uma credencial, e é por isso que o aviso de partida existe.

Note o que as chaves não cobrem: /healthz, /readyz, /metrics e a própria interface são servidos sem chave nesta versão. Ponha-os atrás do seu próprio ingress se isso importa para você.

Uma entrada vazia é descartada antes que qualquer coisa leia a lista. Uma lista definida que não contém nenhuma chave, como MCPD_EXECUTE_KEYS= ou um valor vazio em um Secret do Kubernetes, impede o daemon de iniciar, e a recusa nomeia a opção e a variável; omita a variável para deixar o escopo não definido, e uma requisição que envia um cabeçalho de chave vazio não apresenta chave nenhuma. Uma chave que ainda é um marcador dos arquivos de exemplo em deploy/, qualquer valor que comece com REPLACE_WITH ou replace-me em maiúsculas ou minúsculas, impede o daemon de iniciar; a recusa nomeia a opção ou a lista do projeto e nunca o valor. O mesmo vale para --redact-key. Gere cada chave com openssl rand -hex 32. Sem --write-key, --execute-key nem --worker-key, as chaves de ingest também iniciam execuções e reivindicam passos, e o mcpd avisa disso ao iniciar e no mcpd doctor.

O log de auditoria

Os handlers que devolvem payloads registram quem leu o quê: GET /v1/api/traces/{id}, as buscas de span e de log, o feedback, os itens de dataset, a captura e o apagamento. Cada linha carrega a hora, um resumo curto da chave que agiu, a ação, o projeto, o recurso e se a autorização permitiu. A chave em si nunca é anotada.

Leia o log com GET /v1/api/audit, que exige o escopo de admin. Ele não é aparado por --retention-days: um log que se apagasse no mesmo ritmo dos dados que descreve não poderia dizer quem leu os dados que já se foram. Escrever uma linha nunca faz falhar a leitura que ela descreve; uma linha que não pode ser escrita é logada e contada como mcpd_audit_write_failures_total em /metrics, o que merece um alerta.

--sample

Uma fração entre 0.0 e 1.0. Um valor fora dessa faixa é um erro de partida, e não um corte silencioso: --sample must be between 0.0 and 1.0, got 2.

A decisão é tomada aplicando um hash ao id do trace, então uma frota de instâncias do mcpd atrás de um balanceador de carga guarda e descarta os mesmos traces sem conversar entre si. Ela é tomada uma vez por trace e herdada por todo span daquele trace no lote, porque um trace gravado pela metade parece um defeito no sistema que você está observando.

O hash lê os bytes finais do id do trace. Alguns produtores derivam os bytes iniciais de um carimbo de tempo, e um amostrador que olhasse para esses bytes se correlacionaria com a hora do dia e descartaria horas inteiras em silêncio.

O tipo de política também carrega "sempre guardar erros" e "sempre guardar mais lento que", mas isso vale para uma decisão de cauda, tomada depois que um trace termina. Esta versão amostra só na cabeça, e nenhuma opção liga a amostragem de cauda. Amostragem trata da troca.

--no-redact, --redact-key, --redact-key-contains e --redact-max-string

A ocultação vem ligada e roda antes de qualquer coisa ser escrita, então um valor que nunca deveria ter sido persistido nunca é. Apagá-lo depois não é a mesma garantia. Três conjuntos de regras vêm embutidos, casados por substrings em minúsculas do nome de uma chave:

Regra Aplicada quando a chave contém O que é armazenado
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 Os dois primeiros e os dois últimos caracteres, ab…yz. Um valor de seis caracteres ou menos é mascarado por inteiro, em vez de vazar quase todo ele
Hash user_id, userid sha256: e 12 bytes de um resumo com chave, para você ainda poder agrupar por usuário sem armazenar nenhum

Uma chave casada que guarda um objeto ou um array tem tudo abaixo dela ocultado também, então {"credentials": {"token": "..."}} não escapa só pelo nome aninhado. Qualquer string única maior que 128 KiB é truncada com um marcador que nomeia a contagem de bytes, o que limita o que um prompt desgovernado faz com o seu disco.

--redact-key é a chave da regra de hash. Entradas iguais geram o mesmo hash sob uma chave, que é o que faz o agrupamento funcionar, e girar a chave quebra esse vínculo de propósito. Defina-a em qualquer implantação que use hash: o padrão embutido está no código-fonte, então resumos sem chave são adivinháveis.

--redact-key-contains acrescenta uma regra de máscara sua às três embutidas. As regras embutidas conhecem os nomes sob os quais credenciais costumam ser guardadas; elas não conhecem o campo em que as suas próprias ferramentas põem o número de conta de um cliente. O valor é casado do mesmo jeito que as regras embutidas, como uma substring em minúsculas da chave, então --redact-key-contains ticket mascara ticket_id e supportTicket igualmente, e tudo o que estiver aninhado sob uma chave casada por elas. Repita a opção para várias, ou defina MCPD_REDACT_KEY_CONTAINS com uma lista separada por vírgulas.

--redact-max-string baixa o limite de comprimento a partir dos 128 KiB embutidos. Uma string maior que o limite é cortada ali, com um marcador que nomeia quantos bytes foram descartados, antes de ser armazenada.

--no-redact desliga todo o pipeline, inclusive o limite de comprimento. Como variável de ambiente, é MCPD_NO_REDACT=true. Só faz sentido quando algo antes dele já oculta, e o daemon se recusa a iniciar se qualquer um dos dois acréscimos acima estiver definido ao lado dele, porque não há nada a que eles se apliquem.

--runs-data

Onde mora o armazenamento das execuções: as definições, o diário das tentativas de cada etapa, cada prazo e cada espera. É um arquivo embutido, seja qual for o armazenamento de sinais, então em Postgres ou ClickHouse as execuções continuam em um arquivo ao lado do daemon, e a superfície de execução é servida por uma réplica. Por padrão, é o caminho de --data com .runs antes da extensão, ./mcpd.runs.db no caso do padrão, e :memory: quando --data é. --runs-data off não serve execução nenhuma: todo endpoint de execução responde 503 nomeando esta opção, e o resto do daemon fica igual.

Os tempos do armazenamento ficam no arquivo de configuração, sob runs, em milissegundos inteiros: lease_ttl_ms, reclaim_window_ms, stall_window_ms, idempotency_window_ms, park_ttl_ms e max_depth. Os padrões, um lease de 30 segundos e uma janela de recuperação de 30 segundos, são certos para um daemon em uma máquina e são o que um worker lê de volta quando se registra.

--cors-origin

Quais origens de navegador podem chamar a API de consulta a partir de outro site. Sem nenhuma definida, uma requisição de outra origem não recebe cabeçalho access-control-allow-origin e o navegador se recusa a entregar a resposta à página. Esse é o padrão, e é o que impede que um daemon no seu notebook, com a API de leitura aberta, seja legível por qualquer página aberta no mesmo navegador. A interface incluída é servida pela própria origem do daemon e nunca precisa desta opção.

Servir a interface de outro lugar, uma CDN ou um portal interno, é o caso para o qual esta opção existe: nomeie essa origem, como esquema e host, por exemplo --cors-origin https://console.example. Repita a opção para várias, ou defina MCPD_CORS_ORIGINS com uma lista separada por vírgulas. Um valor que não é uma origem, console.example ou um com barra no fim, recusa a partida nomeando a opção, em vez de deixar você com uma interface que não consegue conectar e nenhuma razão para isso. --cors-origin '*' permite toda origem, que é o que versões anteriores faziam sem perguntar.

/healthz, /readyz e /metrics não são governados por esta opção. Eles não carregam dado armazenado, e a interface sonda /healthz antes de fazer qualquer outra coisa, então continuam legíveis a partir de qualquer origem.

--request-timeout-seconds

O máximo que uma requisição pode levar, leitura do corpo incluída, antes de o daemon responder por ela. Um handler que passa do prazo recebe como resposta um 503 cujo corpo carrega timeout e o nome desta opção. O padrão é 30 segundos, o que é longo o bastante para um lote grande ser lido e armazenado, e curto o bastante para que um armazenamento que aceita conexões e nunca responde seja relatado em vez de esperado. Não dá para desligar; aumente se uma operação realmente precisar de mais tempo.

--max-body-bytes e --max-batch-signals

Os dois limites de uma requisição. --max-body-bytes é o maior corpo que o daemon lê, 32 MiB por padrão, e é o que decide quanta memória uma requisição pode tomar antes de qualquer parte dela ser decodificada. Um corpo maior é recusado com 413 nomeando os dois números, e um corpo comprimido é medido pelo mesmo número depois de inflado, então um corpo que se expande mil vezes é recusado em vez de expandido.

--max-batch-signals é quantos sinais uma requisição pode carregar depois de decodificada, 10.000 por padrão. É contado por toda a requisição, então um array de lotes e um fluxo NDJSON são submetidos ao mesmo número que um único lote. Nada é armazenado quando uma requisição passa do limite: a recusa vem antes da primeira escrita, e não depois de metade delas.

Os dois também são chaves do arquivo de configuração, max_body_bytes e max_batch_signals, e a opção vence onde os dois estiverem definidos. Nenhum dos dois pode ser desligado, e zero é recusado na partida em vez de ser lido como "sem limite": um limite de corpo igual a zero recusa toda requisição, então a leitura que parece generosa é a que derruba a ingestão.

--log-format e MCPD_LOG

text para um terminal, json para um pipeline de logs. MCPD_LOG aceita uma string de filtro do tracing e não tem opção, porque é um controle de depuração e não uma decisão de implantação. Defina-a como mcpd_server=debug quando quiser ver detalhe por requisição.

Onde valem os dois limites de requisição

O limite de corpo vale para toda rota que lê um corpo, API de consulta incluída. O limite de sinais vale para as portas de ingestão, e é isto que ele conta em cada uma delas.

Porta O que o limite de sinais conta
POST /v1/ingest Cada span, log, métrica, nota, pedaço de feedback e nota de aspecto da requisição, somados por tudo o que houver nela, tenha chegado como um lote, um array de lotes ou um fluxo de linhas NDJSON
POST /v1/traces Os spans que o remetente enviou, contados antes de serem mapeados, que é o número sobre o qual o remetente pode agir
POST /v1/logs Os registros de log da exportação
POST /v1/metrics Os pontos de dados da exportação, por toda métrica que houver nela

Subcomandos

Comando O que faz
mcpd serve Roda o daemon. É o que acontece sem subcomando nenhum
mcpd demo Escreve uma execução de exemplo realista no armazenamento e depois serve, para a interface ter algo dentro dela na primeira vez que você a abrir
mcpd config Imprime a configuração que esta invocação usaria e sai
mcpd mcp Serve o Model Context Protocol por stdio, para um agente poder ler os próprios traces de um daemon em execução
mcpd doctor Dá uma nota a esta implantação e relata o que está mal configurado. Sai com 2 diante de uma falha, então pode servir de portão para uma implantação. Veja Como executar

mcpd config existe por causa da discussão que você está prestes a ter com um contêiner. Opções, variáveis de ambiente e padrões acabam todos em um lugar só, e isto imprime esse lugar sem iniciar um servidor nem tocar no arquivo de dados:

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

Leia as seis linhas de autenticação antes de implantar qualquer coisa. open quer dizer aberto a quem alcançar a porta. Uma linha entre parênteses nomeia a lista mais ampla de que aquele escopo está herdando, que é o estado em que começa uma implantação atualizada a partir de uma versão mais antiga.

Uma implantação configurada

Nada aqui é específico do mcpd: as variáveis são ambiente comum, então funcionam do mesmo jeito no Compose, em um manifesto do Kubernetes e em uma unidade do 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:

Os mesmos ajustes como opções, para um shell ou um arquivo de unidade:

mcpd --listen 0.0.0.0:4317 --data /data/mcpd.db --retention-days 30 --ingest-key $KEY --log-format json

O que os SDKs leem

Estas variáveis são lidas pelo processo do seu agente, não pelo daemon. Elas estão listadas aqui porque os dois conjuntos compartilham nomes, e um contêiner que roda um agente e o mcpd é configurado por um ambiente só.

Variável Padrão
MCPD_ENDPOINThttp://localhost:4317
MCPD_API_KEYnão definido
MCPD_PROJECTdefault
MCPD_ENVIRONMENTnão definido
MCPD_SERVICE_NAMEnão definido
MCPD_SERVICE_VERSIONnão definido
MCPD_SAMPLE1.0
MCPD_ENABLEDtrue
MCPD_CAPTURE_PAYLOADStrue
MCPD_LOG_LEVELdebug

MCPD_PROJECT e MCPD_SAMPLE significam a mesma coisa dos dois lados, que normalmente é o que você quer: defina uma vez e o agente e o daemon concordam sobre o projeto. A amostragem é a que exige pensar. Um SDK descarta um trace antes de ele ser enviado, o que poupa rede. O daemon o descarta antes de ser armazenado, que é o único lugar onde a taxa pode ser imposta a remetentes que você não controla. Defina a taxa em um lugar, não nos dois, ou você estará filtrando um fluxo já filtrado.

MCPD_ENABLED=false faz de toda chamada de SDK uma operação sem efeito que ainda devolve objetos válidos, então o mcpd pode ser desligado em testes sem mudar código nenhum. MCPD_CAPTURE_PAYLOADS=false mantém prompts e respostas dentro do processo: os spans ainda chegam com o tempo, o uso, o modelo e os erros deles, mas input e output são descartados antes de entrarem na fila, então nunca atravessam a rede. A ocultação no daemon é a outra ponta do mesmo fio, e a ponta errada para um time que não pode enviar os bytes de jeito nenhum. O detalhe por linguagem está em cada página de SDK.

--config-file

Um arquivo JSON com os ajustes que não cabem em uma linha de comando: monitores, objetivos de nível de serviço, o intervalo em que são avaliados, o encaminhamento, o juiz, os tempos do armazenamento de execuções, os dois limites de requisição e o bloco por projeto que vem abaixo. JSON, e não YAML ou TOML, porque o mcpd já interpreta JSON para todo o resto, então o arquivo não acrescenta dependência nem uma segunda sintaxe a aprender. Ele monta como um ConfigMap do Kubernetes sem mudança nenhuma.

max_body_bytes e max_batch_signals estão aqui além da linha de comando, para uma frota poder mantê-los ao lado dos monitores com que são implantados. Uma opção vence o arquivo, que é como uma instância assume um limite diferente sem que o arquivo compartilhado seja editado para todo mundo.

Uma chave desconhecida faz a partida falhar em vez de ser ignorada, porque um erro de digitação que silenciosamente não observa nada é o pior desfecho disponível.

O mesmo vale para um valor com o qual um temporizador em segundo plano não consegue rodar: um objective que não seja pelo menos 0 e menor que 1, um window_days de objetivo fora de 1 a 365, um monitor_interval_seconds acima de 86400, um aspects.window_seconds fora de 1 a 31536000, um aspects.interval_seconds acima de 604800 e um judge.timeout_seconds fora de 1 a 300. Cada recusa diz a chave e o intervalo.

{
  "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
  }
}

Projetos, e as chaves que os alcançam

Um projeto é a chave de isolamento. Listar chaves sob um projeto é o que torna isso verdadeiro em vez de decorativo: uma chave no bloco de um projeto alcança aquele projeto e nenhum outro, então um daemon serve vários times sem que nenhum deles consiga ler os prompts dos outros.

{
  "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
    }
  }
}

As seis listas existem por projeto, como ingest_keys, read_keys, write_keys, admin_keys, execute_keys e worker_keys, e recuam do mesmo jeito: um projeto sem write_keys aceita as próprias chaves de ingest, e um sem admin_keys aceita as próprias chaves de leitura. O bloco support acima não define nenhuma das duas, então se comporta exatamente como se comportava antes de os dois escopos existirem.

Uma chave definida com qualquer uma das seis opções de nível superior alcança todo projeto. Esse é o caminho de inquilino único, e ele continua sendo o caminho simples: se você nunca abrir este bloco, nunca encontra nada disto.

retention_days e sample recuam para as opções globais quando um projeto não os define. Os dois são por projeto pela mesma razão que as chaves são: a exigência de noventa dias de um time não deveria obrigar todo mundo a guardar os dados, e o tráfego de um time não deveria decidir como outro time é amostrado.

Um lote pode nomear vários projetos. Uma chave de ingest com escopo escreve os sinais que ela alcança e recusa o resto, relatando a recusa na resposta em vez de descartá-los em silêncio, então uma frota apontada para o projeto errado fica visível na resposta que ela já está lendo.

Uma requisição de leitura para um projeto que a sua chave não alcança é recusada sem dizer se aquele projeto existe. Em um daemon compartilhado, os nomes dos projetos costumam ser os nomes de times ou de clientes.

Monitores e objetivos de nível de serviço são tratados em Monitores e alertas; os objetivos vão em um array slos ao lado de monitors e são avaliados no mesmo temporizador. O bloco judge liga a avaliação por juiz e é tratado em Testes de regressão. Ele fica desligado até você defini-lo, e um avaliador por juiz nunca roda a menos que uma requisição o nomeie, porque julgar gasta dinheiro a cada execução que lê.

{
  "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 é de quanto em quanto tempo os monitores e os objetivos são avaliados. O padrão é 60, e um valor abaixo de 5 é elevado a 5.

derived_metrics

Quais métricas o daemon deriva dos spans que guarda. As duas metades ficam ligadas quando o bloco não existe. durations grava um ponto de duração para cada span terminado de agente, ferramenta, workflow, geração de modelo ou processador. tokens grava até treze contadores de tokens para cada span de geração de modelo que informou o uso. Defina qualquer uma das duas como false para parar de gravá-la; um campo omitido continua ligado. Não há opção de linha de comando para isso. Os pontos derivados entram depois do teto por projeto e não contam no número accepted de uma resposta de ingestão.

{
  "derived_metrics": { "durations": true, "tokens": false }
}

aspects

Se o daemon agrupa as notas de aspecto em temas, e sob quais limites. Desligado quando o bloco não existe. enabled liga a passada e não precisa de modelo nem de credencial, porque o próprio daemon grava o aspecto outcome. interval_seconds é de quanto em quanto tempo uma passada roda, 300 por padrão, e um valor abaixo de 30 é elevado a 30. window_seconds é até onde uma passada lê notas, 86400 por padrão. disabled nomeia aspectos que nenhuma passada agrupa; as notas já gravadas sob um deles continuam legíveis.

{
  "aspects": {
    "enabled": true,
    "interval_seconds": 300,
    "window_seconds": 86400,
    "disabled": ["sentiment"]
  }
}

--forward-endpoint

Copia todo span armazenado para outro endpoint OTLP/HTTP. Dê a URL base; /v1/traces é acrescentado. A escrita local sempre acontece primeiro, então um destino fora do ar não custa nada a você.

Esta opção vence o bloco forward do arquivo de configuração, então uma instância pode ser apontada para outro lugar sem editar um ConfigMap compartilhado. Veja Encaminhamento.

--forward-header

Um cabeçalho enviado com todo lote encaminhado, escrito como Name: value. Repita a opção para mais de um. A variável de ambiente aceita uma lista separada por vírgulas.

Um valor sem dois-pontos é recusado na partida, com a forma que funciona, em vez de ser descartado. Um cabeçalho no estilo do OpenTelemetry, key=value, aqui se escreve key: value.

--clickhouse-url

O endpoint HTTP de um servidor ClickHouse, por exemplo http://clickhouse:8123. Defini-lo troca o armazenamento do arquivo embutido para o ClickHouse, e --data deixa de ser lido. O esquema é criado na partida.

Este é o ajuste que remove os limites de um escritor e de uma réplica do armazenamento embutido. Recorra a ele quando a retenção for medida em meses de tráfego de produção, e não antes: Como executar trata da troca.

--clickhouse-database, --clickhouse-user, --clickhouse-password

O banco em que criar as tabelas, e as credenciais com que alcançá-lo. Um usuário sem senha é enviado sem uma, o que é certo para um contêiner em uma rede privada e errado em qualquer outro lugar.

--postgres-url

Uma string de conexão do Postgres, por exemplo postgres://mcpd:secret@db:5432/mcpd. Defini-la troca o armazenamento do arquivo embutido para o Postgres, e --data deixa de ser lido. O esquema é criado na partida.

Esta é a opção do meio. Ela remove os mesmos limites de um escritor e de uma réplica que o ClickHouse remove, sem pedir que você rode um banco que ainda não tem. Nomear esta e --clickhouse-url faz a partida falhar em vez de escolher uma das duas.

A migração toma um advisory lock, então várias réplicas partindo juntas é seguro, e uma partida sem nada a aplicar não toca em esquema nenhum.

Para TLS, acrescente sslmode=require à string de conexão. O mcpd então cifra a conexão e sempre verifica o certificado do servidor. Ele o confere com as autoridades certificadoras públicas, ou com a de sslrootcert=/caminho/para/ca.pem, que é o que um serviço gerenciado com autoridade própria exige. verify-ca e verify-full são lidos como require. Com prefer, o padrão, ou com disable, a conexão não é cifrada.

Registrar quem, e não só qual chave

O log de auditoria anota um resumo da chave que agiu. Isso responde "uma chave de admin apagou esta execução" e não "qual pessoa apagou". O mcpd não tem provedor de identidade, então a identidade tem de vir do ingress que já está na frente dele.

Dê a ele o cabeçalho que o seu proxy define, e os endereços autorizados a defini-lo:

tome a identidade de um ingress em que você confia

mcpd serve \ --trusted-identity-header x-forwarded-user \ --trusted-proxy-cidr 10.42.0.0/16

As duas metades são obrigatórias, e a lista de endereços é a razão de isto ser seguro. Um cabeçalho é uma string que qualquer um capaz de abrir um socket para a porta pode enviar, então o mcpd só o lê de um endereço que você nomeou. Uma requisição de qualquer outro lugar é servida normalmente e a linha de auditoria dela não carrega sujeito, que é a resposta honesta em vez de um palpite. O valor também tem o comprimento limitado e é despido de caracteres de controle, porque um cabeçalho sofre influência de quem ataca mesmo quando o proxy não sofre.

Sem nenhuma das duas opções definidas, nada muda e nenhum sujeito é jamais registrado. Esse é o padrão.

Um projeto não pode gastar o daemon inteiro

Um projeto que inunda o mcpd custa caro a todo outro projeto nele: o disco enche, a retenção não dá conta, e o time que não fez nada de errado é o que perde execuções. --quota-per-minute é o teto que impede isso.

um teto para todo projeto, e um maior para um deles

mcpd serve --quota-per-minute 10000

Não definido quer dizer sem teto, então uma implantação que nunca o define se comporta exatamente como se comportava antes de as cotas existirem. Definido, ele cobre três coisas: todo projeto do bloco projects que não nomeia o próprio quota_per_minute, o projeto nomeado por --project e todo projeto que a configuração não lista de jeito nenhum.

Este último é a parte que faz dele um teto e não uma sugestão. O nome do projeto chega dentro do corpo da requisição, então um remetente que escolhe um nome que arquivo de configuração nenhum lista era cobrado de ninguém e podia armazenar sem limite. Um limite do qual o remetente escapa inventando uma string não é um limite.

Todo projeto não listado divide um contador, em vez de cada um ganhar o seu. Um contador por nome seria uma alocação que qualquer remetente poderia mover inventando nomes, que é o mesmo buraco em outra forma. Dividir quer dizer que uma inundação espalhada por mil nomes inventados é limitada no total por este número, e quer dizer que um projeto que merece a própria cota deve ser nomeado em projects, onde ele ganha uma.

O que o teto realmente limita

O contador é do tipo ponderado de duas janelas: a contagem deste minuto, mais a do minuto anterior ponderada por quanto dele a janela que termina agora ainda cobre. Ele segura a taxa sustentada no teto e permite um pico de até um teto inteiro por cima disso, porque um SDK esvaziando uma fila cheia chega como um pico e um contador que o recusasse recusaria tráfego comum.

Esses dois juntos têm uma consequência aritmética que vale enunciar em vez de esconder. Um projeto pode armazenar um pouco menos que o dobro do teto dentro de um intervalo de 60 segundos: um teto inteiro bem no fim de uma janela, e um teto inteiro de novo bem no fim da seguinte. Isso não é um artefato da aproximação. Qualquer limitador que deixe um projeto gastar o teto de uma vez e o teto de novo por minuto depois disso permite isso na virada, e o único jeito de tirar é recusar o pico.

A janela é de um minuto e não é configurável. A opção nomeia a unidade, então um segundo ajuste que mudasse o que "por minuto" quer dizer tornaria o primeiro número ilegível.

O que acontece com o excesso

Sinais acima do teto são recusados e relatados, nunca descartados em silêncio. O lote não é recusado inteiro: o mcpd armazena o que cabe e diz a você o que não coube.

  • Spans vão por trace inteiro. Meia execução tem spans cujo pai ninguém armazenou, o que se lê como uma execução quebrada e não como uma execução faltante. Um trace grande demais para o que sobrou é pulado e o próximo é tentado, então uma execução grande não gasta o resto da cota à toa.
  • Logs, métricas, notas e feedback são independentes, então preenchem o que sobrou na ordem de chegada.
  • Só o que é armazenado é cobrado. O corte guarda traces inteiros e por isso pode armazenar menos sinais do que lhe foi permitido, e a diferença é devolvida. Sem isso, um projeto cujas execuções são maiores que a cota restante pagaria até zerar sem armazenar nada.
  • Outros projetos da mesma requisição ficam intocados. Um projeto acima do teto dele não custa a outro os sinais que este enviou junto.

A resposta carrega a contagem em rejected e uma linha em errors nomeando o projeto e o teto dele. Um projeto que a configuração nunca listou recebe outra frase, porque ouvir que o projeto analytics-v2 está acima do teto manda você procurar um ajuste que não existe, quando a resposta é acrescentar o projeto:

oito sinais para um projeto que ninguém configurou, com o teto compartilhado em 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."    }  ]}

Uma recusa é contada em mcpd_signals_over_quota_total além de em mcpd_signals_rejected_total, então uma frota sendo limitada se distingue de uma frota enviando dados malformados. Veja Como executar.

Duas coisas para conferir quando você o definir

O teto cobre toda porta que armazena um sinal: as de lote, /v1/ingest, /v1/traces, /v1/logs e /v1/metrics, e as de registro único, POST /v1/api/scores e POST /v1/api/feedback. As duas respondem de propósito de formas diferentes a um projeto acima do teto. Uma porta de lote armazena o que cabe e relata o resto como rejected com um motivo, porque um lote pode nomear vários projetos e recusar a requisição inteira puniria os que estão dentro do teto. Uma porta de registro único não tem resposta parcial a dar, então recusa com 429 e um cabeçalho Retry-After com os segundos que faltam até o contador daquele projeto admitir outro sinal. O número vem do próprio contador, então um cliente que o respeita volta quando há espaço, e não quando um atraso fixo terminou.

Uma porta de escrita não é cobrada nele: POST /v1/api/score, que roda avaliadores sobre execuções armazenadas. Ela entrega as notas que produziu direto ao armazenamento, então uma passagem cai no disco para um projeto cujo teto já está gasto, e essas linhas não são contadas nem por mcpd_signals_over_quota_total nem por mcpd_signals_unbounded_total. É uma porta que você mesmo abre, e não uma para a qual uma frota aponta, então trate o teto como um limite do que os remetentes conseguem empurrar para dentro, e não de tudo o que pode chegar ao disco.

Um teto abaixo de --max-batch-signals, 10.000 por padrão, quer dizer que um lote legítimo pode passar da cota de um minuto inteiro e é cortado por mais bem comportado que o remetente seja. mcpd doctor dá nota a isso sob limits e nomeia os projetos a que se aplica. Ele também avisa quando um teto é definido por projeto e nenhum teto de nível superior é definido ao lado, que é a configuração que deixa todo projeto não listado sem limite.

mcpd config imprime os projetos limitados pelo nome, para você poder ler de volta o que o ajuste cobre em vez de deduzi-lo. Ele imprime só os tetos nomeados, então confirme o compartilhado a partir da própria opção.