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

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.

sondando uma instância saudável
$ 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.

uma parada limpa
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.

  1. Pare o daemon. Uma restauração sob um processo em execução é um segundo escritor no mesmo arquivo.
  2. Copie o backup por cima do arquivo em --data.
  3. Apague os arquivos -wal e -shm ao lado dele. Eles pertencem ao banco que você substituiu, e deixados ali seriam reaplicados naquele que você restaurou.
  4. Inicie o daemon e confirme que ele está lendo o arquivo restaurado: /readyz responde 200, e uma listagem de traces do projeto devolve as execuções que o backup guardava.
restauração, rodada contra um armazenamento de demonstração
$ 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óprio retention_days onde 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:

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

como termina uma rodada contra uma implantação nova
$ 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.