Vai al contenuto
mcpd
nessun daemon
Codice

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 config
$ MCPD_INGEST_KEYS=k1,k2 mcpd --retention-days 30 --sample 0.1 configlisten          0.0.0.0:4317storage         sqlite at ./mcpd.dbconfig file     nonelog format      textproject         defaultprojects        noneretention       30 daysingest auth     key requiredread auth       openwrite auth      key required (ingest keys)admin auth      openexecute auth    key required (ingest keys)worker auth     key required (ingest keys)runs            sqlite at ./mcpd.runs.dbrun settings    lease_ttl_ms 30000, reclaim_window_ms 30000, stall_window_ms 300000, idempotency_window_ms 86400000, park_ttl_ms 86400000, max_depth 8, max_step_repeats 16identity        offsampling        deterministic 0.1quota           unlimitedredaction       onredact key      built in, which is public; set --redact-keycors            same-origin onlytimeout         30s per requestlimits          32 MiB per request, 10000 signals per batchderived         duration and token metricsaspects         offmonitors        noneobjectives      nonejudge           offforwarding      off

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_ENDPOINThttp://localhost:4317
MCPD_API_KEYnon impostata
MCPD_PROJECTdefault
MCPD_ENVIRONMENTnon impostata
MCPD_SERVICE_NAMEnon impostata
MCPD_SERVICE_VERSIONnon impostata
MCPD_SAMPLE1.0
MCPD_ENABLEDtrue
MCPD_CAPTURE_PAYLOADStrue
MCPD_LOG_LEVELdebug

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:

otto segnali verso un progetto che nessuno ha configurato, con il tetto condiviso a cinque
$ 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.