Gestione
mcpd è un processo solo che tiene un file solo. Questo rende breve quasi tutto quello che serve per gestirlo, e rende utile conoscere con esattezza le poche cose che contano.
Che cosa c’è sulla porta
Un solo indirizzo serve tutto: l’ingest OTLP su /v1/traces,
/v1/logs e /v1/metrics, l’ingest nativo su
/v1/ingest, l’API di query sotto /v1/api,
l’interfaccia per chi gestisce su /, e i tre endpoint operativi
qui sotto. Non c’è nessun sidecar e nessun reverse proxy da configurare, ed è
la ragione principale per cui il deploy sta su una riga.
L’interfaccia è compilata dentro il binario, quindi non c’è nessun server di file statici e nessun secondo volume da montare. L’ingest porta il limite largo sul corpo e nessun CORS. L’API di query porta il CORS, così un’interfaccia servita da un’altra origine può leggerla.
/healthz e /readyz non sono la stessa domanda
Sono separati perché rispondono a conseguenze diverse, e collegarli entrambi allo stesso handler è un modo diffuso di costruirsi un ciclo di riavvii.
| Endpoint | Tocca lo storage | Risponde |
|---|---|---|
GET /healthz | No | Sempre 200 finché il processo sta servendo |
GET /readyz | Sì, una lettura di ciascuno storage | 200 quando rispondono lo storage dei segnali e quello delle esecuzioni, 503 quando uno dei due non risponde |
/healthz è la liveness. Di proposito non tocca mai lo storage:
un database lento o bloccato non deve far uccidere e riavviare il processo,
perché riavviarlo non ripara un disco e il ciclo di riavvii ti costa anche i
batch in volo.
/readyz è la readiness. Esegue una sola query banale
su una connessione di lettura, così un’istanza che non riesce a servire
smette di ricevere traffico. Quando lo storage è irraggiungibile risponde
503 con uno status pari a degraded e
un detail che nomina il guasto, invece di un codice di stato
nudo su cui poi devi indagare alla cieca.
Entrambi i corpi portano gli stessi campi: status,
version, spec_version,
uptime_seconds, storage, e detail
quando qualcosa non va. spec_version è la versione del modello
dei dati che questa build realizza, ed è così che un client può rifiutarsi
di parlare con un server che non capisce.
$ 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.
Nessuna delle due sonde richiede una chiave, e non la richiede nemmeno
/metrics. Se nella tua rete la cosa non è accettabile,
limitali sul tuo ingress.
kubernetes
livenessProbe:
httpGet: { path: /healthz, port: 4317 }
periodSeconds: 10
readinessProbe:
httpGet: { path: /readyz, port: 4317 }
periodSeconds: 5
# SIGTERM viene gestito: dai il tempo di finire alle richieste in volo.
terminationGracePeriodSeconds: 30 Metriche
mcpd esporta i propri contatori su /metrics nel formato di
esposizione testuale di Prometheus. Non stanno dietro nessuna opzione di
attivazione: uno strumento di osservabilità che non si può osservare è un
rischio.
| Metrica | Tipo | Significato |
|---|---|---|
mcpd_signals_accepted_total | counter | Segnali scritti nello storage |
mcpd_signals_rejected_total | counter | Segnali rifiutati, con un motivo restituito al mittente |
mcpd_batches_duplicate_total | counter | Batch riconosciuti come una ripetizione e saltati |
mcpd_traces_sampled_out_total | counter | Tracce che la politica di campionamento ha scartato |
mcpd_signals_over_quota_total | counter | Segnali rifiutati per aver superato il tetto al minuto di un progetto |
mcpd_signals_unbounded_total | counter | Segnali arrivati al passaggio del tetto per un progetto che nessun tetto copre |
mcpd_ingest_requests_total | counter | Richieste di ingest gestite |
mcpd_query_requests_total | counter | Richieste di query gestite |
mcpd_request_errors_total | counter | Richieste fallite con un errore del server |
mcpd_audit_write_failures_total | counter | Righe di audit che non si è riusciti a scrivere; la richiesta che descrivono è comunque riuscita |
mcpd_uptime_seconds | gauge | Secondi dall’avvio di questo processo |
Ogni contatore dura quanto il processo e torna a zero al riavvio, quindi
imposta gli avvisi su un tasso invece che su un livello. I due che vale la
pena guardare sono mcpd_signals_rejected_total, che significa
che ai mittenti si sta dicendo di no e che probabilmente non stanno leggendo
la risposta, e mcpd_request_errors_total, che per definizione è
colpa di mcpd: un 4xx è di chi chiama e lì non viene mai contato.
Un mcpd_traces_sampled_out_total che sale non è un guasto. È il
numero che spiega un buco nell’interfaccia quando qualcuno chiede perché
manca la sua esecuzione.
mcpd_signals_over_quota_total è un numero della stessa forma per
l’altro motivo per cui un segnale non arriva: un progetto ha toccato il suo
tetto al minuto. Quei rifiuti sono contati anche in
mcpd_signals_rejected_total, quindi guarda questo per
distinguere una flotta che sta venendo limitata da una flotta che invia dati
malformati. Vedi
Configurazione per
quello che il tetto copre.
mcpd_signals_unbounded_total è il numero opposto: segnali
conservati perché nessun tetto copriva il progetto che nominavano. I tetti
scritti solo sotto projects limitano i progetti che nominano,
quindi un mittente che ne nomina un altro ci passa accanto. Quei segnali
vengono contati invece che rifiutati, perché rifiutare un progetto che
nessuno ha messo per iscritto scarterebbe i dati di un deploy aggiornato
senza cambiare la sua configurazione. Diverso da zero significa che devi
impostare un --quota-per-minute di primo livello accanto a
quelli per progetto.
Il tetto copre ogni porta da cui può entrare una riga: i quattro endpoint di
ingest, gli endpoint a record singolo di punteggi e feedback, e
POST /v1/api/score, i cui punteggi vengono calcolati e poi
addebitati allo stesso contatore prima di essere archiviati. Quello che lì
non ci sta sotto il tetto viene riportato punteggio per punteggio nella lista
skipped della risposta, invece di essere scritto oltre.
mcpd_audit_write_failures_total conta le righe di audit che non
si è riusciti a scrivere. La richiesta che ciascuna riga descriveva è
comunque riuscita e ha comunque restituito i suoi dati, ed è questo il punto:
una scrittura di audit non fa mai fallire la lettura che descrive. Diverso da
zero significa quindi che sono avvenute letture di payload archiviati di cui
il registro di audit non sa rendere conto, quindi trattalo come un segnale di
integrità e non di disponibilità.
prometheus
scrape_configs:
- job_name: mcpd
static_configs:
- targets: ['mcpd:4317'] Arresto
mcpd gestisce SIGTERM e SIGINT. Con l’uno o con
l’altro smette di accettare nuove connessioni e lascia finire le richieste in
volo prima che il processo esca.
Gestire SIGTERM in particolare è quello che fa funzionare tutto
questo sotto un orchestratore. Kubernetes invia SIGTERM e
aspetta la fine del periodo di grazia prima di SIGKILL; un
container che ascolta solo SIGINT viene ucciso invece che
fermato, e qualunque batch fosse a metà transazione va perso.
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
Quello che mcpd non può salvare mentre esce è tutto ciò che sta ancora nella
coda di un mittente. Gli SDK tengono gli span in memoria e li esportano su un
worker, quindi un processo che esce senza chiamare shutdown()
perde quello che non era ancora partito. È l’arresto dell’agente da fare
bene, non quello del daemon.
Backup
Tutto lo storage è un solo file SQLite in --data. Gira in
modalità WAL, quindi su disco trovi mcpd.db accanto a
mcpd.db-wal e mcpd.db-shm. Copiare il
.db da solo mentre il processo è in esecuzione ti dà un file a
cui manca tutto quello che sta ancora nel write-ahead log.
Prendi il backup attraverso SQLite, che legge un’istantanea coerente mentre mcpd continua a scrivere:
backup a caldo
sqlite3 /data/mcpd.db ".backup '/backup/mcpd-$(date +%F).db'"
Fermare prima il processo e copiare tutti e tre i file vale allo stesso modo
e non richiede nessun binario sqlite3 nell’immagine. In questa
build non c’è nessun sottocomando mcpd backup, e nessun backup
pianificato: è il compito dei tuoi strumenti di snapshot, e fingere il
contrario metterebbe uno strumento fatto a metà sul percorso critico.
La conservazione cancella righe ma non fa rimpicciolire il file. SQLite tiene
le pagine liberate e le riusa, quindi l’occupazione di disco si appiattisce
invece di scendere dopo una cancellazione grande. Esegui
VACUUM su un’istanza ferma se ti serve davvero indietro lo
spazio.
Ripristino
Rimettere a posto un backup sono quattro passi, e l’ordine conta a causa del write-ahead log.
- Ferma il daemon. Un ripristino sotto un processo in esecuzione è un secondo scrittore sullo stesso file.
- Copia il backup sopra il file in
--data. -
Cancella i file
-wale-shmche stanno accanto. Appartengono al database che hai sostituito, e lasciati lì verrebbero riapplicati a quello che hai ripristinato. -
Avvia il daemon, e conferma che sta leggendo il file ripristinato:
/readyzrisponde 200, e un elenco di tracce sul progetto restituisce le esecuzioni che il backup conteneva.
$ 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 Aggiornamenti
Sostituisci il binario o il tag dell’immagine e riavvialo. Lo schema viene
applicato all’avvio, tutte le volte, ed è scritto in modo che eseguirlo su un
file già aggiornato non cambi niente. La versione applicata è registrata in
una tabella schema_version dentro lo stesso file, e questa build
è alla versione 2.
Due cose da tenere a mente. Vai avanti, non indietro: un binario più vecchio
non promette di leggere un file più nuovo, quindi fai prima il backup. E
controlla spec_version da /healthz dopo
l’aggiornamento se fissi le versioni degli SDK, perché è quello il numero con
cui un client si confronta.
Dove si ferma una sola istanza
Essere onesti su questo è più utile di un numero che non abbiamo misurato. Non pubblichiamo nessuna cifra di throughput, perché non ne esiste nessuna che potremmo sostenere.
- Uno scrittore solo. Lo storage SQLite tiene una sola connessione di scrittura dietro un mutex e quattro connessioni di lettura. La modalità WAL fa sì che la query di una dashboard non blocchi mai l’ingest, che è il guasto che conterebbe, ma tutte le scritture sono messe in fila attraverso quell’unica connessione.
- Un’istanza sola per file. Due processi mcpd puntati sullo stesso file non sono un deploy supportato. Eseguilo come una replica sola con un volume, non come un servizio scalato in orizzontale.
- Entrambi i limiti qui sopra sono dello storage incorporato. Smettono di valere su ClickHouse, che è compilato dentro ed è trattato più sotto.
- I payload sono grandi. I prompt e le completion sono kilobyte, non byte, e sono quello che rende mcpd degno di essere eseguito. Lo storage cresce con il volume di testo molto più in fretta che con il numero di span, quindi misura con i tuoi payload invece che con una cifra di span al secondo.
- La conservazione copre i progetti che hai messo per
iscritto. Il passaggio orario cancella da ogni progetto del blocco
projects, ciascuno sul proprioretention_daysdove ne imposta uno e sulla cifra globale dove non lo fa, più il progetto nominato da--project. Un progetto inventato da un mittente che non compare in nessuno dei due posti non ha nessuna finestra e non viene mai tagliato, cosa che vale la pena sapere prima di appoggiarsi alla conservazione come misura di riservatezza.
Quando il volume supera l’istanza, le leve in ordine di quanto ti costano
sono: --retention-days, che limita il file;
--sample, che limita quello che entra ed è deterministico, così
una traccia conservata è conservata intera; e il passaggio a ClickHouse, che
toglie insieme lo scrittore unico e la replica unica. Vedi
Campionamento e
Oscuramento e conservazione.
Postgres, quando ne gestisci già uno
Punta mcpd su una stringa di connessione Postgres e archivia lì invece che nel file incorporato. Lo schema viene creato all’avvio.
archivia su Postgres
mcpd serve --postgres-url postgres://mcpd:$PGPASSWORD@db:5432/mcpd Questo toglie gli stessi due limiti che toglie ClickHouse. Le scritture non passano più da una connessione sola, quindi mcpd gira come più repliche dietro un solo servizio, e lo fa su un database che quasi tutte le squadre già gestiscono, sottopongono a backup e sorvegliano.
La migrazione prende un lock consultivo di Postgres e viene saltata del tutto quando lo schema è già aggiornato. Più repliche che partono insieme non sono quindi un problema, e un deploy progressivo non prende un lock esclusivo su tabelle su cui altre repliche stanno scrivendo.
La ricerca full-text usa una colonna tsvector generata con un
indice GIN, nella configurazione simple: mette in minuscolo e
divide sui confini di parola ma non riduce alla radice, così cercare in un
prompt il token letterale che hai digitato fa quello che ti aspetti.
Scegli ClickHouse invece di Postgres quando la conservazione si misura in miliardi di span. Sotto quella soglia, il database che già gestisci è la risposta migliore.
ClickHouse, quando il file non è più la forma giusta
Punta mcpd su un endpoint HTTP di ClickHouse e archivia lì invece che nel file incorporato. Lo schema viene creato all’avvio, quindi non c’è niente da applicare a mano.
archivia su ClickHouse
mcpd serve \
--clickhouse-url http://clickhouse:8123 \
--clickhouse-database mcpd \
--clickhouse-user default --clickhouse-password $CH_PASSWORD
Tutti e due i vincoli qui sopra cadono insieme. Le scritture non passano più
da una connessione sola, quindi mcpd gira come più repliche dietro un solo
servizio, e gli span finiscono in un MergeTree costruito per la
forma che questi dati hanno davvero: molta più scrittura che lettura, e
query che scorrono un intervallo di tempo.
Scegli ClickHouse quando la conservazione si misura in mesi di traffico di produzione invece che nel disco di un portatile. Su qualsiasi cosa più piccola il file incorporato è più veloce da eseguire, più veloce da sottoporre a backup, e una cosa in meno da gestire. Tieni il file finché non fa male.
Le tracce sono un ReplacingMergeTree, perché la proiezione di
una traccia viene ricalcolata mano a mano che arrivano i suoi span e la
scrittura più recente è quella corretta. Span che arrivano fuori ordine,
ritentati o aggiornati sul posto convergono quindi sulla risposta giusta
invece di accumulare duplicati.
Una garanzia qui è più debole che sugli altri due storage, ed è quella che
potresti dover certificare. Su ClickHouse la cancellazione è una mutazione
asincrona, quindi una richiesta di cancellazione viene accettata e applicata
dopo. SQLite e Postgres cancellano dentro la transazione che ha risposto. La
risposta dice quale delle due cose è successa, in
deletes_pending. Vedi
Oscuramento e conservazione.
Controllare un deploy
mcpd doctor legge la configurazione e lo storage e riferisce che
cosa non va, nominando l’opzione che sistema ogni cosa. Esiste perché i
guasti che fanno male davvero sono quelli silenziosi: non va in crash niente, e
te ne accorgi settimane dopo.
controlla un deploy senza contattare niente oltre allo storage
mcpd doctor --data /data/mcpd.db --offline
Tre opzioni sono del comando stesso. --offline non contatta
niente oltre allo storage, quindi l’endpoint di inoltro viene riportato come
saltato invece che controllato. --read-only non scrive niente,
quindi il controllo dello storage dimostra che lo storage si può leggere e
non che ci si può scrivere, e lo dice. --window-days è quanti
giorni di storia leggono i controlli su attività, orologio e listino, sette
se non dici altrimenti, fra 1 e 36525. Tutto il resto che prende è la
configurazione del daemon stesso, quindi dà un voto all’invocazione che
avvieresti.
Vengono valutati dodici gruppi, in questo ordine, e il resoconto finisce con un conteggio per ogni livello:
| Gruppo | Che cosa valuta |
|---|---|
storage | Lo storage risponde a un ping, e uno span di prova sopravvive a una scrittura, una lettura e una cancellazione |
runs | Lo storage delle esecuzioni risponde, quante versioni di definizione e quante esecuzioni in corso tiene, e se la superficie delle esecuzioni ha bisogno di una chiave su un indirizzo che altre persone possono raggiungere. Solo lettura: il doctor non prende mai in carico un passo né fa scattare una scadenza |
keys | Ogni ambito, con la sua eredità resa esplicita, così il caso in cui non è impostata nessuna chiave admin e ogni chiave di lettura quindi cancella anche sta sulla pagina invece che nella documentazione |
identity | L’header del soggetto di audit, e se l’intervallo autorizzato a impostarlo è abbastanza stretto da essere credibile |
projects | I progetti configurati contro i progetti che contengono esecuzioni. Uno che non ha mai ricevuto uno span è quasi sempre un errore di battitura in un nome |
retention | Fin dove va indietro il dato di ogni progetto, e se qualcosa lo cancellerà mai |
limits | I tetti di ingest per progetto e le frazioni di campionamento |
forwarding | L’endpoint di inoltro, inviandogli in POST un batch OTLP senza nessuno span dentro |
judge | Il blocco del giudice, solo come configurazione |
monitors | Monitor e obiettivi, e i segnali che leggono |
clock | Le esecuzioni ai due estremi della storia di ogni progetto contro l’orologio di questa macchina, perché un mittente il cui orologio è indietro si ordina sotto ogni esecuzione onesta e non comparirebbe mai in una pagina delle più recenti |
pricing | Gli span di modello recenti contro il listino, così un modello che niente riesce a valorizzare viene nominato |
identity e limits sono i due aggiunti più di
recente, e nessuno dei due veniva valutato, per lo stesso motivo: per colpa loro
non va in crash niente. Un registro di audit senza soggetto registra che cosa è
stato fatto e mai chi l’ha fatto. Una frazione di campionamento fuori
dall’intervallo da 0.0 a 1.0 viene rifiutata per --sample e non
veniva mai controllata in un blocco di progetto, così un progetto poteva
portarne una e il daemon la eseguiva. Un tetto impostato per progetto senza
un tetto di primo livello accanto lascia ogni progetto che nessuno ha messo
per iscritto senza nessun tetto, e il nome del progetto lo sceglie il
mittente.
Saltato non è ok
Un controllo che non è stato eseguito dice skipped, mai ok. Quella distinzione è il senso del comando: un sottosistema che manca dalla configurazione su cui l’hai puntato non è un sottosistema in buona salute, e riportare i due allo stesso modo renderebbe il doctor più sicuro di sé proprio quando gli è stato dato il file sbagliato. La riga di riepilogo conta a parte i controlli saltati e lo dice a parole, così una passata che ha verificato due dei suoi quattordici risultati non può leggersi come un certificato di buona salute.
$ 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. Uno storage occupato non è uno storage rotto
Il controllo di prova sullo storage è l’unico che ha bisogno del lock di scrittura. Tutto quello che sta sotto legge, e chi legge non fa la fila dietro chi scrive. Una scrittura di prova che perde la corsa per quel lock viene riportata come skipped: viene tentata tre volte, e se tutte e tre aspettano il lock e rinunciano, il resoconto dice che la scrittura non è stata controllata e perché.
Questo conta perché lo storage incorporato accetta uno scrittore solo, e un
deploy sotto ingest reale tiene quel lock quasi di continuo. Riportare la
contesa come un guasto faceva fallire un deploy proprio quando il deploy era
sano e occupato. I guasti che la prova esiste per prendere si nominano
ciascuno da sé e nessuno di essi dice «bloccato»: un mount di sola lettura, un
disco pieno e un ruolo senza INSERT restano guasti, e lo è anche
un lock corrotto, che dice «locking protocol» ed è un danno vero.
Il codice di uscita
Esce diverso da zero su un guasto, così può bloccare un deploy. Un avvertimento non è un guasto: esce con zero e si aspetta che una persona lo legga. Una sola configurazione è un guasto invece che un avvertimento, e vale la pena saperlo prima di collegare tutto questo a una pipeline: un ambito di lettura aperto su un indirizzo non di loopback, che consegna ogni prompt archiviato a chiunque riesca a raggiungere la porta. Un ambito di ingest aperto sullo stesso indirizzo resta un avvertimento, perché le righe spazzatura sono quello che la conservazione toglie e una divulgazione non si può riprendere. Un intervallo di identità fidata che contiene tutta internet viene valutato allo stesso modo, per lo stesso motivo: qualsiasi indirizzo potrebbe allora impostare l’header ed essere registrato come chiunque esso nomini.
Le impostazioni da controllare prima che sia raggiungibile
Quando si lega a un indirizzo non di loopback, mcpd registra all’avvio un avvertimento per ogni superficie aperta a chi non dovrebbe raggiungerla, e ogni avvertimento nomina le opzioni da impostare. Avverte invece di rifiutarsi di partire, perché rifiutare romperebbe il percorso che va da niente a una prima traccia. Resta comunque un avvertimento su un buco vero: un’API di lettura aperta consegna ogni prompt archiviato a chiunque riesca a raggiungere la 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 Senza le ultime tre chiavi, la chiave di ingest e la chiave di lettura raggiungono anche la superficie delle esecuzioni, e la chiave di lettura cancella anche. Il dettaglio completo su ogni chiave, e su tutto il resto che puoi impostare, sta in Configurazione.