Configurazione
Ogni impostazione è un’opzione, e ogni opzione ha una variabile d’ambiente con lo stesso significato. Le poche troppo strutturate per una riga di comando stanno in un solo file JSON. Questa pagina le contiene tutte.
Due modi per dire la stessa cosa
Esegui mcpd senza argomenti e funziona: si mette in ascolto su
0.0.0.0:4317, tiene i dati in ./mcpd.db, oscura i
segreti e conserva ogni traccia per sempre. Tutto quello che segue cambia
una di queste decisioni.
Un’opzione vince sempre sulla sua variabile d’ambiente. Sono entrambe globali, quindi vengono accettate prima o dopo un sottocomando.
Riferimento
| Opzione | Variabile | Valore predefinito |
|---|---|---|
--listen | MCPD_LISTEN | 0.0.0.0:4317 |
--data | MCPD_DATA | ./mcpd.db |
--project | MCPD_PROJECT | default |
--retention-days | MCPD_RETENTION_DAYS | non impostata: conserva tutto |
--ingest-key | MCPD_INGEST_KEYS | nessuna: l’ingest è aperto |
--read-key | MCPD_READ_KEYS | nessuna: la lettura è aperta |
--write-key | MCPD_WRITE_KEYS | nessuna: vengono accettate le chiavi di ingest |
--admin-key | MCPD_ADMIN_KEYS | nessuna: vengono accettate le chiavi di lettura |
--execute-key | MCPD_EXECUTE_KEYS | nessuna: vengono accettate le chiavi di scrittura |
--worker-key | MCPD_WORKER_KEYS | nessuna: vengono accettate le chiavi di scrittura |
--runs-data | MCPD_RUNS_DATA | il percorso di --data con .runs prima dell’estensione |
--trusted-identity-header | MCPD_TRUSTED_IDENTITY_HEADER | nessuno: non viene registrato nessun soggetto |
--trusted-proxy-cidr | MCPD_TRUSTED_PROXY_CIDRS | nessuno: nessun indirizzo può impostare quell’header |
--sample | MCPD_SAMPLE | non impostata: conserva ogni traccia |
--quota-per-minute | MCPD_QUOTA_PER_MINUTE | non impostata: nessun tetto |
--no-redact | MCPD_NO_REDACT | spenta: l’oscuramento è attivo |
--redact-key | MCPD_REDACT_KEY | una chiave integrata, che è pubblica |
--redact-key-contains | MCPD_REDACT_KEY_CONTAINS | nessuna: solo le regole integrate |
--redact-max-string | MCPD_REDACT_MAX_STRING | 128 KiB |
--cors-origin | MCPD_CORS_ORIGINS | nessuna: solo la stessa origine |
--request-timeout-seconds | MCPD_REQUEST_TIMEOUT_SECONDS | 30 |
--max-body-bytes | MCPD_MAX_BODY_BYTES | 33554432, cioè 32 MiB |
--max-batch-signals | MCPD_MAX_BATCH_SIGNALS | 10000 |
--log-format | MCPD_LOG_FORMAT | text |
--clickhouse-url | MCPD_CLICKHOUSE_URL | spento: viene usato il file incorporato |
--clickhouse-database | MCPD_CLICKHOUSE_DATABASE | mcpd |
--clickhouse-user | MCPD_CLICKHOUSE_USER | nessuno |
--clickhouse-password | MCPD_CLICKHOUSE_PASSWORD | nessuna |
--postgres-url | MCPD_POSTGRES_URL | spento: viene usato il file incorporato |
--config-file | MCPD_CONFIG_FILE | nessuno |
--forward-endpoint | MCPD_FORWARD_ENDPOINT | spento: non viene inoltrato niente |
--forward-header | MCPD_FORWARD_HEADERS | nessuno |
| nessuna | MCPD_LOG | mcpd=info,mcpd_server=info,tower_http=warn |
--listen
L’indirizzo a cui legarsi. Una sola porta serve gli endpoint OTLP, l’endpoint di ingest nativo, l’API di query, le sonde, le metriche e l’interfaccia, quindi questo è l’unico indirizzo che mcpd ha.
Legarsi a un indirizzo di loopback è la scelta discreta: mcpd avverte
dell’ingest aperto solo quando l’indirizzo a cui si è legato non è di
loopback. Su 0.0.0.0 senza nessuna chiave impostata avverte due
volte all’avvio, una per l’ingest e una per la lettura.
--data
Percorso del file SQLite. Le directory superiori vengono create se non
esistono. Passa :memory: per uno storage che non lascia niente
dietro di sé, il che è giusto per una demo o un test e sbagliato per
qualsiasi cosa tu voglia guardare domani.
--project
Il progetto in cui finisce un segnale quando chi lo invia non ne ha nominato
uno. Un progetto è il confine fra tenant: ogni query è limitata a
uno solo. Il passaggio di conservazione gira su di esso e su ogni progetto
del blocco projects; un progetto che non compare in nessuno dei
due posti non viene mai tagliato, il che conta se chi ti invia i dati nomina
progetti propri.
--retention-days
Cancella i segnali più vecchi di questo numero di giorni. Non impostata conserva tutto, che è il valore predefinito corretto per un portatile e una decisione da prendere di proposito in qualsiasi altro posto.
La finestra va da 1 a 36500 giorni, qui e nel retention_days di
un progetto. Lo zero viene rifiutato all’avvio invece di essere letto come
nessuna retention: metterebbe il taglio ad adesso, e il primo passaggio gira
all’avvio, quindi cancellerebbe tutti i segnali del progetto. Per tenere
tutto, non impostare il valore.
Il passaggio gira su un timer orario invece che al momento della query, così una cancellazione grande non finisce mai dentro la richiesta di un utente. Span, tracce, log e metriche vengono tagliati in base a quando mcpd li ha ricevuti; i punteggi, i feedback e le note di aspetto in base al loro timestamp. Cancellare righe non fa rimpicciolire il file. Vedi Gestione.
--ingest-key, --read-key, --write-key, --admin-key, --execute-key e --worker-key
Sei ambiti, perché portano rischi davvero diversi. Ogni endpoint dichiara
quello che gli serve in spec/mcpd.spec.json, il suo handler è
marcato allo stesso modo, e make check confronta i due, quindi
un ambito documentato qui è l’ambito che il server in esecuzione applica.
| Ambito | Si imposta con | Che cosa apre |
|---|---|---|
ingest | --ingest-key | I quattro endpoint di ingest. Non legge niente. |
read | --read-key | L’API di query: tracce, span, log, metriche, punteggi, analitiche. |
write | --write-key | Punteggi, feedback, dataset, esperimenti, conferme, e la cattura di un’esecuzione dentro un dataset. |
admin | --admin-key | Cancellare una traccia, leggere il registro di audit, e tutto quello che aprono i due ambiti qui sotto. |
execute | --execute-key | Avviare, annullare e segnalare un’esecuzione, e pubblicare un evento verso una. |
worker | --worker-key | Registrare definizioni, prendere in carico i passi e riferirne l’esito. Non legge nessuna traccia. |
La divisione che conta di più è ingest contro tutto il resto.
Una chiave di ingest vive in ogni processo che emette uno span, compresi
quelli che eseguono codice che non hai scritto tu. Dare a quella popolazione
una credenziale capace anche di rileggere ogni prompt archiviato è un errore
che mcpd non rende comodo: una chiave di ingest presentata all’API di
query viene rifiutata.
write è separata da ingest per lo stesso motivo su
scala minore. Catturare un’esecuzione dentro un dataset ne legge l’input e
l’output, quindi è una lettura vestita da scrittura e non va sulla chiave
che porta con sé la tua flotta.
admin è separata da read perché cancellare
un’esecuzione non è leggerla. Finché non imposti --admin-key,
una chiave di lettura cancella oltre che leggere, e mcpd lo dice all’avvio.
execute e worker sono i due ambiti della superficie
delle esecuzioni. Avviare un’esecuzione fa lavorare il worker di qualcuno e
fa spendere denaro, cosa che annotare una traccia non fa, quindi non è
write. Una chiave worker vive nel processo che esegue i corpi
dei passi, che di solito è il processo che tiene la credenziale del
fornitore, quindi apre il protocollo di lease e niente che legga un prompt
archiviato. Finché non imposti le due opzioni, per entrambe vengono
accettate le chiavi di scrittura, e una chiave admin lo è sempre.
Aggiornare senza un’interruzione
Una --write-key non impostata accetta le chiavi di ingest, e una
--admin-key non impostata accetta le chiavi di lettura. Un
deploy configurato prima che questi due ambiti esistessero mantiene quindi
esattamente la portata che aveva, e all’aggiornamento non si rompe niente.
Imposta le due opzioni per prendere le concessioni più strette, e
mcpd config ti dice in quale stato ti trovi: stampa
key required quando l’ambito ha chiavi proprie e
key required (ingest keys) o
key required (read keys) quando le sta ereditando.
Ripeti una qualsiasi delle sei opzioni per più chiavi, che è il modo di
ruotarne una senza un’interruzione. Le variabili d’ambiente prendono una
lista separata da virgole. Chi chiama presenta una chiave come
Authorization: Bearer KEY o come X-Mcpd-Key: KEY,
e il confronto è a tempo costante, quindi non si osserva né la lunghezza né
la posizione della prima differenza.
Una chiave che mcpd non conosce è un 401. Una chiave che mcpd
conosce, usata su un endpoint che il suo ambito non copre, è un
403. Vale la pena tenerli distinti: un 401 ti manda a caccia di
un errore di battitura in una chiave che è perfettamente corretta.
Senza nessuna chiave configurata, ogni ambito è aperto. È voluto, perché la prima esecuzione non deve richiedere una credenziale, ed è il motivo per cui esiste l’avvertimento all’avvio.
Nota che cosa le chiavi non coprono: /healthz,
/readyz, /metrics e l’interfaccia stessa vengono
serviti senza chiave in questa build. Mettili dietro il tuo ingress se la
cosa ti riguarda.
Una voce vuota viene scartata prima che qualcosa legga la lista. Una lista impostata che non contiene nessuna chiave, come MCPD_EXECUTE_KEYS= o un valore vuoto in un Secret di Kubernetes, impedisce al demone di partire, e il rifiuto nomina l’opzione e la variabile; ometti la variabile per lasciare l’ambito non impostato, e una richiesta che invia un’intestazione di chiave vuota non presenta nessuna chiave. Una chiave che è ancora un segnaposto dei file di esempio in deploy/, qualsiasi valore che inizi con REPLACE_WITH o replace-me in maiuscolo o minuscolo, impedisce al demone di partire; il rifiuto nomina l’opzione o la lista del progetto e mai il valore. Lo stesso vale per --redact-key. Genera ogni chiave con openssl rand -hex 32. Senza --write-key, --execute-key né --worker-key, le chiavi di ingest avviano anche esecuzioni e reclamano passi, e mcpd lo segnala all’avvio e in mcpd doctor.
Il registro di audit
Gli handler che restituiscono payload registrano chi ha letto che cosa:
GET /v1/api/traces/{id}, le ricerche su span e log, i
feedback, gli elementi di un dataset, la cattura e la cancellazione. Ogni
riga porta l’ora, un digest breve della chiave che ha agito, l’azione, il
progetto, la risorsa e se l’autorizzazione l’ha permessa. La chiave stessa
non viene mai scritta.
Leggi il registro con GET /v1/api/audit, che richiede l’ambito
admin. Non viene tagliato da --retention-days: un registro che
si cancellasse da solo con lo stesso calendario dei dati che descrive non
potrebbe dire chi ha letto i dati nel frattempo spariti. Scrivere una riga
non fa mai fallire la lettura che descrive; una riga che non si riesce a
scrivere finisce nei log e viene contata come
mcpd_audit_write_failures_total su /metrics, cosa
su cui vale la pena impostare un avviso.
--sample
Una frazione fra 0.0 e 1.0. Un valore fuori da quell’intervallo è un errore
all’avvio, non un troncamento silenzioso: --sample must be between 0.0
and 1.0, got 2.
La decisione si prende facendo l’hash dell’id della traccia, così una flotta di istanze mcpd dietro un bilanciatore conserva e scarta le stesse tracce senza parlarsi. Si prende una volta per traccia e viene ereditata da ogni span di quella traccia nel batch, perché una traccia registrata a metà sembra un bug nel sistema che stai osservando.
L’hash legge i byte finali dell’id della traccia. Certi produttori ricavano i byte iniziali da un timestamp, e un campionatore che guardasse quelli si correlerebbe con l’ora del giorno e scarterebbe in silenzio ore intere.
Il tipo di politica porta anche «conserva sempre gli errori» e «conserva sempre quelle più lente di», ma quelli valgono per una decisione di coda, presa una volta che una traccia è finita. Questa build campiona solo in testa, e nessuna opzione attiva il campionamento di coda. Campionamento tratta il compromesso.
--no-redact, --redact-key, --redact-key-contains e --redact-max-string
L’oscuramento è attivo per impostazione predefinita e gira prima che qualunque cosa venga scritta, così un valore che non sarebbe mai dovuto finire su disco non ci finisce. Cancellarlo dopo non è la stessa garanzia. Tre insiemi di regole sono integrati, confrontati su sottostringhe minuscole del nome di una chiave:
| Regola | Applicata quando la chiave contiene | Che cosa viene archiviato |
|---|---|---|
| Maschera | 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] |
| Parziale | email, phone, ssn,
credit_card, card_number,
iban |
I primi due e gli ultimi due caratteri, ab…yz. Un valore
di sei caratteri o meno viene mascherato del tutto invece di lasciar
trapelare quasi tutto quello che contiene
|
| Hash | user_id, userid | sha256: e 12 byte di un digest con chiave, così puoi
ancora raggruppare per utente senza archiviarne uno
|
Quando una chiave corrispondente contiene un oggetto o un array, viene
oscurato anche tutto quello che le sta sotto, così
{"credentials": {"token": "..."}}
non passa in virtù del solo nome annidato. Qualsiasi singola stringa più
lunga di 128 KiB viene troncata con un marcatore che nomina il numero di
byte, il che limita quello che un prompt fuori controllo fa al tuo disco.
--redact-key è la chiave della regola di hash. Input uguali
danno hash uguali sotto una stessa chiave, ed è questo che fa funzionare il
raggruppamento; ruotare la chiave rompe quel legame di proposito. Impostala
in ogni deploy che fa hash: il valore predefinito integrato sta nel codice
sorgente, quindi i digest senza chiave sono indovinabili.
--redact-key-contains aggiunge una regola di mascheramento tua
sopra alle tre integrate. Le regole integrate conoscono i nomi sotto cui di
solito si archiviano le credenziali; non conoscono il campo in cui i tuoi
strumenti mettono il numero di conto di un cliente. Il valore viene
confrontato come lo sono le regole integrate, come sottostringa minuscola
della chiave, quindi --redact-key-contains ticket maschera sia
ticket_id sia supportTicket, e con loro tutto
quello che sta annidato sotto una chiave corrispondente. Ripeti l’opzione
per averne più di una, oppure imposta
MCPD_REDACT_KEY_CONTAINS con una lista separata da virgole.
--redact-max-string abbassa il limite di lunghezza rispetto ai
128 KiB integrati. Una stringa più lunga del limite viene tagliata lì, con un
marcatore che dice quanti byte sono stati scartati, prima di essere
archiviata.
--no-redact spegne tutta la catena, compreso il limite di
lunghezza. Come variabile d’ambiente è
MCPD_NO_REDACT=true. Ha senso solo quando qualcosa a monte
oscura già, e il daemon si rifiuta di partire se una delle due aggiunte qui
sopra è impostata accanto a esso, perché non c’è niente su cui applicarle.
--runs-data
Dove vive lo storage delle esecuzioni: le definizioni, il giornale dei
tentativi di ogni passo, ogni scadenza e ogni attesa. È un file incorporato
qualunque sia lo storage dei segnali, quindi su Postgres o ClickHouse le
esecuzioni stanno comunque in un solo file accanto al daemon, e la superficie
delle esecuzioni è servita da una replica sola. Per impostazione predefinita
è il percorso di --data con .runs prima
dell’estensione, ./mcpd.runs.db per il valore predefinito, e
:memory: quando lo è --data.
--runs-data off non serve nessuna esecuzione: ogni endpoint
delle esecuzioni risponde 503 nominando questa opzione, e il
resto del daemon resta invariato.
I tempi dello storage stanno nel file di configurazione sotto
runs, in millisecondi interi: lease_ttl_ms,
reclaim_window_ms, stall_window_ms,
idempotency_window_ms, park_ttl_ms e
max_depth. I valori predefiniti, un lease di 30 secondi e una
finestra di recupero di 30 secondi, vanno bene per un daemon su una macchina
e sono quelli che un worker rilegge quando si registra.
--cors-origin
Quali origini del browser possono chiamare l’API di query da un
altro sito. Senza nessuna impostata, una richiesta cross-origin non riceve
nessun header access-control-allow-origin e il browser si
rifiuta di consegnare la risposta alla pagina. È il valore predefinito, ed è
quello che impedisce che un daemon sul tuo portatile con un’API di lettura
aperta sia leggibile da qualsiasi pagina web aperta nello stesso browser.
L’interfaccia inclusa viene servita dall’origine del daemon stesso e non ha
mai bisogno di questa opzione.
Servire l’interfaccia da un altro posto, una CDN o un portale interno, è il
caso per cui questa opzione esiste: nomina quell’origine, come schema e
host, per esempio --cors-origin https://console.example.
Ripetila per averne più di una, oppure imposta
MCPD_CORS_ORIGINS con una lista separata da virgole. Un valore
che non è un’origine, console.example o uno con una barra
finale, rifiuta l’avvio nominando l’opzione invece di lasciarti con
un’interfaccia che non riesce a collegarsi e nessun motivo del perché.
--cors-origin '*' permette ogni origine, che è quello che
facevano le build precedenti senza chiedere.
/healthz, /readyz e /metrics non sono
governati da questa opzione. Non portano nessun dato archiviato, e
l’interfaccia interroga /healthz prima di fare qualsiasi altra
cosa, quindi restano leggibili da qualunque origine.
--request-timeout-seconds
Il tempo massimo che una richiesta può prendere, lettura del corpo compresa,
prima che il daemon risponda al posto suo. Un handler che supera la scadenza
riceve un 503 il cui corpo porta timeout e il nome
di questa opzione. Il valore predefinito è 30 secondi, abbastanza lungo
perché un batch grande venga letto e archiviato, e abbastanza corto perché
uno storage che accetta connessioni e non risponde mai venga segnalato
invece che atteso. Non si può spegnere; alzalo se un’operazione ha davvero
bisogno di più tempo.
--max-body-bytes e --max-batch-signals
I due limiti su una richiesta. --max-body-bytes è il corpo più
grande che il daemon legge, 32 MiB per impostazione predefinita, ed è quello
che decide quanta memoria può prendere una richiesta prima che una sola
parte di essa sia stata decodificata. Un corpo più grande viene rifiutato
con 413 nominando entrambi i numeri, e uno compresso è tenuto
alla stessa cifra dopo essere stato espanso, così un corpo che si espande di
mille volte viene rifiutato invece che espanso.
--max-batch-signals è quanti segnali può portare una richiesta
una volta decodificata, 10.000 per impostazione predefinita. Viene contato su
tutta la richiesta, quindi un array di batch e un flusso NDJSON sono tenuti
allo stesso numero di un singolo batch. Non viene archiviato niente quando
una richiesta lo supera: il rifiuto arriva prima della prima scrittura
invece che dopo metà di esse.
Sono entrambi anche chiavi del file di configurazione,
max_body_bytes e max_batch_signals, e dove sono
impostati tutti e due vince l’opzione. Nessuno dei due si può spegnere, e lo
zero viene rifiutato all’avvio invece che letto come «nessun limite»: un
limite di corpo pari a zero rifiuta ogni richiesta, quindi la lettura che
sembra generosa è quella che manda giù l’ingest.
--log-format e MCPD_LOG
text per un terminale, json per una pipeline di
log. MCPD_LOG prende una stringa di filtro
tracing e non ha un’opzione, perché è un controllo per il debug
più che una decisione di deploy. Impostala a
mcpd_server=debug quando vuoi vedere il dettaglio per singola
richiesta.
Dove si applicano i due limiti di richiesta
Il limite sul corpo si applica a ogni rotta che legge un corpo, API di query compresa. Il limite sui segnali si applica alle porte di ingest, ed ecco che cosa conta a ciascuna di esse.
| Porta | Che cosa conta il limite sui segnali |
|---|---|
POST /v1/ingest | Ogni span, log, metrica, punteggio, feedback e nota di aspetto nella richiesta, sommati su tutta quanta, che siano arrivati come un solo batch, un array di batch o un flusso di righe NDJSON |
POST /v1/traces | Gli span che il mittente ha inviato, contati prima di essere mappati, che è il numero su cui il mittente può agire |
POST /v1/logs | I record di log nell’export |
POST /v1/metrics | I punti dati nell’export, su ogni metrica che contiene |
Sottocomandi
| Comando | Che cosa fa |
|---|---|
mcpd serve | Esegue il daemon. È quello che succede senza nessun sottocomando |
mcpd demo | Scrive nello storage un’esecuzione di esempio realistica, poi serve, così l’interfaccia ha qualcosa dentro la prima volta che la apri |
mcpd config | Stampa la configurazione che questa invocazione userebbe, poi esce |
mcpd mcp | Serve il Model Context Protocol su stdio, così un agente può leggere le proprie tracce da un daemon in esecuzione |
mcpd doctor | Dà un voto a questo deploy e riferisce che cosa è configurato male. Esce con 2 su un guasto, così può bloccare un deploy. Vedi Gestione |
mcpd config esiste per la discussione che stai per avere con un
container. Opzioni, variabili d’ambiente e valori predefiniti finiscono
tutti in un posto solo, e questo stampa quel posto senza avviare un server
né toccare il file dei dati:
$ 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
Leggi le sei righe di autenticazione prima di mettere in esercizio
qualsiasi cosa. open significa aperto a chiunque riesca a
raggiungere la porta. Una riga fra parentesi nomina la lista più larga da cui
quell’ambito sta ereditando, che è lo stato in cui parte un deploy aggiornato
da una release più vecchia.
Un deploy configurato
Qui non c’è niente di specifico di mcpd: le variabili sono ambiente ordinario, quindi funzionano allo stesso modo in Compose, in un manifest Kubernetes e in una unit 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: Le stesse impostazioni come opzioni, per una shell o un file di unit:
mcpd --listen 0.0.0.0:4317 --data /data/mcpd.db --retention-days 30 --ingest-key $KEY --log-format json Che cosa leggono gli SDK
Queste variabili le legge il processo del tuo agente, non il daemon. Sono elencate qui perché i due insiemi condividono i nomi, e un container che esegue sia un agente sia mcpd è configurato da un solo ambiente.
| Variabile | Valore predefinito |
|---|---|
MCPD_ENDPOINT | http://localhost:4317 |
MCPD_API_KEY | non impostata |
MCPD_PROJECT | default |
MCPD_ENVIRONMENT | non impostata |
MCPD_SERVICE_NAME | non impostata |
MCPD_SERVICE_VERSION | non impostata |
MCPD_SAMPLE | 1.0 |
MCPD_ENABLED | true |
MCPD_CAPTURE_PAYLOADS | true |
MCPD_LOG_LEVEL | debug |
MCPD_PROJECT e MCPD_SAMPLE significano la stessa
cosa dalle due parti, che di solito è quello che vuoi: impostale una volta e
l’agente e il daemon vanno d’accordo sul progetto. Il campionamento è quello
su cui riflettere. Un SDK scarta una traccia prima che venga inviata, il che
risparmia la rete. Il daemon la scarta prima che venga archiviata, che è
l’unico posto in cui il tasso si può imporre a mittenti che non controlli.
Imposta il tasso in un posto solo, non in tutti e due, altrimenti stai
filtrando un flusso già filtrato.
MCPD_ENABLED=false rende ogni chiamata di ogni SDK
un’operazione a vuoto che restituisce comunque oggetti validi, così mcpd si
può spegnere nei test senza cambiare una riga di codice.
MCPD_CAPTURE_PAYLOADS=false tiene prompt e completion dentro il
processo: gli span arrivano comunque con i loro tempi, il consumo, il
modello e gli errori, ma input e output vengono
scartati prima di essere messi in coda, quindi non attraversano mai la rete.
L’oscuramento sul daemon è l’altro capo dello stesso filo, e il capo
sbagliato per una squadra che non deve proprio inviare quei byte. Il
dettaglio per linguaggio sta su ogni
pagina di SDK.
--config-file
Un file JSON che tiene le impostazioni che non stanno su una riga di comando: i monitor, gli obiettivi di livello di servizio, l’intervallo su cui vengono valutati, l’inoltro, il giudice, i tempi dello storage delle esecuzioni, i due limiti di richiesta e il blocco per progetto qui sotto. JSON invece di YAML o TOML perché mcpd analizza già JSON per tutto il resto, quindi il file non aggiunge nessuna dipendenza e nessuna seconda sintassi da imparare. Si monta come una ConfigMap Kubernetes senza modifiche.
max_body_bytes e max_batch_signals stanno qui oltre
che sulla riga di comando, così una flotta può tenerli accanto ai monitor con
cui viene rilasciata. Un’opzione batte il file, ed è così che una singola
istanza prende un limite diverso senza che il file condiviso venga modificato
per tutti.
Una chiave sconosciuta fa fallire l’avvio invece di essere ignorata, perché un errore di battitura che in silenzio non guarda niente è il peggiore degli esiti disponibili.
Lo stesso vale per un valore con cui un timer in background non può girare:
un objective che non sia almeno 0 e minore di 1, un
window_days di un obiettivo fuori da 1 a 365, un
monitor_interval_seconds sopra 86400, un
aspects.window_seconds fuori da 1 a 31536000, un
aspects.interval_seconds sopra 604800 e un
judge.timeout_seconds fuori da 1 a 300. Ogni rifiuto nomina la
chiave e l’intervallo.
{
"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
}
} I progetti, e le chiavi che li raggiungono
Un progetto è la chiave di tenancy. Elencare delle chiavi sotto uno è quello che lo rende vero invece che decorativo: una chiave nel blocco di un progetto raggiunge quel progetto e nessun altro, così un solo daemon serve più squadre senza che nessuna di esse possa leggere i prompt delle altre.
{
"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
}
}
}
Tutte e sei le liste esistono per progetto, come
ingest_keys, read_keys, write_keys,
admin_keys, execute_keys e worker_keys, e ricadono allo stesso modo: un progetto senza
write_keys accetta le proprie chiavi di ingest, e uno senza
admin_keys accetta le proprie chiavi di lettura. Il blocco
support qui sopra non imposta né l’una né l’altra, quindi si
comporta esattamente come si comportava prima che i due ambiti esistessero.
Una chiave impostata con una qualsiasi delle sei opzioni di primo livello raggiunge ogni progetto. È il percorso a singolo tenant, e resta quello semplice: se non apri mai questo blocco non ne incontri mai niente.
retention_days e sample ricadono sulle opzioni
globali quando un progetto non le imposta. Sono entrambe per progetto per lo
stesso motivo per cui lo sono le chiavi: il requisito di novanta giorni di
una squadra non dovrebbe obbligare a conservare i dati di tutti gli altri, e
il traffico di una squadra non dovrebbe decidere come viene campionata
un’altra.
Un batch può nominare più progetti. Una chiave di ingest limitata scrive i segnali che raggiunge e rifiuta il resto, riportando il rifiuto nella risposta invece di scartarli in silenzio, così una flotta puntata sul progetto sbagliato si vede dalla risposta che sta già leggendo.
Una richiesta di lettura per un progetto che la tua chiave non raggiunge viene rifiutata senza dire se quel progetto esiste. Su un daemon condiviso i nomi dei progetti sono spesso i nomi di squadre o di clienti.
I monitor e gli obiettivi di livello di servizio sono trattati in
Monitor e avvisi; gli
obiettivi vanno in un array slos accanto a
monitors e vengono valutati sullo stesso timer. Il blocco
judge attiva la valutazione giudicata da un modello ed è
trattato in
Test di regressione. È
spento finché non lo imposti, e uno scorer giudicato non gira mai a meno che
una richiesta non lo nomini, perché dare un voto costa denaro a ogni
esecuzione che legge.
{
"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 è ogni quanto vengono valutati i monitor e gli obiettivi. Vale 60 per impostazione predefinita, e un valore sotto 5 viene alzato a 5.
derived_metrics
Quali metriche il demone ricava dagli span che memorizza. Le due metà sono attive quando il blocco manca. durations scrive un punto di durata per ogni span concluso di agente, strumento, workflow, generazione di modello o processore. tokens scrive fino a tredici contatori di token per ogni span di generazione di modello che ha dichiarato il proprio uso. Imposta l’una o l’altra a false per smettere di scriverla; un campo omesso resta attivo. Non c’è un’opzione per questo. I punti ricavati si aggiungono dopo il tetto per progetto e non contano nel numero accepted di una risposta di ingest.
{
"derived_metrics": { "durations": true, "tokens": false }
} aspects
Se il demone raggruppa le note di aspetto in temi, e entro quali limiti. Spento quando il blocco manca. enabled accende il passaggio e non richiede modello né credenziale, perché il demone scrive da sé l’aspetto outcome. interval_seconds è ogni quanto gira un passaggio, 300 per impostazione predefinita, e un valore sotto 30 viene alzato a 30. window_seconds è fin dove un passaggio legge le note, 86400 per impostazione predefinita. disabled nomina gli aspetti che nessun passaggio raggruppa; le note già registrate sotto uno di essi restano leggibili.
{
"aspects": {
"enabled": true,
"interval_seconds": 300,
"window_seconds": 86400,
"disabled": ["sentiment"]
}
} --forward-endpoint
Copia ogni span archiviato verso un altro endpoint OTLP/HTTP. Dai l’URL di
base; /v1/traces viene aggiunto in coda. La scrittura locale
avviene sempre per prima, quindi una destinazione ferma non ti costa niente.
Questa opzione batte il blocco forward nel file di
configurazione, così una singola istanza può essere puntata altrove senza
modificare una ConfigMap condivisa. Vedi
Inoltro.
--forward-header
Un header inviato con ogni batch inoltrato, scritto come
Name: value. Ripeti l’opzione per averne più di uno. La
variabile d’ambiente prende una lista separata da virgole.
Un valore senza i due punti viene rifiutato all’avvio, con la forma che
funziona, invece di essere scartato. Un header nello stile di OpenTelemetry,
key=value, qui si scrive key: value.
--clickhouse-url
L’endpoint HTTP di un server ClickHouse, per esempio
http://clickhouse:8123. Impostarlo commuta lo storage dal file
incorporato a ClickHouse, e --data smette di essere letta. Lo
schema viene creato all’avvio.
È l’impostazione che toglie i limiti di scrittore unico e di replica unica dello storage incorporato. Passaci quando la conservazione si misura in mesi di traffico di produzione, e non prima: Gestione tratta il compromesso.
--clickhouse-database, --clickhouse-user, --clickhouse-password
Il database in cui creare le tabelle, e le credenziali con cui raggiungerlo. Un utente senza password viene inviato senza, il che è giusto per un container su una rete privata e sbagliato in qualsiasi altro posto.
--postgres-url
Una stringa di connessione Postgres, per esempio
postgres://mcpd:secret@db:5432/mcpd. Impostarla commuta lo
storage dal file incorporato a Postgres, e --data smette di
essere letta. Lo schema viene creato all’avvio.
È l’opzione di mezzo. Toglie gli stessi limiti di scrittore unico e di
replica unica che toglie ClickHouse, senza chiederti di far girare un
database che non hai già. Nominare sia questa sia
--clickhouse-url fa fallire l’avvio invece di sceglierne una.
La migrazione prende un lock consultivo, quindi più repliche che partono insieme non sono un problema, e un avvio senza niente da applicare non tocca nessuno schema.
Per TLS, aggiungi sslmode=require alla stringa di connessione.
mcpd allora cifra la connessione e verifica sempre il certificato del server.
Lo controlla rispetto alle autorità di certificazione pubbliche, o a quella in
sslrootcert=/percorso/ca.pem, che è ciò che serve a un servizio
gestito con una propria autorità. verify-ca e
verify-full sono letti come require. Con
prefer, il predefinito, o con disable, la
connessione non è cifrata.
Registrare chi, non solo quale chiave
Il registro di audit registra un digest della chiave che ha agito. Questo risponde a «una chiave admin ha cancellato questa esecuzione» e non a «quale persona lo ha fatto». mcpd non ha un identity provider, quindi l’identità deve arrivare dall’ingress che gli sta già davanti.
Dagli l’header che il tuo proxy imposta, e gli indirizzi autorizzati a impostarlo:
prendi l’identità da un ingress di cui ti fidi
mcpd serve \
--trusted-identity-header x-forwarded-user \
--trusted-proxy-cidr 10.42.0.0/16 Servono entrambe le metà, e la lista di indirizzi è il motivo per cui questo è sicuro. Un header è una stringa che chiunque riesca ad aprire un socket verso la porta può inviare, quindi mcpd lo legge solo da un indirizzo che hai nominato tu. Una richiesta che arriva da qualsiasi altro posto viene servita normalmente e la sua riga di audit non porta nessun soggetto, che è la risposta onesta invece di una supposizione. Il valore ha anche un limite di lunghezza e viene ripulito dai caratteri di controllo, perché un header è influenzabile da un attaccante anche quando il proxy non lo è.
Senza nessuna delle due opzioni impostate non cambia niente e non viene mai registrato nessun soggetto. È il valore predefinito.
Un progetto non può spendersi tutto il daemon
Un progetto che inonda mcpd costa a ogni altro progetto che ci sta sopra: il
disco si riempie, la conservazione non riesce a stare dietro, e la squadra
che non ha fatto niente di male è quella che perde esecuzioni.
--quota-per-minute è il tetto che ferma tutto questo.
un tetto per ogni progetto, e uno più alto per uno di essi
mcpd serve --quota-per-minute 10000
Non impostata significa nessun tetto, quindi un deploy che non la imposta mai
si comporta esattamente come si comportava prima che le quote esistessero.
Impostata, copre tre cose: ogni progetto del blocco projects che
non nomina un proprio quota_per_minute, il progetto nominato da
--project, e ogni progetto che la configurazione non
elenca affatto.
Quest’ultima è la parte che ne fa un tetto invece che un suggerimento. Il nome del progetto arriva dentro il corpo della richiesta, quindi un mittente che sceglie un nome che nessun file di configurazione elenca prima non veniva addebitato a niente e poteva archiviare senza limite. Un limite da cui il mittente esce inventandosi una stringa non è un limite.
Ogni progetto non elencato condivide un solo contatore invece di averne uno a
testa. Un contatore per nome sarebbe un’allocazione che qualsiasi mittente
potrebbe pilotare inventando nomi, che è lo stesso buco in un’altra forma.
Condividere significa che un’inondazione sparsa su mille nomi inventati è
limitata in totale da questo numero, e significa che un progetto che merita
una dotazione propria va nominato in projects, dove ne riceve
una.
Che cosa limita davvero il tetto
Il contatore è del tipo a due finestre pesate: il conteggio di questo minuto, più quello del minuto precedente pesato per quanta parte di esso la finestra che finisce adesso copre ancora. Tiene il tasso sostenuto al tetto e permette una raffica fino a un tetto intero sopra di esso, perché un SDK che svuota una coda piena arriva come una sola raffica e un contatore che la rifiutasse rifiuterebbe il traffico ordinario.
Quelle due cose insieme hanno una conseguenza aritmetica che vale la pena dire invece che nascondere. Un progetto può archiviare poco meno del doppio del suo tetto dentro un intervallo di 60 secondi: un tetto pieno proprio alla fine di una finestra, e un tetto pieno di nuovo alla fine di quella successiva. Non è un artefatto dell’approssimazione. Qualsiasi limitatore che lasci a un progetto spendere il suo tetto in una volta e di nuovo il suo tetto ogni minuto dopo permette questo attraverso un confine, e l’unico modo di toglierlo è rifiutare la raffica.
La finestra è di un minuto e non è configurabile. L’opzione nomina l’unità, quindi una seconda impostazione che cambiasse il significato di «al minuto» renderebbe illeggibile il primo numero.
Che fine fa l’eccedenza
I segnali sopra il tetto vengono rifiutati e riportati, mai scartati in silenzio. Il batch non viene rifiutato per intero: mcpd archivia quello che ci sta e ti dice quello che non ci stava.
- Gli span vanno per traccia intera. Mezza esecuzione ha span il cui genitore nessuno ha archiviato, il che si legge come un’esecuzione rotta invece che come una mancante. Una traccia troppo grande per quello che resta viene saltata e si prova la successiva, così una sola esecuzione grande non spende per niente il resto della dotazione.
- Log, metriche, punteggi e feedback stanno da soli, quindi riempiono quello che resta nell’ordine di arrivo.
- Viene addebitato solo quello che viene archiviato. Il taglio conserva tracce intere e quindi può archiviare meno segnali di quanti gliene fossero permessi, e la differenza viene restituita. Senza questo, un progetto le cui esecuzioni sono più grandi della dotazione che gli resta pagherebbe fino a zero senza archiviare niente.
- Gli altri progetti nella stessa richiesta non vengono toccati. Un progetto sopra il suo tetto non costa a un altro i segnali che ha inviato insieme a esso.
La risposta porta il conteggio in rejected e una riga in
errors che nomina il progetto e il suo tetto. Un progetto che la
configurazione non ha mai elencato riceve una frase diversa, perché sentirsi
dire che il progetto analytics-v2 è sopra il suo tetto ti manda
a cercare un’impostazione che non esiste quando la risposta è aggiungere il
progetto:
$ curl -sX POST "$MCPD/v1/ingest" -H content-type:application/json -d @batch.json | jq{ "accepted": 5, "rejected": 3, "duplicate": 0, "errors": [ { "signal": "span", "index": 0, "message": "3 signal(s) named project analytics-v2, which no configuration lists. Every unlisted project shares one ceiling of 5 per minute and it was full, so they were not stored. Add analytics-v2 to the `projects` block to give it an allowance of its own, or raise --quota-per-minute." } ]}
Un rifiuto viene contato in mcpd_signals_over_quota_total oltre
che in mcpd_signals_rejected_total, così una flotta che sta
venendo limitata si distingue da una flotta che invia dati malformati. Vedi
Gestione.
Due cose da controllare quando lo imposti
Il tetto copre ogni porta che archivia un segnale: quelle a batch,
/v1/ingest, /v1/traces, /v1/logs e
/v1/metrics, e quelle a record singolo,
POST /v1/api/scores e POST /v1/api/feedback. Le due
rispondono di proposito in modo diverso a un progetto sopra il suo tetto. Una
porta a batch archivia quello che ci sta e riporta il resto come
rejected con un motivo, perché un batch può nominare più
progetti e rifiutare tutta la richiesta punirebbe quelli che stanno dentro il
loro tetto. Una porta a record singolo non ha nessuna risposta parziale da
dare, quindi rifiuta con 429 e un header
Retry-After che porta i secondi che mancano perché il contatore
di quel progetto ammetta un altro segnale. Il numero viene dal contatore
stesso, quindi un client che lo rispetta torna quando c’è posto invece che
quando è scaduto un ritardo fisso.
Una porta di scrittura non viene addebitata al tetto:
POST /v1/api/score, che fa girare gli scorer su esecuzioni
archiviate. Consegna i punteggi che ha prodotto direttamente allo storage,
quindi un passaggio finisce su disco per un progetto il cui tetto è già
speso, e quelle righe non vengono contate né da
mcpd_signals_over_quota_total né da
mcpd_signals_unbounded_total. È una porta che apri tu invece di
una su cui una flotta punta, quindi tratta il tetto come un limite su quello
che i mittenti possono spingere dentro, non su tutto quello che può
raggiungere il disco.
Un tetto sotto --max-batch-signals, 10.000 per impostazione
predefinita, significa che un solo batch legittimo può superare la dotazione
di un minuto intero e viene tagliato per quanto bene si comporti il mittente.
mcpd doctor dà un voto a questo sotto limits e
nomina i progetti a cui si applica. Avverte anche quando un tetto è impostato
per progetto e non ce n’è uno di primo livello accanto, che è la
configurazione che lascia senza limite ogni progetto non elencato.
mcpd config stampa per nome i progetti limitati, così puoi
rileggere che cosa copre l’impostazione invece di dedurlo. Stampa solo i
tetti nominati, quindi conferma quello condiviso dall’opzione stessa.