OpenTelemetry
Se il tuo agente emette già OpenTelemetry, mcpd è un cambio di endpoint. Questa pagina è tutto il percorso OTLP: i tre endpoint, la codifica, e ogni attributo che mcpd legge.
I tre endpoint
mcpd serve gli endpoint OTLP su HTTP sulla stessa porta della sua API di lettura e della sua interfaccia. Non c’è nessun collector da far girare davanti, nessun secondo container e nessuna porta di ricezione a parte.
| Endpoint | Accetta | Risponde con |
|---|---|---|
POST /v1/traces | ExportTraceServiceRequest | partialSuccess.rejectedSpans |
POST /v1/logs | ExportLogsServiceRequest | partialSuccess.rejectedLogRecords |
POST /v1/metrics | ExportMetricsServiceRequest | partialSuccess.rejectedDataPoints |
Ogni export riuscito risponde con la forma partialSuccess
propria di OTLP, che porta un conteggio. È così che un collector dice a chi
manda di avere scartato qualcosa, e gli exporter la leggono. Restituire un
corpo vuoto lascerebbe che chi manda riferisca un successo che non ha
ottenuto.
Puntaci un exporter
Il lavoro lo fanno due variabili. Impostale nell’ambiente del processo che fa girare il tuo agente, e non cambia nient’altro.
exporter
# L'URL di base. Il percorso /v1/traces lo attacca l'exporter.
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
# Facoltativo. Manda gzip se i tuoi payload sono grossi.
export OTEL_EXPORTER_OTLP_COMPRESSION=gzip
# Solo quando mcpd gira con --ingest-key.
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Bearer YOUR_INGEST_KEY"
Vale la pena conoscere due dettagli prima di metterti a cercare la ragione
di un silenzio. Primo,
OTEL_EXPORTER_OTLP_ENDPOINT è un URL di base e l’exporter ci
attacca in fondo il percorso del segnale. Imposta invece
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT se vuoi dare l’URL intero,
percorso compreso. Secondo, in OpenTelemetry la porta 4317 è per convenzione
quella gRPC e la 4318 quella HTTP. mcpd serve HTTP sulla 4317 perché serve
tutto su una porta sola, quindi un exporter lasciato sulla porta predefinita
manda alla 4318 e tu non vedi arrivare niente. Nomina la porta in modo
esplicito.
Tutte e due le codifiche, per le tracce
/v1/traces legge l’header content-type e accetta
l’una o l’altra codifica: OTLP/protobuf quando contiene
protobuf, OTLP/JSON altrimenti. Protobuf è il formato
predefinito di OpenTelemetry, quindi un SDK non modificato e puntato su
mcpd manda tracce che atterrano senza nessuna configurazione.
/v1/logs e /v1/metrics accettano solo JSON. Lì un
corpo protobuf viene rifiutato come body is not valid JSON. Se
stai mandando tutti e tre i segnali, imposta il protocollo una volta sola,
globalmente:
json per ogni segnale
export OTEL_EXPORTER_OTLP_PROTOCOL=http/json
Quando non arriva nessun content-type, mcpd assume
application/json.
Compressione e dimensione
Il corpo di una richiesta può arrivare con
content-encoding: gzip, con identity, o senza
header di codifica. Qualunque altra cosa viene rifiutata per nome:
content-encoding "br" is not supported; use gzip or send the body
uncompressed. Le risposte sono compresse per negoziazione del
contenuto, quindi anche una traccia grossa riletta su una linea lenta
viaggia compressa.
A una richiesta si applicano due limiti. Il limite sul corpo è 32 MiB ed è
misurato su quello che arriva, quindi gzip ti compra spazio. Il limite sul
batch è 10.000 segnali, contati dopo la decodifica su ogni ingresso di
ingest: gli span su /v1/traces, i record su
/v1/logs, i punti dati su /v1/metrics e tutto
quello che c’è nella richiesta su /v1/ingest. Un batch più
grande viene rifiutato con 413 payload_too_large, che nomina
tutti e due i numeri. I due limiti sono impostazioni:
--max-body-bytes e --max-batch-signals, descritte
in Configurazione.
Che cosa succede fra l’arrivo e l’archiviazione
Uno span OTLP e uno span nativo fanno lo stesso percorso. È a questo che serve avere una pipeline in comune: l’ordine qui sotto non può divergere fra i due ingressi.
-
Decode
OTLP or native, gzip or plain
-
Sample
Decided once, at the root
-
Redact
Before anything is written
-
Price
From the configured price book
-
Store
One transaction per batch
Redaction is stage three, not a cleanup job. A value that a rule matches never reaches storage at all.
La valorizzazione all’ingest è la ragione per cui uno span OTLP riceve una cifra di costo. mcpd lo valorizza con il listino con cui questo server sta girando, nel momento in cui arriva, così cambiare le tue tariffe dopo non può riscrivere in silenzio quanto è costato il mese scorso. Vedi Contabilità dei costi.
Tutte e due le convenzioni semantiche
Per gli span di IA sono in uso due insiemi di nomi di attributo in
concorrenza fra loro: i gen_ai.* di OpenTelemetry e gli
llm.* di OpenInference con
openinference.span.kind. mcpd li legge tutti e due, e sopra
quelli i nomi mcpd.* suoi. Tu non rinomini niente.
Ogni riga qui sotto è letta in ordine, vince la prima che corrisponde. Un attributo che nessuna riga nomina viene comunque tenuto: sullo span è archiviata l’intera mappa appiattita degli attributi, quindi niente di quello che mandi va perso per non essere stato mappato.
Modello, payload e consumo
| Campo mcpd | Letto da, in ordine |
|---|---|
model | gen_ai.request.model, gen_ai.response.model, llm.model_name, mcpd.model |
provider | gen_ai.system, llm.provider, mcpd.provider |
input | input.value, gen_ai.prompt, mcpd.input |
output | output.value, gen_ai.completion, mcpd.output |
entity_name | gen_ai.agent.name, mcpd.entity.name |
usage.input_tokens | gen_ai.usage.input_tokens, gen_ai.usage.prompt_tokens, llm.token_count.prompt, mcpd.usage.input_tokens |
usage.output_tokens | gen_ai.usage.output_tokens, gen_ai.usage.completion_tokens, llm.token_count.completion, mcpd.usage.output_tokens |
usage.total_tokens | llm.token_count.total, gen_ai.usage.total_tokens |
input_details.cache_read | gen_ai.usage.cache_read_input_tokens, llm.token_count.prompt_details.cache_read, mcpd.usage.cache_read_tokens |
input_details.cache_write | gen_ai.usage.cache_creation_input_tokens, llm.token_count.prompt_details.cache_write, mcpd.usage.cache_write_tokens |
input_details.audio | llm.token_count.prompt_details.audio |
output_details.reasoning | llm.token_count.completion_details.reasoning, gen_ai.usage.reasoning_tokens |
output_details.audio | llm.token_count.completion_details.audio |
Uno span che non ha nessuno dei tre conteggi di token non porta nessun oggetto di consumo, invece di portarne uno pieno di zeri. Zero token di input e token di input sconosciuti sono due fatti diversi, e un rapporto sui costi costruito sul primo quando valeva il secondo è sbagliato in un modo che nessuno nota.
Correlazione
I campi di correlazione sono quelli che ti permettono di chiedere le esecuzioni di una persona, o quelle di una sessione. Vengono letti prima gli attributi dello span, poi quelli della risorsa.
| Campo mcpd | Letto da, in ordine |
|---|---|
project | mcpd.project sullo span, poi sulla risorsa, poi il --project del daemon |
environment | mcpd.environment, deployment.environment.name della risorsa, deployment.environment della risorsa |
service_name | service.name della risorsa |
service_version | service.version della risorsa |
session_id | gen_ai.conversation.id, session.id, mcpd.session_id |
thread_id | mcpd.thread_id, thread.id |
user_id | user.id, enduser.id, mcpd.user_id |
request_id | mcpd.request_id, http.request.id |
release | mcpd.release, service.version della risorsa |
run_id, resource_id, experiment_id | mcpd.run_id, mcpd.resource_id, mcpd.experiment_id |
tags | mcpd.tags, come array di stringhe |
Stato ed errori
Il codice di stato OTLP 1 diventa ok e il
2 diventa error. Uno stato non impostato con un
tempo di fine viene registrato come ok, perché uno span che ha
finito senza lamentarsi è andato bene. Uno stato non impostato senza tempo
di fine resta unset: lo span sta ancora girando, e mcpd lo
archivia così invece di inventarsi un esito.
Su uno span in errore, il messaggio viene da status.message,
poi da exception.message, e, se mancano entrambi, dalla stringa
letterale unspecified error. Il tipo di errore viene da
exception.type e lo stack da
exception.stacktrace. Uno span in errore porta sempre un
messaggio, perché «qualcosa è fallito» senza testo non è qualcosa su cui puoi
agire.
Tipo di span
Il tipo decide come viene disegnato uno span nel waterfall. mcpd lo deduce
in ordine di specificità, e ripiega su generic, che è sempre
sicuro. Etichettare uno span in modo sbagliato è peggio che rinunciare a
etichettarlo.
mcpd.span.kind, quando nomina un tipo che mcpd conosce.openinference.span.kind.gen_ai.operation.name.- La presenza di un attributo che porta un solo tipo di span.
| Valore dell’attributo | Tipo mcpd |
|---|---|
openinference.span.kind=LLM | model_generation |
openinference.span.kind=CHAIN | workflow_run |
openinference.span.kind=TOOL | tool_call |
openinference.span.kind=AGENT | agent_run |
openinference.span.kind=RETRIEVER | retrieval |
openinference.span.kind=EMBEDDING | rag_embedding |
openinference.span.kind=RERANKER | rag_action |
openinference.span.kind=GUARDRAIL | guardrail |
openinference.span.kind=EVALUATOR | scorer_run |
gen_ai.operation.name=chat | model_generation |
gen_ai.operation.name=text_completion | model_generation |
gen_ai.operation.name=generate_content | model_generation |
gen_ai.operation.name=embeddings | rag_embedding |
gen_ai.operation.name=execute_tool | tool_call |
gen_ai.operation.name=invoke_agent | agent_run |
gen_ai.operation.name=create_agent | agent_run |
uno qualunque fra gen_ai.system, llm.model_name, llm.provider presente | model_generation |
mcp.tool.name presente | mcp_tool_call |
http.request.method oppure http.method presente | http_request |
| qualunque altra cosa | generic |
Un tipo di span che mcpd non riconosce sotto
openinference.span.kind o
gen_ai.operation.name ricade su generic invece di
essere rifiutato. L’elenco completo dei tipi è nel
modello dei dati.
Log e metriche
I record di log vengono mappati per numero di gravità, nel modo che
prescrive la specifica: da 1 a 4 è trace, da 5 a 8 debug, da 9 a 12 info, da
13 a 16 warn, da 17 a 20 error, e tutto quello che sta sopra è fatal. Senza
numero di gravità, un record è trattato come info. Il messaggio viene da
body.stringValue, il nome del logger dal nome dello scope, e
traceId e spanId sono tenuti quando sono validi,
ed è quello che mette una riga di log accanto allo span che l’ha emessa.
Per le metriche, le somme diventano contatori e i gauge restano gauge. Gli istogrammi sono registrati con la loro somma e il loro conteggio, tenendo i confini dei bucket: mcpd non finge di poter ricavare di nuovo un quantile esatto da un istogramma già aggregato. Gli exemplar vengono tenuti, ed è quello che permette di aprire, da un picco su un grafico, l’esecuzione che lo ha causato. Quasi tutti gli strumenti li buttano via.
Gli id, e il traceparent W3C
mcpd usa esattamente le forme di identificatore di OpenTelemetry: un id di traccia è 128 bit come 32 caratteri esadecimali minuscoli, un id di span è 64 bit come 16. Tenere identico il formato di trasmissione è quello che toglie di mezzo lo strato di traduzione. L’id tutto a zeri è riservato e vuol dire «nessuna traccia», quindi viene rifiutato ovunque compaia, compresi gli id che mcpd genera da sé.
OTLP/JSON prescrive l’esadecimale per i campi id, ma diversi SDK emettono base64. mcpd accetta tutti e due e archivia esadecimale. Uno span i cui id mancano, sono della lunghezza sbagliata o sono tutti a zeri viene saltato e il resto del batch viene comunque scritto, perché un collector che rifiuta diecimila span buoni per via di uno cattivo è un collector che la gente spegne.
I timestamp sono nanosecondi dall’epoca Unix. Superano 253,
quindi OTLP/JSON li manda come stringhe, e mcpd li legge come stringhe.
Leggerli in virgola mobile perde precisione, e la cosa si vede come span che
sembrano partire a microsecondi di distanza quando non era così. Un
endTimeUnixNano a 0 o assente vuol dire che lo
span non ha finito.
Un header traceparent è
version-traceid-parentid-flags. mcpd lo interpreta con le
regole che fissa la specifica: la versione ff viene rifiutata,
la versione 00 deve avere esattamente quattro campi, le
versioni successive possono portarne di più, gli id devono essere validi, e
il bit 0 del byte dei flag è il flag di campionamento. Un header malformato
non è mai una ragione per rifiutare una richiesta. mcpd apre invece una
traccia nuova.
Dove questo conta: gli SDK di mcpd leggono un traceparent in
entrata e ne emettono uno sulle chiamate in uscita, così una traccia
sopravvive al confine fra due servizi. Gli endpoint OTLP prendono i loro id
dal payload, perché il tuo exporter li ha già messi negli span. Se una
traccia comincia in un servizio che non strumenti con mcpd, propaga il
suo traceparent e le due metà atterrano sotto un solo id di
traccia.
Una richiesta, per intero
Niente di quello che c’è sopra ha bisogno di un SDK. Questo è un export completo e valido che puoi incollare in un terminale:
OTLP/JSON grezzo
curl -X POST http://localhost:4317/v1/traces \
-H 'content-type: application/json' \
-d '{
"resourceSpans": [{
"resource": { "attributes": [
{ "key": "service.name", "value": { "stringValue": "checkout-agent" } }
]},
"scopeSpans": [{
"spans": [{
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736",
"spanId": "00f067aa0ba902b7",
"name": "chat gpt-4o",
"startTimeUnixNano": "1700000000000000000",
"endTimeUnixNano": "1700000001500000000",
"status": { "code": 1 },
"attributes": [
{ "key": "gen_ai.system", "value": { "stringValue": "openai" } },
{ "key": "gen_ai.request.model", "value": { "stringValue": "gpt-4o" } },
{ "key": "gen_ai.operation.name", "value": { "stringValue": "chat" } },
{ "key": "gen_ai.usage.input_tokens", "value": { "intValue": "1200" } },
{ "key": "gen_ai.usage.output_tokens", "value": { "intValue": "350" } }
]
}]
}]
}]
}'
# {"partialSuccess":{"rejectedSpans":0,"errorMessage":""}} Che cosa OTLP non sa portare
OTLP ha una forma per span, log e metriche. Non ne ha nessuna per un punteggio automatico, per il pollice giù di una persona o per una nota di aspetto, e non ha vocabolario per il dettaglio dei token su cui fattura un provider moderno. Due strade per mandare queste cose:
-
POST /v1/ingest, l’endpoint di batch nativo di mcpd, che porta l’intero modello dei dati in una richiesta sola. -
POST /v1/api/scoresePOST /v1/api/feedback, che si attaccano a una traccia che hai già mandato su OTLP.
Mescolare le due cose è normale e previsto. Esporta gli span con l’SDK OpenTelemetry che fai già girare, e manda i punteggi dal tuo job di valutazione. Si uniscono sull’id di traccia. Vedi Punteggi e feedback e l’API HTTP.
Prima di esporre la porta
Senza nessun --ingest-key impostato, chiunque riesca a
raggiungere la 4317 può scriverci, e senza --read-key può
leggere ogni prompt archiviato. mcpd scrive un avvertimento all’avvio quando si
mette in ascolto su un indirizzo che non è di loopback con uno dei due
lasciato aperto. Impostali dalla
configurazione, poi
manda la chiave come Authorization: Bearer, che è quello che fa
OTEL_EXPORTER_OTLP_HEADERS qui sopra.
avviare con le chiavi
mcpd --ingest-key $INGEST_KEY --read-key $READ_KEY