Vai al contenuto
mcpd
nessun daemon
Codice

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.

Ingest pipeline
  1. Decode

    OTLP or native, gzip or plain

  2. Sample

    Decided once, at the root

  3. Redact

    Before anything is written

  4. Price

    From the configured price book

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

  1. mcpd.span.kind, quando nomina un tipo che mcpd conosce.
  2. openinference.span.kind.
  3. gen_ai.operation.name.
  4. La presenza di un attributo che porta un solo tipo di span.
Valore dell’attributo Tipo mcpd
openinference.span.kind=LLMmodel_generation
openinference.span.kind=CHAINworkflow_run
openinference.span.kind=TOOLtool_call
openinference.span.kind=AGENTagent_run
openinference.span.kind=RETRIEVERretrieval
openinference.span.kind=EMBEDDINGrag_embedding
openinference.span.kind=RERANKERrag_action
openinference.span.kind=GUARDRAILguardrail
openinference.span.kind=EVALUATORscorer_run
gen_ai.operation.name=chatmodel_generation
gen_ai.operation.name=text_completionmodel_generation
gen_ai.operation.name=generate_contentmodel_generation
gen_ai.operation.name=embeddingsrag_embedding
gen_ai.operation.name=execute_tooltool_call
gen_ai.operation.name=invoke_agentagent_run
gen_ai.operation.name=create_agentagent_run
uno qualunque fra gen_ai.system, llm.model_name, llm.provider presentemodel_generation
mcp.tool.name presentemcp_tool_call
http.request.method oppure http.method presentehttp_request
qualunque altra cosageneric

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/scores e POST /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