Como executar
O mcpd é um processo segurando um arquivo. Isso deixa curta a maior parte de operá-lo, e faz das poucas coisas que importam algo que vale conhecer com exatidão.
O que está na porta
Um endereço serve tudo: a ingestão OTLP em /v1/traces,
/v1/logs e /v1/metrics, a ingestão nativa em
/v1/ingest, a API de consulta sob /v1/api, a
interface de operação em / e os três endpoints operacionais
abaixo. Não há sidecar nem proxy reverso a configurar, que é a maior razão
de a implantação caber em uma linha.
A interface é compilada dentro do binário, então não há servidor de arquivos estáticos nem um segundo volume a montar. A ingestão carrega o limite de corpo grande e nenhum CORS. A API de consulta carrega CORS, para que uma interface servida de outra origem consiga lê-la.
/healthz e /readyz não são a mesma pergunta
Eles são separados porque respondem a consequências diferentes, e ligar os dois ao mesmo handler é uma forma comum de construir um laço de reinício.
| Endpoint | Toca o armazenamento | Responde |
|---|---|---|
GET /healthz | Não | Sempre 200 enquanto o processo está servindo |
GET /readyz | Sim, uma leitura de cada armazenamento | 200 quando o armazenamento de sinais e o de execuções respondem, 503 quando um não responde |
/healthz é liveness. Ele nunca toca o armazenamento, de
propósito: um banco lento ou travado não pode fazer o processo ser morto e
reiniciado, porque reiniciá-lo não conserta um disco e o laço de reinício
ainda custa a você os lotes em voo.
/readyz é readiness. Ele roda uma consulta trivial em uma
conexão de leitura, então uma instância que não consegue servir deixa de
receber tráfego. Quando o armazenamento está inalcançável, ele responde
503 com status igual a degraded e um
detail nomeando a falha, em vez de um código de status seco que
você depois teria de investigar às cegas.
Os dois corpos carregam os mesmos campos: status,
version, spec_version,
uptime_seconds, storage e detail
quando algo está errado. spec_version é a versão do modelo de
dados que esta compilação implementa, que é como um cliente pode se recusar
a falar com um servidor que ele não entende.
$ curl -s localhost:4317/healthz{"status":"ok","version":"0.1.0","spec_version":"1.0.0", "uptime_seconds":86400,"storage":"sqlite"}$ curl -s -o /dev/null -w "%{http_code}\n" localhost:4317/readyz200$ curl -s localhost:4317/metrics | head -4# HELP mcpd_signals_accepted_total Signals written to storage.# TYPE mcpd_signals_accepted_total countermcpd_signals_accepted_total 184203# HELP mcpd_signals_rejected_total Signals refused, with a reason returned to the sender.
Nenhuma das duas sondas exige chave, e /metrics também não. Se
isso não for aceitável na sua rede, restrinja o acesso a elas no seu ingress.
kubernetes
livenessProbe:
httpGet: { path: /healthz, port: 4317 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: 4317 }
periodSeconds: 5
# SIGTERM is handled, so give in-flight ingest time to finish.
terminationGracePeriodSeconds: 30 Métricas
O mcpd exporta os próprios contadores em /metrics, no formato
de texto do Prometheus. Eles não estão atrás de uma chave de funcionalidade:
uma ferramenta de observabilidade que não pode ser observada é um passivo.
| Métrica | Tipo | Significado |
|---|---|---|
mcpd_signals_accepted_total | counter | Sinais escritos no armazenamento |
mcpd_signals_rejected_total | counter | Sinais recusados, com um motivo devolvido ao remetente |
mcpd_batches_duplicate_total | counter | Lotes reconhecidos como reenvio e pulados |
mcpd_traces_sampled_out_total | counter | Traces que a política de amostragem descartou |
mcpd_signals_over_quota_total | counter | Sinais recusados por passarem do teto por minuto de um projeto |
mcpd_signals_unbounded_total | counter | Sinais que chegaram à etapa do teto para um projeto que teto nenhum cobre |
mcpd_ingest_requests_total | counter | Requisições de ingestão atendidas |
mcpd_query_requests_total | counter | Requisições de consulta atendidas |
mcpd_request_errors_total | counter | Requisições que falharam com um erro de servidor |
mcpd_audit_write_failures_total | counter | Linhas de auditoria que não puderam ser escritas; a requisição que elas descrevem mesmo assim teve êxito |
mcpd_uptime_seconds | gauge | Segundos desde que este processo começou |
Todo contador vale pelo tempo de vida do processo e volta a zero no
reinício, então alerte sobre uma taxa e não sobre um nível. Os dois que
valem acompanhar são mcpd_signals_rejected_total, que quer
dizer que remetentes estão ouvindo não e provavelmente não estão lendo a
resposta, e mcpd_request_errors_total, que é culpa do próprio
mcpd por definição: um 4xx é de quem chamou e nunca é contado ali.
Um mcpd_traces_sampled_out_total subindo não é uma falha. É o
número que explica uma lacuna na interface quando alguém pergunta por que a
execução dele sumiu.
mcpd_signals_over_quota_total é um número da mesma forma para o
outro motivo de um sinal não cair: um projeto bateu no teto por minuto dele.
Essas recusas também são contadas em
mcpd_signals_rejected_total, então acompanhe esta para
distinguir uma frota sendo limitada de uma frota enviando dados malformados.
Veja Configuração para
o que o teto cobre.
mcpd_signals_unbounded_total é o número oposto: sinais que
foram guardados porque teto nenhum cobria o projeto que eles nomearam. Tetos
escritos só sob projects limitam os projetos que nomeiam, então
um remetente que nomeia outra coisa passa por eles. Esses sinais são
contados em vez de recusados, porque recusar um projeto que ninguém anotou
descartaria os dados de uma implantação que atualizou sem mudar a
configuração. Diferente de zero quer dizer: defina um
--quota-per-minute de nível superior ao lado dos tetos por
projeto.
O teto cobre toda porta pela qual uma linha pode entrar: os quatro endpoints
de ingestão, os endpoints de registro único de nota e de feedback, e
POST /v1/api/score, cujos scores são calculados e depois
cobrados do mesmo contador antes de serem armazenados. O que ali não couber
sob o teto é relatado score a score na lista skipped da
resposta, em vez de escrito por cima dele.
mcpd_audit_write_failures_total conta linhas de auditoria que
não puderam ser escritas. A requisição que cada linha descrevia mesmo assim
teve êxito e devolveu os dados dela, que é justamente o ponto: uma escrita
de auditoria nunca faz falhar a leitura que descreve. Diferente de zero quer
dizer, então, que houve leituras de payloads armazenados de que o log de
auditoria não consegue dar conta, então trate isso como um sinal de
integridade e não de disponibilidade.
prometheus
scrape_configs:
- job_name: mcpd
static_configs:
- targets: ['mcpd:4317'] Desligamento
O mcpd trata SIGTERM e SIGINT. Em qualquer um dos
dois, ele para de aceitar conexões novas e deixa as requisições em voo
terminarem antes de o processo sair.
Tratar especificamente o SIGTERM é o que faz isso funcionar sob
um orquestrador. O Kubernetes envia SIGTERM e espera o período
de graça antes do SIGKILL; um contêiner que só escuta
SIGINT é morto em vez de parado, e o lote que estivesse no meio
de uma transação se perde.
mcpd 0.1.0 listening on http://0.0.0.0:4317 storage sqlite at /data/mcpd.dbretention removed 12480 rows older than 30 daysreceived termination signalshutting down
O que o mcpd não consegue salvar na saída é qualquer coisa ainda parada na
fila de um remetente. Os SDKs guardam spans em memória e os exportam em um
worker, então um processo que sai sem chamar shutdown() perde o
que ainda não tinha ido. Esse é o desligamento do agente a acertar, não o do
daemon.
Backups
Todo o armazenamento é um arquivo SQLite em --data. Ele roda em
modo WAL, então no disco você vai encontrar mcpd.db ao lado de
mcpd.db-wal e mcpd.db-shm. Copiar o
.db sozinho enquanto o processo roda dá a você um arquivo sem
tudo o que ainda está no write-ahead log.
Faça o backup pelo SQLite, que lê um retrato consistente enquanto o mcpd continua escrevendo:
backup a quente
sqlite3 /data/mcpd.db ".backup '/backup/mcpd-$(date +%F).db'"
Parar o processo antes e copiar os três arquivos vale igualmente e não
precisa de um binário sqlite3 na imagem. Não há subcomando
mcpd backup nesta versão, nem backup agendado: esse é o trabalho
da sua ferramenta de retratos, e fingir o contrário poria uma ferramenta
meia-boca no caminho crítico.
A retenção apaga linhas mas não encolhe o arquivo. O SQLite guarda as
páginas liberadas e as reaproveita, então o uso de disco estabiliza em vez
de cair depois de um apagamento grande. Rode VACUUM contra uma
instância parada se você realmente precisar do espaço de volta.
Restauração
Repor um backup são quatro passos, e a ordem importa por causa do write-ahead log.
- Pare o daemon. Uma restauração sob um processo em execução é um segundo escritor no mesmo arquivo.
- Copie o backup por cima do arquivo em
--data. -
Apague os arquivos
-wale-shmao lado dele. Eles pertencem ao banco que você substituiu, e deixados ali seriam reaplicados naquele que você restaurou. -
Inicie o daemon e confirme que ele está lendo o arquivo restaurado:
/readyzresponde 200, e uma listagem de traces do projeto devolve as execuções que o backup guardava.
$ kill $(cat mcpd.pid) && cp /backup/mcpd-2026-09-01.db /data/mcpd.db$ rm -f /data/mcpd.db-wal /data/mcpd.db-shm$ mcpd --data /data/mcpd.db &$ curl -s -o /dev/null -w "%{http_code}\n" localhost:4317/readyz200$ curl -s "localhost:4317/v1/api/traces?project=default&limit=500" | jq ".traces | length"2 Atualizações
Troque o binário ou a tag da imagem e inicie de novo. O esquema é aplicado
na partida, toda vez, e é escrito de modo que rodá-lo contra um arquivo já
atualizado não muda nada. A versão aplicada fica registrada em uma tabela
schema_version dentro do mesmo arquivo, e esta compilação está
na versão 2.
Duas coisas a ter em mente. Avance, não volte: um binário mais antigo não
promete ler um arquivo mais novo, então faça o backup antes. E confira o
spec_version em /healthz depois da atualização se
você fixa versões de SDK, porque é esse o número que um cliente compara.
Onde uma instância só para
Ser honesto sobre isto é mais útil que um número que não medimos. Não publicamos números de vazão, porque não existe nenhum que pudéssemos sustentar.
- Um escritor. O armazenamento SQLite mantém uma única conexão de escrita atrás de um mutex e quatro conexões de leitura. O modo WAL faz com que uma consulta de painel nunca bloqueie a ingestão, que seria a falha que importaria, mas todas as escritas são serializadas por aquela conexão.
- Uma instância por arquivo. Dois processos do mcpd apontados para o mesmo arquivo não é uma implantação suportada. Rode como uma réplica única com um volume, e não como um serviço escalado na horizontal.
- Os dois limites acima são do armazenamento embutido. Eles deixam de valer no ClickHouse, que é compilado junto e tratado abaixo.
- Os payloads são grandes. Prompts e respostas têm quilobytes, não bytes, e são o que faz o mcpd valer a pena. O armazenamento cresce com o volume de texto muito mais rápido que com a contagem de spans, então meça com os seus próprios payloads em vez de com um número de spans por segundo.
- A retenção cobre os projetos que você anotou. A passagem
de hora em hora apaga de todo projeto do bloco
projects, cada um pelo próprioretention_daysonde ele define um e pelo número global onde não define, mais o projeto nomeado por--project. Um projeto que um remetente inventou e que não aparece em nenhum dos dois lugares não tem janela e nunca é aparado, o que vale saber antes de você depender da retenção como controle de privacidade.
Quando o volume passa do tamanho da instância, as alavancas, na ordem do que
custam a você, são: --retention-days, que limita o arquivo;
--sample, que limita a entrada e é determinística, então um
trace guardado é guardado inteiro; e mudar para o ClickHouse, que remove o
escritor único e a réplica única de uma vez. Veja
Amostragem e
Ocultação e retenção.
Postgres, quando você já roda um
Aponte o mcpd para uma string de conexão do Postgres e ele armazena lá em vez de no arquivo embutido. O esquema é criado na partida.
armazenar no Postgres
mcpd serve --postgres-url postgres://mcpd:$PGPASSWORD@db:5432/mcpd Isso remove os mesmos dois limites que o ClickHouse remove. As escritas deixam de passar por uma conexão única, então o mcpd roda como várias réplicas atrás de um serviço, e faz isso em um banco que a maioria dos times já opera, já copia e já monitora.
A migração toma um advisory lock do Postgres e é pulada por inteiro quando o esquema já está atual. Várias réplicas partindo ao mesmo tempo é, portanto, seguro, e uma implantação gradual não toma um lock exclusivo sobre tabelas em que outras réplicas estão escrevendo.
A busca em texto completo usa uma coluna tsvector gerada com um
índice GIN, na configuração simple: ela passa para minúsculas e
quebra nas fronteiras de palavra, mas não reduz ao radical, então buscar em
um prompt o token literal que você digitou faz o que você espera.
Escolha o ClickHouse em vez do Postgres quando a retenção for medida em bilhões de spans. Abaixo disso, o banco que você já roda é a resposta melhor.
ClickHouse, quando o arquivo deixa de ter a forma certa
Aponte o mcpd para um endpoint HTTP do ClickHouse e ele armazena lá em vez de no arquivo embutido. O esquema é criado na partida, então não há nada a aplicar à mão.
armazenar no ClickHouse
mcpd serve \
--clickhouse-url http://clickhouse:8123 \
--clickhouse-database mcpd \
--clickhouse-user default --clickhouse-password $CH_PASSWORD
As duas restrições acima caem de uma vez. As escritas deixam de passar por
uma conexão única, então o mcpd roda como várias réplicas atrás de um
serviço, e os spans caem em um MergeTree feito para a forma que
estes dados realmente têm: muito mais escrita que leitura, e consultas que
varrem um intervalo de tempo.
Escolha-o quando a retenção for medida em meses de tráfego de produção, e não no disco de um notebook. Em qualquer coisa menor, o arquivo embutido é mais rápido de rodar, mais rápido de copiar e uma coisa a menos para operar. Fique com o arquivo até ele doer.
Traces são um ReplacingMergeTree, porque a projeção de um trace
é recalculada conforme os spans dele chegam e a última escrita é a correta.
Spans que chegam fora de ordem, repetidos ou atualizados no lugar convergem,
portanto, para a resposta certa em vez de acumular duplicatas.
Uma garantia é mais fraca aqui que nos outros dois backends, e é justamente
a que você pode ter de certificar. Apagar no ClickHouse é uma mutação
assíncrona, então um pedido de eliminação é aceito e aplicado depois. SQLite
e Postgres apagam dentro da transação que respondeu. A resposta diz qual dos
dois aconteceu, em deletes_pending. Veja
Ocultação e retenção.
Conferir uma implantação
mcpd doctor lê a configuração e o armazenamento e relata o que
está errado, nomeando a opção que conserta cada coisa. Ele existe porque as
falhas que de fato mordem são as caladas: nada quebra, e você descobre
semanas depois.
confira uma implantação sem contatar nada além do armazenamento
mcpd doctor --data /data/mcpd.db --offline
Três opções são do próprio comando. --offline não contata nada
além do armazenamento, então o endpoint de encaminhamento é relatado como
pulado em vez de conferido. --read-only não escreve nada, então
a verificação de armazenamento prova que o armazenamento pode ser lido e não
que pode ser escrito, e diz isso. --window-days é quantos dias
de histórico as verificações de atividade, de relógio e de tabela de preços
leem, sete a menos que você diga outra coisa, entre 1 e 36525. Todo o resto
que ele recebe é a configuração do próprio daemon, então ele confere a
invocação que você iniciaria.
Doze grupos são conferidos, nesta ordem, e o relatório termina com uma contagem de cada nível:
| Grupo | O que ele confere |
|---|---|
storage | O armazenamento responde a um ping, e um span de sonda sobrevive a uma escrita, uma leitura e um apagamento |
runs | O armazenamento de execuções responde, quantas versões de definição e quantas execuções em andamento ele guarda, e se a superfície de execução precisa de chave em um endereço que outras pessoas alcançam. Só leitura: o doctor nunca reivindica uma etapa nem dispara um prazo |
keys | Todo escopo, com a herança dele explicitada, para que o caso em que nenhuma chave de admin está definida e toda chave de leitura também apaga fique na tela, e não na documentação |
identity | O cabeçalho de sujeito da auditoria, e se a faixa autorizada a defini-lo é estreita o bastante para se acreditar nela |
projects | Projetos configurados contra projetos que guardam execuções. Um que nunca recebeu um span é quase sempre um erro de digitação em um nome |
retention | Até quando vão para trás os dados de cada projeto, e se alguma coisa vai apagá-los algum dia |
limits | Os tetos de ingestão por projeto e as frações de amostragem |
forwarding | O endpoint de encaminhamento, enviando por POST um lote OTLP sem span nenhum dentro |
judge | O bloco do juiz, apenas como configuração |
monitors | Monitores e objetivos, e os sinais que eles leem |
clock | Execuções das duas pontas do histórico de cada projeto contra o relógio desta máquina, porque um remetente cujo relógio atrasa fica ordenado abaixo de toda execução honesta e nunca apareceria em uma página das mais recentes |
pricing | Spans de modelo recentes contra a tabela de preços, para que um modelo que nada consegue precificar seja nomeado |
identity e limits são os dois mais recentes, e os
dois ficavam sem conferência pela mesma razão: nada neles quebra. Um log de
auditoria sem sujeito anota o que foi feito e nunca quem fez. Uma fração de
amostragem fora de 0.0 a 1.0 é recusada para --sample e nunca
era conferida em um bloco de projeto, então um projeto podia carregar uma e
o daemon a executaria. Um teto definido por projeto sem um teto de nível
superior ao lado deixa todo projeto que ninguém anotou sem teto algum, e é o
remetente quem escolhe o nome do projeto.
Pulado não é ok
Uma verificação que não foi rodada diz pulada, nunca ok. Essa distinção é o objetivo do comando: um subsistema ausente da configuração para a qual você o apontou não é um subsistema com boa saúde, e relatar os dois do mesmo jeito deixaria o doctor mais confiante exatamente quando lhe tivessem entregado o arquivo errado. A linha de resumo conta as verificações puladas em separado e diz isso por extenso, para que uma rodada que confirmou duas das quatorze constatações não possa se ler como um atestado de saúde.
$ mcpd doctor --data /data/mcpd.db | tail -42 ok, 9 skipped, 2 warnings, 1 faultA skipped check proved nothing: it was not run, or there was nothing thereto run it against. Only the 2 ok were verified.A fault is something broken or open. Exiting 2. Um armazenamento ocupado não é um armazenamento quebrado
A sonda de armazenamento é a única verificação que precisa do lock de escrita. Tudo abaixo dela lê, e leitores não entram na fila atrás de um escritor. Uma escrita de sonda que perde a corrida por aquele lock é relatada como pulada: ela é tentada três vezes, e se as três esperarem pelo lock e desistirem, o relatório diz que a escrita não foi conferida e por quê.
Isso importa porque o armazenamento embutido aceita um escritor, e uma
implantação sob ingestão real segura aquele lock quase o tempo todo. Relatar
a disputa como falha fazia o comando reprovar uma implantação exatamente
quando ela estava saudável e ocupada. As falhas que a sonda existe para
pegar se nomeiam sozinhas e nenhuma delas diz "locked": uma montagem só de
leitura, um disco cheio e uma role sem INSERT continuam sendo
falhas, assim como um lock corrompido, que diz "locking protocol" e é dano
de verdade.
O código de saída
Ele sai diferente de zero diante de uma falha, então pode servir de portão para uma implantação. Um aviso não é uma falha: ele sai com zero e espera que uma pessoa o leia. Uma configuração é falha e não aviso, e vale conhecer antes de você ligar isto a um pipeline: um escopo de leitura aberto em um endereço que não é loopback, o que entrega todo prompt armazenado a quem alcançar a porta. Um escopo de ingestão aberto no mesmo endereço continua sendo aviso, porque linhas de lixo são o que a retenção remove e a divulgação não pode ser desfeita. Uma faixa de identidade confiável que contém a internet inteira é tratada do mesmo jeito, pela mesma razão: qualquer endereço poderia então definir o cabeçalho e ser registrado como quem ele nomear.
Os ajustes a conferir antes de ele ficar alcançável
Quando escuta em um endereço que não é loopback, o mcpd registra na partida um aviso para cada superfície aberta a quem não deveria alcançá-la, e cada aviso nomeia as opções a definir. Ele avisa em vez de se recusar a iniciar, porque recusar quebraria o caminho do nada até um primeiro trace. Ainda assim é um aviso sobre um buraco real: uma API de leitura aberta entrega todo prompt armazenado a quem alcançar a porta.
mcpd --data /data/mcpd.db --ingest-key $INGEST_KEY --read-key $READ_KEY --execute-key $EXECUTE_KEY --worker-key $WORKER_KEY --admin-key $ADMIN_KEY --retention-days 30 Sem as três últimas chaves, a chave de ingestão e a chave de leitura também alcançam a superfície de execuções, e a chave de leitura também apaga. O detalhe completo de cada chave, e de tudo o mais que você pode definir, está em Configuração.